最近,我们见证了 WebAssembly 的 惊人的编译速度、加速 JS 库 以及 生成更小的二进制文件。我们甚至 制定了高级计划,旨在增强 Rust 和 JavaScript 社区以及其他 Web 编程语言之间的互操作性。正如那篇 之前文章 所述,我想深入探讨一个具体组件的细节,即 wasm-bindgen
。
如今,WebAssembly 规范 仅定义了四种类型:两种整数类型和两种浮点数类型。然而,大多数情况下,JS 和 Rust 开发人员都在使用更丰富的类型!例如,JS 开发人员经常与 document
进行交互以添加或修改 HTML 节点,而 Rust 开发人员则使用诸如 Result
之类的类型来处理错误,几乎所有程序员都使用字符串。
仅仅局限于 WebAssembly 当前提供的类型将过于限制,而这正是 wasm-bindgen
存在的意义。wasm-bindgen
的目标是为 JS 和 Rust 类型提供桥梁。它允许 JS 使用字符串调用 Rust API,或者 Rust 函数捕获 JS 异常。wasm-bindgen
消除了 WebAssembly 和 JavaScript 之间的阻抗不匹配,确保 JavaScript 可以高效且无样板地调用 WebAssembly 函数,而 WebAssembly 也可以对 JavaScript 函数执行同样的操作。
wasm-bindgen
项目在其 README 中有更多描述。为了开始学习,让我们深入研究一个使用 wasm-bindgen
的示例,然后探索它还能提供什么。
你好,世界!
这是一个经典的例子,学习新工具的最佳方法之一是探索其等效于打印“Hello, World!”的功能。在本例中,我们将探索一个 示例,它正是这样做的——在页面上显示“Hello, World!”的警报框。
这里目标很简单,我们想定义一个 Rust 函数,该函数在给定名称的情况下,将在页面上创建一个对话框,显示 Hello, ${name}!
。在 JavaScript 中,我们可能将此函数定义为
export function greet(name) {
alert(`Hello, ${name}!`);
}
不过,本示例的注意事项是,我们希望用 Rust 来编写它。这里已经有很多我们必须处理的步骤
- JavaScript 将调用 WebAssembly 模块,即
greet
导出。 - Rust 函数将接受一个字符串作为输入,即我们要问候的
name
。 - Rust 内部将使用插值创建一个包含该名称的新字符串。
- 最后,Rust 将使用它创建的字符串调用 JavaScript 的
alert
函数。
首先,我们创建一个全新的 Rust 项目
$ cargo new wasm-greet --lib
这将在一个新的 wasm-greet
文件夹中进行初始化,我们将在该文件夹中工作。接下来,我们将使用以下信息修改我们的 Cargo.toml
(Rust 中相当于 package.json
的文件)
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
我们暂时忽略 [lib]
部分,但下一部分声明了对 wasm-bindgen
crate 的依赖关系。这里包含了我们在 Rust 中使用 wasm-bindgen
所需的所有支持。
接下来,该编写一些代码了!我们将用以下内容替换自动生成的 src/lib.rs
#![feature(proc_macro, wasm_custom_section, wasm_import_module)]
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}
如果你不熟悉 Rust,这可能看起来有点啰嗦,但别担心!wasm-bindgen
项目一直在不断改进,将来肯定不会总是需要这些。最需要注意的部分是 #[wasm_bindgen]
属性,这是一个 Rust 代码中的注释,这里的意思是“请根据需要用包装器来处理”。我们的 alert
函数导入和 greet
函数导出都使用此属性进行了注释。过一会儿,我们将看到幕后发生了什么。
但首先,让我们在浏览器中打开它!让我们编译我们的 wasm 代码
$ rustup target add wasm32-unknown-unknown --toolchain nightly # only needed once
$ cargo +nightly build --target wasm32-unknown-unknown
这将在 target/wasm32-unknown-unknown/debug/wasm_greet.wasm
中生成一个 wasm 文件。如果我们使用诸如 wasm2wat
之类的工具查看该 wasm 文件的内容,可能会有点吓人。事实证明,这个 wasm 文件实际上还没有准备好被 JS 使用!相反,我们需要再执行一个步骤才能使其可用
$ cargo install wasm-bindgen-cli # only needed once
$ wasm-bindgen target/wasm32-unknown-unknown/debug/wasm_greet.wasm --out-dir .
这一步是很多魔法发生的地方:wasm-bindgen
CLI 工具会后处理输入的 wasm
文件,使其适合使用。稍后我们将看到“适合”的含义,现在只需知道,如果我们导入新创建的 wasm_greet.js
文件(由 wasm-bindgen
工具创建),我们就可以获得在 Rust 中定义的 greet
函数。
最后,我们需要做的就是用一个打包器将其打包,并创建一个 HTML 页面来运行我们的代码。在撰写本文时,只有 Webpack 4.0 版本 具有足够的 WebAssembly 支持才能开箱即用(尽管它也有一个 Chrome 注意事项)。随着时间的推移,其他打包器也会纷纷效仿。这里我将跳过细节,但你可以按照 Github 仓库中的 示例 配置来操作。如果我们查看内部内容,我们的页面 JS 代码如下
const rust = import("./wasm_greet");
rust.then(m => m.greet("World!"));
…就这样!打开我们的网页,现在应该会显示一个由 Rust 驱动的漂亮的“Hello, World!”对话框。
wasm-bindgen
的工作原理
哇,这可真是一段复杂的“Hello, World!”。让我们深入了解一下幕后发生了什么以及工具是如何工作的。
wasm-bindgen
最重要的方面之一是,它的集成本质上是建立在 wasm 模块只是另一种 ES 模块的概念之上的。例如,在上面的代码中,我们想要一个带有以下签名(在 Typescript 中)的 ES 模块
export function greet(s: string);
WebAssembly 无法原生实现这一点(请记住它目前只支持数字),因此我们依赖 wasm-bindgen
来填补空白。在上面的最后一步中,当我们运行 wasm-bindgen
工具时,你会注意到除了 wasm_greet_bg.wasm
文件之外,还生成了一个 wasm_greet.js
文件。前者是实际的 JS 接口,它执行必要的粘合代码来调用 Rust。*_bg.wasm
文件包含实际的实现和所有编译的代码。
当我们导入 ./wasm_greet
模块时,我们得到了 Rust 代码想要暴露的内容,但目前无法原生实现。现在我们已经了解了集成的运作方式,让我们跟踪脚本的执行过程,看看发生了什么。首先,我们的示例运行了
const rust = import("./wasm_greet");
rust.then(m => m.greet("World!"));
这里我们异步导入我们想要的接口,等待它被解析(通过下载和编译 wasm)。然后,我们调用模块上的 greet
函数。
注意:这里需要异步加载 Webpack 当前的要求,但这种情况可能不会永远存在,而且对于其他打包器来说可能不是必需的。
如果我们看一下 wasm-bindgen
工具生成的 wasm_greet.js
文件,我们会看到类似以下的内容
import * as wasm from './wasm_greet_bg';
// ...
export function greet(arg0) {
const [ptr0, len0] = passStringToWasm(arg0);
try {
const ret = wasm.greet(ptr0, len0);
return ret;
} finally {
wasm.__wbindgen_free(ptr0, len0);
}
}
export function __wbg_f_alert_alert_n(ptr0, len0) {
// ...
}
注意:请记住,这是未经优化的生成代码,可能并不美观或小巧!在用 LTO(链接时优化)和 Rust 中的发布版本进行编译,并在经过 JS 打包器管道(压缩)后,它应该会小得多。
这里我们可以看到 wasm-bindgen
为我们生成了 greet
函数。幕后,它仍然在调用 wasm 的 greet
函数,但它使用指针和长度而不是字符串来调用。有关 passStringToWasm
的更多细节,请查看 Lin Clark 的之前文章。除了 wasm-bindgen
工具为我们处理了这些内容之外,所有这些都是你必须自己编写的样板代码!稍后我们会谈到 __wbg_f_alert_alert_n
函数。
深入一层,下一个感兴趣的内容是 WebAssembly 中的 greet
函数。要查看它,让我们看看 Rust 编译器看到了什么代码。请注意,与上面生成的 JS 包装器类似,你在这里没有编写 greet
导出符号,而是 #[wasm_bindgen]
属性会生成一个垫片,它为你进行转换,即
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}
#[export_name = "greet"]
pub extern fn __wasm_bindgen_generated_greet(arg0_ptr: *mut u8, arg0_len: usize) {
let arg0 = unsafe { ::std::slice::from_raw_parts(arg0_ptr as *const u8, arg0_len) }
let arg0 = unsafe { ::std::str::from_utf8_unchecked(arg0) };
greet(arg0);
}
这里我们可以看到我们的原始代码 greet
,但 #[wasm_bingen]
属性插入了这个奇怪的函数 __wasm_bindgen_generated_greet
。这是一个导出的函数(用 #[export_name]
和 extern
关键字指定),它接受 JS 传递的指针/长度对。它在内部将指针/长度转换为 &str
(Rust 中的字符串),并转发给我们定义的 greet
函数。
换句话说,#[wasm_bindgen]
属性会生成两个包装器:一个在 JavaScript 中,用于将 JS 类型转换为 wasm 类型,另一个在 Rust 中,用于接收 wasm 类型并将其转换为 Rust 类型。
好的,让我们看一下最后一组包装器,即 alert
函数。Rust 中的 greet
函数使用标准的 format!
宏创建一个新字符串,然后将其传递给 alert
。请记住,当我们声明 alert
函数时,我们使用 #[wasm_bindgen]
声明了它,所以让我们看看 rustc 为此函数看到了什么
fn alert(s: &str) {
#[wasm_import_module = "__wbindgen_placeholder__"]
extern {
fn __wbg_f_alert_alert_n(s_ptr: *const u8, s_len: usize);
}
unsafe {
let s_ptr = s.as_ptr();
let s_len = s.len();
__wbg_f_alert_alert_n(s_ptr, s_len);
}
}
现在,这不太是我们写的,但我们多少可以看出它是如何组合在一起的。alert
函数实际上是一个薄包装器,它接受 Rust 的 &str
,然后将其转换为 wasm 类型(数字)。这会调用我们上面看到的 __wbg_f_alert_alert_n
函数,但有一个奇怪的地方是 #[wasm_import_module]
属性。
在 WebAssembly 中,所有函数的导入都来自特定的模块,由于 `wasm-bindgen` 基于 ES 模块构建,所以这也会被解释为 ES 模块的导入!虽然 `__wbindgen_placeholder__` 模块并不实际存在,但它表明该导入将由 `wasm-bindgen` 工具重写,以便从我们生成的 JS 文件中导入。
最后,我们来看一下生成的 JS 文件,它包含:
export function __wbg_f_alert_alert_n(ptr0, len0) {
let arg0 = getStringFromWasm(ptr0, len0);
alert(arg0)
}
哇!原来幕后发生了很多事情,从 JS 中的 `greet` 到浏览器中的 `alert`,经历了一条相对较长的链路。不过不用担心,`wasm-bindgen` 的关键之处在于它隐藏了所有这些基础设施!你只需要编写带有少量 `#[wasm_bindgen]` 注解的 Rust 代码。然后你的 JS 可以像使用另一个 JS 包或模块一样使用 Rust。
`wasm-bindgen` 还能做什么呢?
`wasm-bindgen` 项目的范围很广,我们在这里没有足够的时间详细介绍所有内容。探索 `wasm-bindgen` 功能的最佳方法是查看 [示例目录](https://github.com/alexcrichton/wasm-bindgen/tree/master/examples),其中包含从像上面我们看到的 [Hello, World!](https://github.com/alexcrichton/wasm-bindgen/tree/master/examples/hello_world) 到 [完全用 Rust 操作 DOM 节点](https://github.com/alexcrichton/wasm-bindgen/tree/master/examples/dom) 的各种示例。
从高层次来看,`wasm-bindgen` 的功能包括:
- 导入 JS 结构、函数、对象等,并在 wasm 中调用它们。你可以在结构体上调用 JS 方法并访问属性,这使得你编写的 Rust 代码在所有 `#[wasm_bindgen]` 注解都关联起来后,会有一种“原生”的感觉。
-
将 Rust 结构体和函数导出到 JS。这样,JS 不再局限于只使用数字,而是可以导出一个 Rust `struct`,它会在 JS 中变成一个 `class`。然后,你就可以传递结构体,而不必仅仅传递整数。[smorgasboard](https://github.com/alexcrichton/wasm-bindgen/tree/master/examples/smorgasboard) 示例很好地展示了支持的互操作性。
-
允许其他杂项功能,例如从全局范围(即 `alert` 函数)导入,使用 Rust 中的 `Result` 捕获 JS 异常,以及一种模拟在 Rust 程序中存储 JS 值的通用方法。
如果你想了解更多功能,请关注 [问题追踪器](https://github.com/alexcrichton/wasm-bindgen/issues)!
`wasm-bindgen` 的下一步计划是什么?
在我们结束之前,我想花一点时间描述一下 `wasm-bindgen` 的未来愿景,因为我认为这是该项目当前最激动人心的方面之一。
支持不仅仅是 Rust
从第一天起,`wasm-bindgen` CLI 工具就被设计为支持多种语言。虽然 Rust 是目前唯一支持的语言,但该工具被设计为也可以接入 C 或 C++。`#[wasm_bindgen]` 属性会在输出的 `*.wasm` 文件中创建一个自定义部分,`wasm-bindgen` 工具会解析并随后移除这个部分。这个部分描述了要生成哪些 JS 绑定以及它们的接口。这个描述与 Rust 无关,所以 C++ 编译器插件也可以轻松创建这个部分并让 `wasm-bindgen` 工具进行处理。
我发现这个方面尤其令人兴奋,因为我认为它可以让像 `wasm-bindgen` 这样的工具成为 WebAssembly 和 JS 集成的标准实践。希望它能对所有编译到 WebAssembly 的语言都有益,并能被捆绑器自动识别,从而避免上面几乎所有配置和构建工具的需求。
自动绑定 JS 生态系统
如今,使用 `#[wasm_bindgen]` 宏导入功能时,其中一个缺点是你必须手动编写所有内容,并确保没有错误。这有时会是一个单调乏味(且容易出错)的过程,非常适合自动化。
所有 Web API 都是用 [WebIDL](https://heycam.github.io/webidl/) 指定的,从 [WebIDL 生成 `#[wasm_bindgen]` 注解](https://github.com/alexcrichton/wasm-bindgen/issues/42) 应该相当可行。这意味着你将不再需要像上面那样定义 `alert` 函数,而只需要编写类似以下内容:
#[wasm_bindgen]
pub fn greet(s: &str) {
webapi::alert(&format!("Hello, {}!", s));
}
在这种情况下,`webapi` crate 可以完全从 Web API 的 WebIDL 描述中自动生成,从而保证没有错误。
我们甚至可以更进一步,利用 TypeScript 社区的出色工作,[从 TypeScript 生成 `#[wasm_bindgen]` 注解](https://github.com/alexcrichton/wasm-bindgen/issues/18)。这将允许自动免费绑定 npm 上所有具有 TypeScript 的包!
比 JS DOM 更快的性能
最后,但并非最不重要的,在 `wasm-bindgen` 的视野中:超快的 DOM 操作——许多 JS 框架的圣杯。如今,对 DOM 函数的调用必须通过代价高昂的垫片,因为它们需要从 JavaScript 转换为 C++ 引擎实现。然而,有了 WebAssembly,这些垫片将不再需要。众所周知,WebAssembly 类型良好……而且,它确实有类型!
`wasm-bindgen` 代码生成从一开始就被设计为与未来的 [主机绑定提案](https://github.com/WebAssembly/host-bindings) 相兼容。一旦它成为 WebAssembly 中的功能,我们就可以直接调用导入的函数,而无需任何 `wasm-bindgen` 的 JS 垫片。此外,这将使 JS 引擎能够积极优化 WebAssembly 操作 DOM,因为调用类型良好,不再需要 JS 调用所需的参数验证检查。到那时,`wasm-bindgen` 不仅可以简化对更丰富类型(如字符串)的操作,而且还会提供最佳的 DOM 操作性能。
总结
我个人发现 WebAssembly 令人难以置信地令人兴奋,不仅因为社区,还因为快速取得的巨大进步。`wasm-bindgen` 工具拥有光明的未来。它正在让 JS 和 Rust 等语言之间的互操作性成为一流的体验,同时还为 WebAssembly 的持续发展提供长期益处。
尝试使用 [wasm-bindgen
](https://github.com/alexcrichton/wasm-bindgen) 试试,[打开一个 issue](https://github.com/alexcrichton/wasm-bindgen/issues) 来提出功能需求,并以其他方式 [参与](http://fitzgeraldnick.com/2018/02/27/wasm-domain-working-group.html) Rust 和 WebAssembly 的发展!
关于 Alex Crichton
Alex 是 Rust 核心团队的成员,自 2012 年底开始参与 Rust 的开发。如今,他正在帮助 WebAssembly Rust 工作组让 Rust+Wasm 的体验尽可能好。Alex 还帮助维护 Cargo(Rust 的包管理器)、Rust 标准库以及 Rust 的发布和 CI 基础设施。
4 条评论