文章同步发布于本人博客,戳这儿
布景
WebAssembly(WASM)是一个简略的机器模型和可履行格局,具有广泛的标准。它被设计为便携、紧凑,代码履行可以达到挨近本机原生指令的履行速度。
作为一种编程言语,WebAssembly 由两种格局组成,它们以不同的办法表明相同的结构:
- 后缀为
.wat
的文本格局(称为“WebAssembly Text”),可以被人类理解,运用 S-表达式。 - 后缀为
.wasm
的二进制格局是较低等级的,人无法读懂,它旨在供 wasm 虚拟机直接运用。
作为参考,下面是一个在 JS 中调用两数求和 WASM 函数的例子:
const wasmInstance = new WebAssembly.Instance(wasmModule, {});
const { addTwo } = wasmInstance.exports;
for (let i = 0; i < 5; i++) {
console.log(addTwo(i, i));
}
/**
* output:
* 0
* 2
* 4
* 6
* 8
**/
addTwo
函数本身是由其他言语编写而成的,并且被编译成了 .wat
格局。以下是这个 addTwo
求和函数的 .wat
文件:
(module
(func (export "addTwo") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add))
可以经过这个网站将
.wat
生成对应的二进制.wasm
文件:wat2wasm demo
环境装备
- 装置 rust 东西链(
rustup
,rustc
,cargo
) - 装置 wasm-pack,一个构建、测验和发布 WASM 的 Rust CLI 东西,咱们将运用
wasm-pack
相关的指令来构建 WASM 二进制内容。 - npm,JS 包管理器
如果不装置 wasm-pack,运用打包东西 webpack
加上 @wasm-tool/wasm-pack-plugin
插件也能构建 WASM,后文会详细介绍。
Tips:装置 cargo-generate,可以运用现有的 git 库房生成一个新的 Rust 项目:
cargo install cargo-generate
快速入门
项目初始化
首先咱们履行 cargo new wasm-demo
初始化 Rust 项目,新建一个名为 wasm-demo
的文件夹(也可以选一个你喜爱的文件夹名),主动生成装备文件 Cargo.toml
,结构如下。
装备包文件
咱们可以在 Cargo.toml
文件中加上下列代码并保存,保存之后 Cargo 会主动下载依靠。
-
cdylib
用来指明库的类型。 -
wasm-bindgen
是一个简化 Rust WASM 与 JS 之间交互的库。- 它可以将如 DOM 操作、console.log 和 performance 等 JS 相关 API 露出给 Rust 运用
- 它可以将 Rust 功用导出到 JS 中,如类、函数等
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.83"
编写代码
接着开始编写一些简略的 Rust 代码。将模板文件中的 src/main.rs
改成 src/lib.rs
,里面写上一个求斐波那契数列的 Rust 函数。需求加上#[wasm_bindgen]
标示告诉 wasm-pack 需求将这个函数编译成 wasm 可履行文件。
use wasm_bindgen::prelude::*; // 用于加载 Prelude(预导入)模块
#[wasm_bindgen]
pub fn fib(n: u32) -> u32 {
if n == 0 || n == 1 {
return 1;
}
fib(n - 1) + fib(n - 2)
}
当时目录应该长这样:
Rust 中包管理体系将 crate 包分为二进制包(Binary)和库包(Library)两种,二者可以在同一个项目中同时存在。
二进制包:
-
main.rs
是二进制项目的进口 - 二进制项目可直接履行
- 一个项目中二进制包可以有多个,所以在 Cargo.toml 中经过双方括号标识
[[bin]]
库包:
-
lib.rs
是库包的进口。 - 库项目不行直接履行,通常用来作为一个模块被其他项目引用。
- 一个项目中库包仅有1个,在 Cargo.toml 中经过单独括号标识
[lib]
由于咱们这儿期望将 WASM 转为一个可以在 JS 项目中运用的模块,所以需求运用库包 lib.rs
的命名。
履行编译
只需求履行咱们之前装置的 wasm-pack 即可将刚刚的 Rust 代码转换成可以被 JS 导入的模块。
wasm-pack build
编译完成后,咱们会发现根目录下多了一个 pkg/
文件夹,里面便是咱们的 WASM 产品地点的 npm 包了。
包的进口文件是不带 _bg
的 .js
文件,即 wasm_demo2.js
。
wasm_demo2.js
的内容如下:
import * as wasm from "./wasm_demo2_bg.wasm";
export * from "./wasm_demo2_bg.js";
wasm_demo2_bg.js
的内容如下:
import * as wasm from './wasm_demo2_bg.wasm';
/**
* @param {number} n
* @returns {number}
*/
export function fib(n) {
const ret = wasm.fib(n);
return ret >>> 0;
}
wasm_demo2.d.ts
的内容如下:
/* tslint:disable */
/* eslint-disable */
/**
* @param {number} n
* @returns {number}
*/
export function fib(n: number): number;
可以看到,wasm-pack 打包不只输出一个 ESM 标准的模块,并且还支持主动生成 d.ts 文件,对模块的运用者十分友爱。
运用 WASM 包
下面咱们就新建一个 html 页面去调用刚刚生成的模块。在根目录下生成 index.html
,并输入以下内容。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rust WASM demo</title>
<script>
/**
* 1. 经过运用 instantiateStreaming 调用流式实例化
**/
WebAssembly.instantiateStreaming(fetch("./pkg/wasm_demo2.wasm")).then((obj) => {
const fib = obj.instance.exports.fib;
const out = fib(20);
console.log("rust output: ", out);
})
/**
* 2. 不经过流式调用,直接读取二进制文件并对字节进行实例化
**/
fetch("./pkg/wasm_demo2.wasm")
.then(res => res.arrayBuffer())
.then(bytes => WebAssembly.instantiate(bytes))
.then(results => {
const fib = results.instance.exports.fib;
const out = fib(20);
console.log("rust output: ", out);
})
</script>
</head>
<body>
</body>
</html>
如上所示,可以经过流式与非流式两种原生 JS API 办法进行 .wasm
二进制文件的模块实例化。
接下来编写一个简略的服务 server.js
:
const http = require('http');
const fs = require('fs');
const reqListener = function(req, res) {
f = req.url === '/' ? 'index.html' : './pkg/wasm_demo2_bg.wasm';
if (f === './pkg/wasm_demo2_bg.wasm') {
res.setHeader('Content-type', 'application/wasm')
}
res.writeHead(200)
return fs.createReadStream(f).pipe(res)
}
const server = http.createServer(reqListener);
server.listen(8081);
console.log('listening: http://localhost:8081')
开启服务:
node server.js
翻开 html 页面 http://localhost:8081/ ,在控制台可看到两份 fib(20)
的成果被打印出来了。
进阶用法
配合 Webpack 运用
运用 Webpack + wasm-pack 插件的办法构建和测验,可以直接经过 npm scripts 运转代码,像前端开发相同调试。
用 npm init -y
新建一个项目,在 package.json
中新增如下代码:
...
"scripts": {
"build": "webpack",
"serve": "webpack serve"
},
"devDependencies": {
"@wasm-tool/wasm-pack-plugin": "1.5.0",
"html-webpack-plugin": "^5.3.2",
"text-encoding": "^0.7.0",
"webpack": "^5.49.0",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "^3.11.2"
},
...
履行 npm i
装置依靠。
新建 index.js
文件,作为 WASM 模块的履行文件。在里面写入如下内容:
// 由于 webpack 的 bug(webpack/webpack#6615),这儿暂时只能运用动态导入 import
const rust = import('./pkg');
rust.then(m => {
const out = m.fib(20);
console.log("rust output: ", out);
}).catch(console.error)
新建 webpack.config.js
文件,并进行如下装备:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); // 赋予 webpack 处理 wasm 才能的插件
/**
* @type import('webpack').Configuration
*/
module.exports = {
entry: './index.js',
devServer: {
port: '8082'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
},
plugins: [
new HtmlWebpackPlugin(),
new WasmPackPlugin({
crateDirectory: path.resolve(__dirname, ".")
}),
// Have this example work in Edge which doesn't ship `TextEncoder` or
// `TextDecoder` at this time. 处理浏览器兼容问题
new webpack.ProvidePlugin({
TextDecoder: ['text-encoding', 'TextDecoder'],
TextEncoder: ['text-encoding', 'TextEncoder']
})
],
mode: 'development',
experiments: {
asyncWebAssembly: true // 翻开异步 WASM 功用
}
};
履行 npm run build
构建出 WASM 二进制产品。
履行 npm run serve
即可进行开发,在浏览器的控制台中实时看到对应 fib(20)
的成果。咱们可以在 index.js
中更改传入的参数,并查看控制台的新输出成果。
总结便是,运用 webpack 可以协助可以更加高效地进行 Rust WASM 运用的开发和调试。
这一块凭借了 webpack-dev-server 的 HMR 模块实现热更新:
- 打包时将一块 webpack 脚本代码(JSONP 脚本)打包到客户端运用中。
- 当本地
lib.rs
文件发生变化时,服务端 webpack-dev-server 经过 websocket 告诉客户端运用代码中的 webpack 脚本代码,客户端向服务端请求最新编译好的 wasm 模块 - 新的 WASM 模块以 JSONP 的办法从服务端传输到客户端
- 经过 webpack 重写的
__webpack_require__
办法获取到新模块并加载、包裹、运转和缓存,实现模块的热替换。
Rust 操纵 DOM
实现一个求斐波那契数的运用,如下所示:
需求先装置一个叫 web-sys
的 Crate,它为 Rust 提供了控制 DOM 的才能,在 Cargo.toml
中新增依靠:
[dependencies.web-sys]
version = "0.3.4"
features = [ 'Document', 'Element', 'HtmlElement', 'Node', 'Window', 'HtmlInputElement']
features
需求开发者手动地声明需求运用到的模块,这样做的好处有二:一是防止不同模块下的features
名字发生冲突;二是条件编译各个依靠中的特性,对不运用的features
不编译。
在 lib.rs
函数中新增 init()
函数,用于生成 DOM 节点、挂载监听器并挂载 DOM 节点。
// start 标识 init() 在 WASM 加载时主动履行
#[wasm_bindgen(start)]
pub fn init() -> Result<(), JsValue> {
// 运用 web_sys 的 window 大局目标
let window = web_sys::window().expect("不存在大局 window 目标");
let document = window.document().expect("需求在 window 上存在 document");
let body = document.body().expect("document 中需求存在一个 body");
// 生成 dom 元素
let input = document
.create_element("input")?
.dyn_into::<web_sys::HtmlInputElement>()?;
let btn = document.create_element("button")?;
btn.set_text_content(Some(&"点击核算斐波那契数"));
let out = document.create_element("h3")?;
let input = Rc::new(input); // 为了不违背“一个变量只能有一个所有者”的规矩,需求运用 Rc 包裹 input 元素,方便在闭包中拿到并运用它的值
let out = Rc::new(RefCell::new(out)); // 由于需求改动 out 元素的 textContent,需求运用 RefCell 包裹方便去在闭包中把它作为可变变量来改动它
{
let out = out.clone(); // 仿制一份智能指针
let input = input.clone();
// 运用到 wasm_bindgen::closure::Closure,它的作用是打通 Rust 中的闭包和 JS 中的闭包
let closure = Closure::<dyn Fn()>::new(move || {
let val = input.value();
let num = val.parse::<u32>().unwrap();
let res = fib(num);
out.borrow_mut()
.set_text_content(Some(res.to_string().as_str())); // 在这儿运用 borrow_mut 把 out 作为可变变量获取出来,并设置 textContent
});
btn.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref())?; // 挂载事件监听器,将闭包函数先转换为 JS 值,再越过类型判别,再作为回调函数传给 btn
closure.forget(); // 开释 Rust 对这片堆内存的管理,交给 JS 的 GC 去收回
}
body.append_child(&input)?;
body.append_child(&btn)?;
body.append_child(&out.borrow())?; // 挂载 DOM 元素节点
Ok(())
}
上述 init()
添加了 #[wasm_bindgen(start)]
宏标示,init()
函数会在 WASM 模块初始化时主动履行。因此不再需求更改 index.js
文件。
直接运转服务:
npm run serve
翻开 http://localhost:8082/ ,成功!
翻开查看,咱们可以发现 body
上正确地挂载了 DOM 元素。
功能指标
咱们先在 lib.rs
中加上以下代码,答应 Rust 代码中调用 console.log
。
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str); // 将 js 命名空间中的 console.log 办法界说在 Rust 中
}
// 界说 console.log! 宏
macro_rules! console_log {
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}
接着,在刚刚的求斐波那契数的运用中加上 performance
API 相关的代码。
在 Cargo.toml
中加上 Performance 的 features:
[dependencies.web-sys]
version = "0.3.4"
features = [
'Document',
'Element',
'HtmlElement',
'Node',
'Window',
'Performance', // 加上这一行
'HtmlInputElement'
]
将求斐波那契数运用中的 closure
办法进行如下重写:
let closure = Closure::<dyn Fn()>::new(move || {
let performance = window
.performance()
.expect("performance should be available"); // 获取 window.performance
let val = input.value();
let num = val.parse::<u32>().unwrap();
let start = performance.now(); // 调用 performance.now() 获取当时时刻
console_log!("the start time (in ms) is {}", start);
let res = fib(num);
let end = performance.now();
console_log!("the end time (in ms) is {}", end);
console_log!("delta (in ms) is {}", end-start);
out.borrow_mut()
.set_text_content(Some(res.to_string().as_str()));
});
运转服务器:
npm run serve
现在在控制台就可以看到履行运算的耗时了。
履行运算的录屏
功能比较:
fib(n) | 10 | 20 | 30 | 35 | 40 | 45 |
---|---|---|---|---|---|---|
wasm | 0.30 | 0.90 | 69.90 | 726.59 | 8018 | 90918.79 |
js | 0.19 | 0.80 | 67.70 | 753.20 | 8061 | 91794.70 |
可以看到,在 n 不大的场景下,WASM 的耗时比纯 JS 还要长,这是由于浏览器需求在 VM 容器中对 WASM 模块进行实例化,可能这一部分会耗费适当的时刻,导致功能不如纯 JS 的履行。随着运算规划变大,WASM 的优化越来越显着。
总结
WASM 从 2017 年 3 月推出以来,已然成了 Web 开发的未来发展趋势之一。
本文不只介绍了 WASM 的布景、环境装备、Rust 项目初始化、编译和运用 WASM 等基本用法,还经过一个简略的运用介绍了 WASM 与 webpack 配合开发、与 DOM 之间交互以及功能指标剖析等进阶用法。
参考资料
- Rust and WebAssembly
- hello-world
- Using the WebAssembly JavaScript API
- Closure in wasm_bindgen::closure – Rust (rustwasm.github.io)
- start – The
wasm-bindgen
Guide (rustwasm.github.io) - Make a Combined Library and Binary Project in Rust – DEV Community