WebAssembly With Rust

4 大主流浏览器已经都支持了 WebAssembly,同时也很多项目中得到了实际应用,本文将分享如何构建一个 WebAssembly (简称 WASM) 以及在浏览器中的使用。

使用 Rust 构建一个 WASM

try-rust_ScreenShot2021-01-10at12.54.29PM

能够构建 WASM 的语言有很多,本文将使用 Rust 来构建,分享如何从 0 构建一个 WASM。

基础环境初始化

  1. 安装 Rust 环境
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  1. 初始化一个 WASM 项目
cargo new --lib hello-wasm

更多详细的步骤可以看:Rust 和 WebAssembly 用例

如何将 Rust 构建为 WASM

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>

WASM 是如何和 Web 进行交互的

经过上面的步骤,整体跑起来了 Hello World,上面相应的代码在这个仓库中 luxp/code-parctice/hello-wasm

下面我们从源码的角度,看看 WASM 是如何工作的

加载 WASM

wasm-pack 已经直接帮我们打包好了对应的模块,可以直接在浏览器中是用原生 import

<script type="module">
  import init, { greet } from './pkg/hello_wasm.js'
  init().then(() => {
    greet('WASM')
  })
</script>

pkg/hello_wasm.js

实际加载 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 中的方法

我们并不能直接调用 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 的是内存的地址,而不是字符串

WASM 中如何调用 Web 中的方法

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 的双向通信

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 版本下,运行的结果如下图

try-rust_ScreenShot2021-01-10at3.05.30PM

整体上来看相比于纯 JS 的执行,WASM 在数据传输上还是会消耗一定的时间,但整体的耗时相比于 Rust 在高计算复杂度场景下带来的提升影响不大,2 万次的调用耗时在 40ms 内

#tech

杭州