最近我们已经见识了 webAssembly 如何 快速编译 https://hacks.mozilla.org/2018/01/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler/ , 加速 JS https://hacks.mozilla.org/2018/01/oxidizing-source-maps-with-rust-and-webassembly/ 库以及生成 更小的二进制 https://hacks.mozilla.org/2018/01/shrinking-webassembly-and-javascript-code-sizes-in-emscripten/ 格式. 我们甚至为 Rust 和 JavaScript 社区以及其他 Web 编程语言之间的更好的互操作性制定了 高级规划 https://hacks.mozilla.org/2018/03/making-webassembly-better-for-rust-for-all-languages/ . 正如 前面一篇文章 https://hacks.mozilla.org/2018/03/making-webassembly-better-for-rust-for-all-languages/ 中提到的, 我想深入了解一个特定组件的细节, https://github.com/alexcrichton/wasm-bindgen .
今天 WebAssembly 标准 https://webassembly.github.io/spec/ 只定义了四种类型: 两种整数类型和两种浮点类型. 然而, 大多数情况下, JS 和 Rust 开发人员正在使用更丰富的类型! 例如, JS 开发人员经常与互以添加或修改 html 节点相关的 文档 https://developer.mozilla.org/en-US/docs/Web/API/Document 交互, 而 Rust 开发人员使用类似 https://doc.rust-lang.org/std/result/enum.Result.html 等类型进行错误处理, 几乎所有程序员都使用字符串.
被局限在仅使用由 WebAssembly 所提供的类型将会受到太多的限制, 这就是 wasm-bindgen 出现的原因. wasm-bindgen 的目标是提供一个 JS 和 Rust 类型之间的桥接. 它允许 JS 使用字符串调用 Rust API, 或 Rust 函数捕获 JS 异常. wasm-bindgen 抹平了 WebAssembly 和 JavaScript 之间的阻抗失配, 确保 JavaScript 可以高效地调用 WebAssembly 函数, 并且无需 boilerplate, 同时 WebAssembly 可以对 JavaScript 函数执行相同的操作.
wasm-bindgen 项目在其 https://github.com/alexcrichton/wasm-bindgen/blob/master/README.md 文件中有更多描述. 要入门, 让我们深入到一个使用 wasm-bindgen 的例子中, 然后探索它还有提供了什么.
Hello, World!
学习新工具的最好也是最经典的方法之一就是探索下用它来输出 "Hello, World!". 在这里, 我们将探索一个这样的例子 -- 在页面里弹出 "Hello World!" 提醒框.
这里的目标很简单, 我们想要定义一个 Rust 的函数, 给定一个名字, 它会在页面上创建一个对话框, 上面写着 Hello,$name! 在 JavaScript 中, 我们可以将这个函数定义为:
- export function greet(name) {
- alert(`Hello, ${name}!`);
- }
不过在这个例子里要注意的是, 我们将把它用 Rust 编写. 这里已经发生了很多我们必须要处理的事情:
JavaScript 将会调用一个 WebAssembly 模块, 模块名是 greetexport.
Rust 函数将一个字符串作为输入参数, 也就是我们要打招呼的名字.
在内部 Rust 会生成一个新的字符串, 也就是传入的名字.
最后 Rust 会调用 JavaScript 的 https://developer.mozilla.org/en-US/docs/Web/API/Window/alert 函数, 以刚创建的字符串作为参数.
启动第一步, 我们创建一个新的 Rust 工程:
$ cargo new wasm-greet --lib
这将初始化一个新的 wasm-greet 文件夹, 我们的工作都在这里面完成. 接下来我们要使用如下信息修改我们的 Cargo.toml (在 Rust 里 相当于 package.json) :
- [lib]
- crate-type = ["cdylib"]
- [dependencies]
- wasm-bindgen = "0.2"
我们先忽略 [lib] 节的内容, 接下来的部分声明了对 https://crates.io/crates/wasm-bindgen 的依赖. 这里的依赖包含了我们使用 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 代码中的注释, 这里的意思是 "请在必要时用 wrapper 处理这个". 我们对 alert 函数的导入和 greet 函数的导出都被标注为这个属性. 稍后, 我们将看到在引擎盖下发生了什么.
首先, 我们从在浏览器中打开作为例子来切入正题! 我们先编译 wasm 代码:
$ rustup target add wasm32-unknown-unknown --toolchain nightly # only needed once
$ cargo +nightly build --target wasm32-unknown-unknown
这段代码会生成一个 wasm 文件, 路径为 target/wasm32-unknown-unknown/debug/wasm_greet.wasm. 如果我们使用工具如 wasm2wat https://github.com/WebAssembly/wabt 来看这个 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 文件做后期处理, 使它变的 "suitable" 可用. 我们待会再来看 "suitable" 的意思, 现在我们可以肯定的说, 如果我们引入刚创建的 wasm_greet.js 文件(wasm-bindgen 工具创建的), 我们已经获取到了在 Rust 中定义的 greet 函数.
最终我们接下来要做的是使用 bundler 对其打包, 然后创建一个 HTML 页面运行我们的代码. 在写这篇文章的时候, 只有 Webpack's 4.0 release https://medium.com/webpack/webpack-4-released-today-6cdb994702d4 对 WebAssembly 的使用有足够的支持(尽管暂时已经有了 Chrome caveat https://github.com/alexcrichton/wasm-bindgen/blob/master/examples/hello_world/README.md#caveat-for-chrome-users ). 总有一天, 更多的 bundler 也会接着支持 WebAssmbly. 在这我不再描述细节, 但是你可以看一下在 Github 仓库里的 https://github.com/alexcrichton/wasm-bindgen/tree/master/examples/hello_world 配置. 不过如果我们看内容, 这个页面中我们的 JS 在看起来是这样的:
- const rust = import("./wasm_greet");
- rust.then(m => m.greet("World!"));
... 就是这些了! 现在打开我们的网页就会显示一个不错的 "Hello, World!" 对话框, 这就是 Rust 驱动的.
wasm-bindgen 是如何工作的
唷, 那是一个巨大的 "Hello, World!". 让我们深入了解一下更多的细节, 以了解后台发生了什么以及该工具是如何工作的.
wasm-bindgen 最重要的方面之一就是它的集成基本上是建立在一个概念之上的, 即一个 wasm 模块仅是另一种 ES 模块. 例如, 在上述中我们想要一个带有如下签名的 ES 模块(在 Typescript 中):
export function greet(s: string);
WebAssembly 无法在本地执行此操作(请记住, 它目前只支持数字), 所以我们依靠 wasm-bindgen 来填补空白. 在上述的最后一步中, 当我们运行 wasm-bindgen 工具时, 你会注意到 wasm_greet.js 文件与 wasm_greet_bg.wasm 文件一起出现. 前者是我们想要的实际 JS 接口, 执行任何必要的处理以调用 Rust.* _bg.wasm 文件包含实际的实现和我们所有的编译后的代码.
我们可以通过引入 ./wasm_greet 模块得到 Rust 代码愿意暴露出来的东西. 我们已经看到了是如何集成的, 可以继续看看执行的结果如何. 首先是我们的示例:
- const rust = import("./wasm_greet");
- rust.then(m => m.greet("World!"));
我们在这里以异步的方式导入接口, 等待导入完成(下载和编译 wasm). 然后调用模块的 greet 函数.
注: 这里用到的异步加载 目前需要 Webpack 来实现 https://github.com/webpack/webpack/issues/6615 , 但总会不需要的. 而且, 其它打包工具可能没有此功能.
如果我们看看由 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) {
- // ...
- }
注: 记住这是生成的, 未经优化的代码, 它可能既不优雅也不简洁!! 在 Rust 中通过 LTO(Link Time Optimization, 连接时优化)创建新的发行版, 再通过 JS 打包工具流程 (压缩) 之后, 可能会精简一些.
现在可以了解如何使用 wasm-bindgen 来生成 greet 函数. 在底层它仍然调用 wasm 的 greet 函数, 但是它是用一个指针和长度来调用的而不是用字符串. 了解 passStringToWasm 的更多细节可以访问 Lin Clark's previous post https://hacks.mozilla.org/2018/03/making-webassembly-better-for-rust-for-all-languages/ . 它包含了所有的模板, 对我们来说这是除了 wasm-bindgen 工具以外还需要去写的东西! 然后我们接下来看__wbg_f_alert_alert_n 函数.
进入更深一层, 下一个我们感兴趣的就是 WebAssmbly 中的 greet 函数. 为了了解这个, 我们先来看 Rust 编译器能访问到的代码. 注意像上面生成的这种 JS wrapper, 在这里你不用写 greet 的导出符号,#[wasm_bindgen]属性会生成一个 shim, 由它来为你翻译, 命名如下:
- 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_bindgen]属性插入的看起来有意思的函数__wasm_bindgen_generated_greet. 这是一个导出函数 (用 #[export_name] 和 extern 关键词来指定的), 参数为 JS 传进来的指针 / 长度对. 在函数中它会将这个指针 / 长度转换为一个 &str https://doc.rust-lang.org/std/primitive.str.html (Rust 中的一个字符串), 然后将它传递给我们定义的 greet 函数.
从另一个方面看,#[wasm_bindgen]属性生成了两个 wrappers: 一个是在 JavaScript 中将 JS 类型的转换为 wasm, 另外一个是在 Rust 中接收 wasm 类型并将其转为 Rust 类型.
现在我们来看 wrappers 的最后一块, 即 alert 函数. Rust 中的 greet 函数使用标准 format! https://doc.rust-lang.org/std/macro.format.html 宏来创建一个新的字符串然后传给 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 函数事实上是一个简化的 wrapper, 它带有 Rust 的 &str https://doc.rust-lang.org/std/primitive.str.html 然后将它转换为 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 中的浏览器中的警告都有一个相对较长的知识链. 不过, 不要害怕, wasm-bindgen 的核心是所有这些基础设施都被隐藏了! 你只需要在随便使用几个#[wasm_bindgen]编写 Rust 代码即可. 然后你的 JS 可以像使用另一个 JS 包或模块一样使用 Rust 了.
来源: http://www.tuicool.com/articles/n2UZfqI