30分钟零基础实战 WebAssembly 和 Rust

4,588 阅读3分钟

前言

  • 目标:通过实战,理解 WebAssembly 的开发流程,对性能有个简单对比
  • 示例:实现leetcode - 斐波那契数列
  • 学习准备:需要 0 基础,基于 Mac 环境, 因此 bash 命令行的部分 Windows 的同学可能需要替换下
  • 学习时长:约 30 分钟
  • 打包工具: webpack 5
  • 写作时间:2021-10-13
  • 代码@Github:dive-into-wasm

1. 环境安装

安装 Rust 和对应的包。

1.1 安装 Rust

curl -sSf https://static.rust-lang.org/rustup.sh | sh

1.2 更换为国内源

更换为国内源,否则安装太慢了。

新建文件:~/.cargo/config,内容替换为如下,replace-with 这行可自己 ping 文件中各个国内源头,看哪个源快用哪个:

[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"

# 替换成你偏好的镜像源
replace-with = 'sjtu'

# 清华大学
[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"

# 中国科学技术大学
[source.ustc]
registry = "git://mirrors.ustc.edu.cn/crates.io-index"

# 上海交通大学
[source.sjtu]
registry = "https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index"

# rustcc社区
[source.rustcc]
registry = "git://crates.rustcc.cn/crates.io-index"

1.3 安装 cargo-generate 脚手架

cargo install cargo-generate

备注:如果报错

error: could not find system library 'openssl' required by the 'openssl-sys' crate`

需要先安装 brew install openssl 即可。

1.4 安装 wasm-pack

wasm-packRust 编译为 WebAssembly

$ cargo install wasm-pack

2. 完成一个 Rust lib 项目

2.1 创建 Rust 项目

$ cargo new rust --lib

这里 rust 是项目的名称,你可以换成你想要的任意名称。

注意:会自动生成名为 rust 的文件夹,不要手动创建这个文件夹。

2.2 配置 Cargo.toml

新增 [lib] , 在 [dependencies] 下增加 wasm-bindgen 依赖,修改后的 完整内容类似如下:

[package]
name = "rust"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

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

[dependencies]
wasm-bindgen="0.2"

2.3 lib.rs 代码实现

代码细节此处不讲了,文件内容如下:

extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;

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

#[wasm_bindgen]
pub fn fib_tail_call_optimized(i: u32, prev: u32, next: u32) -> u32 {
    match i {
        0 => next,
        1 => next,
        _ => fib_tail_call_optimized(i - 1, next, prev + next)
    }
}

2.4 编译为 WebAssembly 二进制

在 rust 项目根目录,用 wasm-pack 工具编译:

$ wasm-pack build

成功的话会输出类似如下内容:

...
[INFO]: 📦   Your wasm pkg is ready to publish at .../dive-into-wasm/rust/pkg.

此时在 pkg 目录会生成如下5个文件:rust.jsrust.d.tsrust_bg.jsrust_bg.wasmrust_bg.wasm.d.tsrust_bg.wasm 就是二进制文件,我们待会要用到的就是这个了。

3. 完成一个前端项目

完成一个前端项目,并编译到浏览器执行生成的 WebAssembly 。

3.1 创建前端项目

rust 目录的父目录执行(即 web 目录和 rust 目录是平级的,否则本教程的示例代码中的路径要调整了):

$ mkdir web && cd web
$ npm init -y

3.2 配置 webpack 和开发服务器

安装依赖:

$ npm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin

Webpack 5 默认不开启 WebAssembly 支持,需要手动配置一下,同时用 html-webpack-plugin 自动生成一个入口 HTML ,webpack.config.js 内容如下:

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    mode: 'development',
    plugins: [
        new HtmlWebpackPlugin({title: '实战 WASM Rust'})
    ],
    // 实验特性
    // BREAKING CHANGE: Since webpack 5 WebAssembly is not enabled by default and flagged as experimental feature.
    // You need to enable one of the WebAssembly experiments via 'experiments.asyncWebAssembly: true' (based on async modules) or 'experiments.syncWebAssembly: true' (like webpack 4, deprecated).
    experiments: {
        asyncWebAssembly: true
    }
}

3.3 编写代用和测试代码

实现调用 WebAssembly 并增加 JS 实现 ,进行 benchmark 。

JS实现2个方法,一个是普通实现,一个是尾递归优化的实现,src/fib.js 代码:

function fib(i) {
    if (i <= 1) return i

    return fib(i - 1) + fib(i - 2)
}


function fibTailCallOptimized(i, prev = 0, next = 1) {
    if (i <= 1) return next

    return fibTailCallOptimized(i - 1, next, prev + next)
}

export {
    fib,
    fibTailCallOptimized
}

调用和执行 benchmark ,src/index.js 代码:

// 直接引用 wasm 文件
import {fib as wasm_fib, fib_tail_call_optimized as wasm_fib_tail_call_optimized } from '../../rust/pkg/rust_bg.wasm'
import { fib, fibTailCallOptimized } from './fib.js'

function time(timerName, func) {
    console.time(timerName)
    console.log(`${timerName}: `, func())
    console.timeEnd(timerName)
}

// js 实现数字不能太大,否则 CPU 占满,运行不出来结果
const num = 30
time('wasm_fib', () => wasm_fib(num))
time('wasm_fib_tail_call_optimized', () => wasm_fib_tail_call_optimized(num, 0, 1))
time('fib', () => fib(num))
time('fibTailCallOptimized', () => fibTailCallOptimized(num))

注意第一行引用了 rust_bg.wasm 二进制代码。

注意: 浏览器安全策略禁止通过 file:// 协议加载 wasm 文件,所以我们这里使用的 webpack-dev-server

3.4 浏览器运行及benchmark

webpack 打包并在浏览器运行测试效果。

$ npx webpack serve

在浏览器打开,通常是 http://localhost:8080/ 。控制台输入类似如下(已删除结果的展示,你可以自己检查下,首先保证运行结果的一致性):

wasm_fib: 0.193115234375 ms
wasm_fib_tail_call_optimized:   ms

fib: 0.93701171875 ms
fibTailCallOptimized: 0.16796875 ms

可以看到 wasm 执行效率比 JS 未做优化的高 5 倍左右;

尾调用优化后是 2 倍左右。

参考文档

  1. Cargo 中文文档
  2. 更换 cargo 源 —— 附目前可用国内 cargo 镜像源配置