更新于 2024-03-01。主要是 wasm-pack-cli 被弃用。
WASM 是一种虚拟机上的二进制格式,不限语言,跨平台,能够运行在 Web 页面上。相对于 js 而言,WASM 的效率更高,更适合处理高计算量任务。至于为什么会有在浏览器做高计算量的工作…你总会找到场景的,压缩、渲染、游戏计算……
Rust 是对 WASM 支持良好的语言之一,并且从 Rust 本身继承了不少的好处:
Predictable performance
Small code size
Modern amenities
具体可参见 https://www.rust-lang.org/what/wasm 。
在本文中我们将简单入门和使用 Rust 编写一个娱乐性的项目,并编译为 WASM 部署到网页上。前置知识将尽量限制于 Rust,不会过多涉及 Node.js。
配置环境
建议直接使用 rustup 完成这一步:
1 $ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
基本是自动的,不多做介绍。详细步骤和特殊需求可以参考官方文档。
而后我们需要安装 wasm 的 target 和 wasm-pack。
1 2 3 $ rustup target add wasm32-unknown-unknown $ cargo install wasm-pack $ cargo install cargo-generate
Rust 的环境配置到此就结束了。一般情况而言,你可能还想继续配置 Node.js 一侧的环境,但这已经超出了本文内容。下文中所有的操作都不需要 node 环境。
创建项目
我们准备写一个简单的娱乐项目,仅作为学习使用。这个小程序能够把输入的文本使用语气词编码和译码。大概效果就像:
1 2 3 4 5 已经没有什么可怕的了。 呼!啊啊~呜..啊啊!呼~呜呜..呼..呜呜!呜呜!呜呜!咿!唔..咿啊..啊..咿~啊嗯!唔..呜啊..嗯啊!咿呜..呼啊..咿呜~呼~咿..啊嗯~嗯啊!咿呜..咿呜..呜嗯..唔!咿!啊啊..啊嗯!咿呜..啊..啊~啊嗯!哈啊..呼啊..啊嗯~嗯!呼!咿啊~啊~哈啊!啊啊!呼~咿啊..呜..呜嗯!啊嗯~啊啊!啊啊~呜..嗯!呼啊!啊啊..咿呜!呜呜!呜!呼~呜啊..唔!呜嗯!呜~呜啊..
我们创建一个 lib 项目,并删除默认的 Cargo.toml
内容,写入给 core lib 和 wasm binding 预留的 workspace。
1 $ cargo generate --git https://github.com/rustwasm/wasm-pack-template
1 2 3 4 5 [workspace] members =[ "modal_particles" , "modal_particles_wasm" , ]
1 2 $ cargo new modal_particles --lib $ cargo new modal_particles_wasm --lib
我们将在 modal particles
里编写主要逻辑,然后在 modal particles wasm
里编写预留给 WASM 的 API。之后如果有兴趣,可以方便地在这基础上继续增加 CLI 等。
在 modal_particles_wasm
的 Cargo.toml
配置如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 [package] name = "modal_particles_wasm" version = "0.1.0" edition = "2018" [lib] crate-type = ["cdylib" , "rlib" ][features] default = ["console_error_panic_hook" ][dependencies] wasm-bindgen = "0.2.84" console_error_panic_hook = { version = "0.1.7" , optional = true }modal_particles = { path = "../modal_particles" }[dev-dependencies] wasm-bindgen-test = "0.3.34" [profile.release] opt-level = "s"
在正式开始之前,切换到 modal_particles_wasm
目录,编译测试一下环境是否配置正确:
1 2 $ wasm-pack build --target web
理想情况下将在同目录中生成 pkg
文件夹。
基本思路
我们的想法非常简单:把输入使用 UTF-8 编码,将 byte 按照一定规则对应到多个语气词上。译码时反向映射即可。
在思来想去之后我找到了 16 个还算合适的语气词,能够编码一个 4 位 bit。
在把输入编码为 UTF-8 后,我们把一个 byte b
拆分成 b&0xF
b>>4&0xF
2 部分,分别对应到 16 个语气词上。因为选词时并没有考虑前缀重复的问题,所以继续在每个映射结果间插入一些间隔符号,以方便在译码时作为划分依据。
译码时先对输入划分,然后查询相邻两个语气词对应的编号 a
b
,并按照 a|b<<4
拼起来就得到了原始的 UTF-8 编码。
编码的主要部分如下,没什么可说的,就是简单的查表输出……
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 fn write (&mut self , buf: &[u8 ]) -> io::Result <usize > { let mut idx =0usize ; for byte in buf.iter () { let (mut ai,mut bi) = (byte & 0xF , (byte >> 4 ) & 0xF ); let (a, b) = ( ID2WORD.get (&(ai as usize )).unwrap (), ID2WORD.get (&(bi as usize )).unwrap (), ); self .writer.write_all (a.as_bytes ())?; self .writer .write_all (WORD_DICT2.get (idx).unwrap ().as_bytes ())?; idx = (idx + ai as usize ) % WORD_DICT2.len (); self .writer.write_all (b.as_bytes ())?; self .writer .write_all (WORD_DICT2.get (idx).unwrap ().as_bytes ())?; idx = (idx + bi as usize ) % WORD_DICT2.len (); } Ok (buf.len ()) }
同样流式地实现译码过程,节省内存。因为有着 2 对 1 的关系,需要暂存没能解码完全的内容,所以解码器需要一个 buffer。其他也没什么可说的……
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 fn write (&mut self , buf: &[u8 ]) -> io::Result <usize > { let (s, len) = match std::str ::from_utf8 (buf) { Ok (s) => (s, buf.len ()), Err (err) => unsafe { let valid_len = err.valid_up_to (); (std::str ::from_utf8_unchecked (buf), valid_len) }, }; let mut s = s.to_string (); for d in WORD_DICT2 { s = s.replace (d, "/" ); } let u8_list : Vec <_> = s .split ("/" ) .filter (|x| !x.is_empty ()) .map (|x| WORD2ID.get (x).unwrap ()) .map (|x| *x as u8 ) .collect (); self .buffer.write_all (&u8_list)?; while self .buffer.len () >= 2 { let mut t : [u8 ; 2 ] = [0 , 0 ]; self .buffer.read (&mut t)?; let t = t[0 ] | (t[1 ] << 4 ); self .writer.write (&[t])?; } Ok (len) }
除此之外其他部分就都是比较普通的代码了。完成 modal_particles
的编写后,在 wasm 里封装为编码和译码两个 API encode
和 decode
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 use std::io::Write;use wasm_bindgen::prelude::*;use modal_particles::{Encoder, Decoder};#[wasm_bindgen] pub fn encode (str : &str ) -> String { let mut encoder = Encoder::new (Vec ::new ()); encoder.write_all (str .as_bytes ()).unwrap (); String ::from_utf8 (encoder.get_writer ()).unwrap () }#[wasm_bindgen] pub fn decode (str : &str ) -> String { let mut decoder = Decoder::new (Vec ::new ()); decoder.write_all (str .as_bytes ()).unwrap (); String ::from_utf8 (decoder.get_writer ()).unwrap () }
编译 & 使用
运行
1 $ wasm-pack build --target web
目录下将生成 pkg
文件夹,内部有如下文件:
1 2 3 4 5 modal_particles_wasm.js modal_particles_wasm.d.ts modal_particles_wasm_bg.wasm modal_particles_wasm_bg.wasm.d.ts package.json
关于这几个文件其实有一些可说道的。
package.json
是给 NPM 包使用的,因为我们指定编译为网页可使用的代码,所以不必管这个东西。
在理想情况下,应该可以直接 import * from <a wasm file>
。
可惜事实并没有这么美好,所以 wasm-pack 准备了简单的 js 封装以暴露我们在 Rust 中事先定义的接口。
在 HTML 里用以下代码就能加载这个 WASM 并使用了。
1 2 3 4 5 6 7 8 9 <script type ="module" > import init,{encode,decode} from "modal_particles_wasm.js" ; async function run ( ){ await init (); } run (); </script >
试一试:语"气" 。