转载

改进 JavaScript 和 Rust 的互操作性:深入认识 wasm-bindgen 组件

最近我们已经见识了WebAssembly如何 快速编译 、加速 JS 库以及生成 更小的二进制 格式。我们甚至为Rust和JavaScript社区以及其他Web编程语言之间的更好的互操作性制定了 高级规划 。正如 前面一篇文章 中提到的,我想深入了解一个特定组件的细节, wasm-bindgen 。

今天 WebAssembly标准 只定义了四种类型:两种整数类型和两种浮点类型。然而,大多数情况下,JS和Rust开发人员正在使用更丰富的类型! 例如,JS开发人员经常与互以添加或修改HTML节点相关的 文档 交互,而Rust开发人员使用类似 Result 等类型进行错误处理,几乎所有程序员都使用字符串。

被局限在仅使用由WebAssembly所提供的类型将会受到太多的限制,这就是wasm-bindgen出现的原因。wasm-bindgen的目标是提供一个JS和Rust类型之间的桥接。它允许JS使用字符串调用Rust API,或Rust函数捕获JS异常。wasm-bindgen抹平了WebAssembly和JavaScript之间的阻抗失配,确保JavaScript可以高效地调用WebAssembly函数,并且无需boilerplate,同时WebAssembly可以对JavaScript函数执行相同的操作。

wasm-bindgen项目在其 README 文件中有更多描述。要入门,让我们深入到一个使用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的 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 的依赖。这里的依赖包含了我们使用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 来看这个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 对WebAssembly的使用有足够的支持(尽管暂时已经有了 Chrome caveat )。总有一天,更多的bundler也会接着支持WebAssmbly。在这我不再描述细节,但是你可以看一下在Github仓库里的 example 配置。不过如果我们看内容,这个页面中我们的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 来实现 ,但总会不需要的。而且,其它打包工具可能没有此功能。

如果我们看看由 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 。它包含了所有的模板,对我们来说这是除了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 (Rust中的一个字符串),然后将它传递给我们定义的greet函数。

从另一个方面看,#[wasm_bindgen]属性生成了两个wrappers:一个是在JavaScript中将JS类型的转换为wasm,另外一个是在Rust中接收wasm类型并将其转为Rust类型。

现在我们来看wrappers的最后一块,即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函数事实上是一个简化的wrapper,它带有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中的浏览器中的警告都有一个相对较长的知识链。不过,不要害怕,wasm-bindgen的核心是所有这些基础设施都被隐藏了! 你只需要在随便使用几个#[wasm_bindgen]编写Rust代码即可。然后你的JS可以像使用另一个JS包或模块一样使用Rust了。

原文  https://www.oschina.net/translate/javascript-to-rust-and-back-again-a-wasm-bindgen-tale
正文到此结束
Loading...