Skip to content

Rust在WebAssembly中的简单使用

依赖

本项目使用 Rust 编写代码并编译成 wasm,并使用 webpack 管理项目。

因此,你得安装如下依赖:

  1. nodejs,不言而喻;
  2. Rust 及其构建工具,安装方式见官网
  3. wasm-pack,这是一个将 Rust 代码编译成 wasm,并生成 JS/TS 调用代码的工具。

创建 Rust 项目

使用 cargo 创建一个 lib 项目(cargo 是 Rust 官方项目管理工具,按照上一节步骤 2 安装之后就可以直接使用了)。

Bash
cargo new --lib wasm-demo

项目目录如下:

img

接着安装 Rust 的依赖 wasm-bindgen,这是一个实现 Rust 和 JS 互调用的 crate(Rust 术语,大致可以理解成依赖)。

C#
cargo add wasm-bindgen

同时稍微配置一下,最终 Cargo.toml (Cargo.toml 是 Rust 项目的配置文件,相当于 package.json)长这样:

TOML
[package]
name = "wasm-demo"
version = "0.1.0"
edition = "2021"

[dependencies]
wasm-bindgen = "0.2.87"

[lib]
crate-type = ["cdylib"]

写一段 demo 代码:

Rust
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn add(left: usize, right: usize) -> usize {
    left + right
}

使用如下命令行就可以将上面的 Rust 代码编译成 wasm 了。

Perl
wasm-pack build -t web

默认生成的代码在 pkg 文件夹下。

img

创建 webpack 项目

使用 npm 创建一个 webpack 项目(本例中放在 www 文件夹下)。

img

使用的依赖和配置如下:

JSON
{
  "name": "wasm-demo-web",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "webpack-dev-server --open",
    "build": "webpack --mode production"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "html-webpack-plugin": "^5.5.3",
    "webpack": "^5.88.2",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^4.15.1"
  }
}
var path = require("path");
const htmlPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./src/index.js",
  output: {
    path: path.join(__dirname, "/dist"),
    filename: "bundle.js",
  },
  plugins: [new htmlPlugin({ title: "wasm-demo" })],
  mode: "development"
};

引入 wasm

这时我们就可以将生成的代码引入我们的 web 项目了,即在 package.json 添加如下依赖。

JSON
"dependencies": {
    "wasm-demo": "file:../pkg"
},

wasm-pack 默认导出了一个 init 函数用于初始化,加载 wasm。只有在初始化之后才能运行其他代码,否则报错。如下,我们运行了之前编写的 add 函数,并打印其结果。

JavaScript
import init, { add } from "wasm-demo";

init().then(() => {
  console.log(add(1, 2));
});

控制台中也输出了结果。

img

在 Rust 调用 JS

在之前的例子中我们已经看到如何在 JS 中调用 Rust 编写的代码,只需要给需要导出的函数、结构体加上属性 #[wasm_bindgen](当然,不是所有函数和结构体都支持导出,它得实现 IntoWasmAbi trait。然而,不可以导出并不意味着代码不能运行,只是不能给 JS 调用而已。)。

Rust
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct Calculator { }

#[wasm_bindgen]
impl Calculator {
    pub fn new() -> Calculator {
        Calculator { }
    }

    pub fn add(&self, left: usize, right: usize) -> usize {
        left + right
    }

    pub fn sub(&self, left: usize, right: usize) -> usize {
        left - right
    }
}

在 JS 中

JavaScript
import init, { Calculator } from "wasm-demo";

init().then(() => {
  const calculator = Calculator.new();
  console.log(calculator.add(1, 2));
  console.log(calculator.sub(2, 1));
});

在 Rust 中也是可以调用 JS API 的,甚至是操作 DOM。我们只需要告诉 Rust 有这么一个函数,它会自己去寻找。

Rust
#[wasm_bindgen]
extern "C" {
    fn alert(s: &str);

    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

pub fn add(&self, left: usize, right: usize) -> usize {
    alert(&format!("messag from Rust: {} + {} = {}", left, right, left + right));
    left + right
}

如此就可以在 Rust 中直接调用 alter

img

Rust 中专门有 crate js-sys 暴露了 JS 相关 API,以及 crate web-sys 导出了浏览器相关 API。

比如下面的代码就是使用 Rust 编写的。它先创建了一个 p 标签,设置其内容为 "Hello from Rust!",最后将其压入 body。

Rust
use wasm_bindgen::prelude::*;

// Called by our JS entry point to run the example
#[wasm_bindgen(start)]
fn run() -> Result<(), JsValue> {
    // Use `web_sys`'s global `window` function to get a handle on the global
    let window: web_sys::Window = web_sys::window().expect("no global `window` exists");
    let document: web_sys::Document = window.document().expect("should have a document on window");

    let val = document.create_element("div")?;
    val.set_text_content(Some("Hello from Rust!"));
    document.body().unwrap().append_child(&val)?;
    print!("Hello from Rust! (printed to stdout)");
    Ok(())
}

toml配置如下:

toml
[package]
name = "wasm-demo"
version = "0.1.0"
edition = "2021"

[dependencies]
wasm-bindgen = "0.2.93"
web-sys = { version = "0.3", features = ['CanvasRenderingContext2d',
  'CssStyleDeclaration',
  'Document',
  'Element',
  'EventTarget',
  'HtmlCanvasElement',
  'HtmlElement',
  'MouseEvent',
  'Node',
  'Window'] 
}

[lib]
crate-type = ["cdylib"]

运行效果如下。

img

性能

只是简单比较,结果可能受到多种因素影响。

我们分别用 JS 和 Rust 写一个求 Fibonacci 数列第 n 项的函数,Rust 的版本如下(使用 release 编译):

Rust
#[wasm_bindgen]
pub fn fib(n: u32) -> u32 {
    match n {
        0 => 0,
        1 => 1,
        _ => fib(n - 1) + fib(n - 2),
    }
}

下面是 JS 版本。两版都是使用递归实现。

JavaScript
function fib(n) {
    if (n < 2) {
        return n;
    }
    return fib(n - 1) + fib(n - 2);
}

测试代码如下。使用 performance 测试,n 设置为 40,测试三轮,取均值作为结果。

JavaScript
import init, { fib as fibInRust } from "wasm-demo";

init().then(() => {
  const n = 30;
  const maxRound = 3;
  for (let round = 0; round < maxRound; round++) {
    performance.mark("start");
    fibInRust(n);
    performance.mark("end");
    performance.measure(`fibInRust-${n}-${round}`, "start", "end");

    performance.mark("start");
    fib(n);
    performance.mark("end");
    performance.measure(`fibInJS-${n}-${round}`, "start", "end");
  }
  
  const measures = performance.getEntriesByType("measure");
  const fibInRustMeasures = measures.filter((m) =>
    m.name.startsWith("fibInRust")
  );
  const fibInJSMesures = measures.filter((m) => m.name.startsWith("fibInJS"));
  const fibInRustAvg = fibInRustMeasures.reduce(
    (acc, m) => acc + m.duration,
    0
  );
  const fibInJSAvg = fibInJSMesures.reduce((acc, m) => acc + m.duration, 0);
  console.log(`fibInRustAvg: ${fibInRustAvg / fibInRustMeasures.length}ms`);
  console.log(`fibInJSAvg: ${fibInJSAvg / fibInJSMesures.length}ms`);
});

测试结果如下,显然 wasm 的优势是很明显,这种优势是随着计算量的增大越发明显的。而且直接调用 wasm 还需要通过一层 JS,也就是说 wasm 真正的运行时间比如下展示的还要少。

img

总结

个人认为 wasm 的优势不仅在于提高了性能,更是极大地拓展了前端生态。wasm 给前端除了 js (ts 本质还是 js)外的更多选择。使用 Rust、Cpp、Go……书写的代码都可以在浏览器中运行。甚至 Docker 都集成了 wasm,在可以预见的将来。wasm 生态将极其丰富,是比 Python 更“黏”的胶水语言。

但编写 wasm 是有门槛的,尤其是使用 Rust 编写。而且,也不是所有的地方都需要 wasm,经过 V8 的优化,js 的性能已经很不错了,对一般业务来说,使用 js 是更具性价比的选择。

前端知识体系 · wcrane