4 大主流浏览器已经都支持了 WebAssembly,同时也很多项目中得到了实际应用,本文将分享如何构建一个 WebAssembly (简称 WASM) 以及在浏览器中的使用。
能够构建 WASM 的语言有很多,本文将使用 Rust 来构建,分享如何从 0 构建一个 WASM。
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo new --lib hello-wasm
更多详细的步骤可以看:Rust 和 WebAssembly 用例
Rust 社区中提供了封装好的工具wasm-pack,可以直接将 rust 的代码打包为一个 npm 包
在 Rust 中调用 JS 中的方法可以通过 wasm-bindgen
在做完了以上的一些方法之后,我们可以运行下面的命令,借助 wasm-pack 将 Rust 打包为一个 WebAssembly
wasm-pack build -t web
打包之后我们会得到得到一个新的 pkg 目录,里面有对应的构建产物,我们可以直接在 Web 中使用,下面是一段代码示例
<script type="module">
import init, { greet } from './pkg/hello_wasm.js'
init().then(() => {
greet('WASM')
})
</script>
经过上面的步骤,整体跑起来了 Hello World,上面相应的代码在这个仓库中 luxp/code-parctice/hello-wasm
下面我们从源码的角度,看看 WASM 是如何工作的
wasm-pack
已经直接帮我们打包好了对应的模块,可以直接在浏览器中是用原生 import
<script type="module">
import init, { greet } from './pkg/hello_wasm.js'
init().then(() => {
greet('WASM')
})
</script>
实际加载 WASM 的是在 pkg/hello_wasm.js
这个文件中,整体的流程可以简化以下的过程。 WebAssembly.instantiate 文档
fetch('hello_wasm_bg.wasm').then(async (module) => {
let imports = {}
let wasm = await WebAssembly.instantiate(module, imports)
// wasm 这个就是 WebAssembly 的实例了
return wasm
})
我们并不能直接调用 wasm 中的方法,而是要经过一层中转,可以看到在下面的代码中,我们调用的 greet 方法实际是从 hello_wasm.js
这个文件中导出的
<script type="module">
import init, { greet } from './pkg/hello_wasm.js'
init().then(() => {
greet('WASM')
})
</script>
实际的 greet
的代码
export function greet(name) {
var ptr0 = passStringToWasm0(
name,
wasm.__wbindgen_malloc,
wasm.__wbindgen_realloc
)
var len0 = WASM_VECTOR_LEN
wasm.greet(ptr0, len0)
}
在调用 greet 时,会先将传入的 name 值写入 WASM 的内存中,最终传入给 WASM 的是内存的地址,而不是字符串
Rust 中的代码,借助wasm_bindgen
通过 extern "C"
定义了要从外部引入的函数
#[wasm_bindgen]
extern "C" {
// 在最终编译的结果中,alert 这一块会被替换
pub fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}
在 WASM 初始化注入,流程可简化为下面
fetch('hello_wasm_bg.wasm').then(async (module) => {
let imports = {}
imports.alert = function (arg0, arg1) {
// 获取到内存区域
alert(getStringFromWasm0(arg0, arg1))
}
// 通过 imports 注入
let wasm = await WebAssembly.instantiate(module, imports)
// wasm 这个就是 WebAssembly 的实例了
return wasm
})
以上通过共享同一块内存和注入函数的方式,实现了 JS 和 WASM 的双向通信
修改了以下 Rust 中的代码,让其简单做一个字符串的拼接
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
let mut result: String = "prefix_".to_owned();
result.push_str(name);
return result;
}
同时在 HTML 中使用 JS 也实现一个版本
<script type="module">
import init, { greet } from './pkg/hello_wasm.js'
init().then((wasm) => {
console.time('greetWASM')
for (let i = 0; i < 20000; ++i) {
greet('Test')
}
console.timeEnd('greetWASM')
function greetJs(str) {
return 'prefix_' + str
}
console.time('greetJS')
for (let i = 0; i < 20000; ++i) {
greetJs('Test')
}
console.timeEnd('greetJS')
})
</script>
在 Chrome 87 版本下,运行的结果如下图
整体上来看相比于纯 JS 的执行,WASM 在数据传输上还是会消耗一定的时间,但整体的耗时相比于 Rust 在高计算复杂度场景下带来的提升影响不大,2 万次的调用耗时在 40ms 内