JavaScript和基于证据的语言设计

原文:JavaScript to Rust and Back Again: A wasm-bindgen Tale

链接:https://hacks.mozilla.org/2018/04/javascript-to-rust-and-back-again-a-wasm-bindgen-tale/

最近我们已经见识了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项目在其自述文件中有更多描述。要开始使用,让我们深入了解一个使用示例,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将alert使用它创建的字符串调用JavaScript 函数。

首先,我们创建一个全新的Rust项目:

$ cargo new wasm-greet --lib

这会初始化一个wasm-greet我们在其中工作的新文件夹。接下来我们使用以下信息修改我们Cargo.toml(相当于package.jsonRust):

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

[dependencies]
wasm-bindgen = "0.2"

我们暂时忽略了这个[lib]业务,但是下一部分声明了对wasm-bindgencrate的依赖。这里的箱子包含我们需要wasm-bindgen在Rust中使用的所有支持。

接下来,是时候编写一些代码了!我们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

这给了我们一个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-bindgenCLI工具对输入wasm文件进行后处理,使其适合使用。稍后我们会看到“合适”意味着什么,现在只要说我们导入新创建的wasm_greet.js文件(由wasm-bindgen工具创建)就足够了,我们就得到了greet我们在Rust中定义的函数。

最后,我们要做的就是用捆绑器打包它,并创建一个HTML页面来运行我们的代码。在撰写本文时,只有Webpack 4.0版本具有足够的WebAssembly支持才能开箱即用(尽管它暂时还有Chrome警告)。随着时间的推移,更多的捆绑商肯定会跟进。我将在此处跳过详细信息,但您可以按照Github仓库中的示例配置进行操作。如果我们查看内部,我们的页面JS看起来像:

const rust = import("./wasm_greet");
rust.then(m => m.greet("World!"));

......就是这样!打开我们的网页现在应该显示一个很好的“Hello,World!”对话框,由Rust驱动。

怎么wasm-bindgen工作

Phew,这是一个有点大的“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_greet.js文件内部,wasm-bindgen我们会看到如下所示的内容:

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捆绑器管道(缩小)之后,这应该小得多。

在这里,我们可以看到如何greet为我们生成函数wasm-bindgen。在引擎盖下它还在调用wasm的greet函数,但它是用指针​​和长度而不是字符串调用的。有关更多详细信息passStringToWasm,请参阅Lin Clark的上一篇文章。除了工具正在为我们处理之外,这是你必须编写的所有样板wasm-bindgen!我们马上就会看到这个__wbg_f_alert_alert_n功能。

更深层次地移动,下一个感兴趣的项目是greetWebAssembly中的函数。看一下,让我们看看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中的一个String)并转发到greet我们定义的函数。

换句话说,该#[wasm_bindgen]属性生成两个包装器:一个在JavaScript中,它将JS类型转换为wasm,另一个在Rust中接收wasm类型并转换为Rust类型。

好吧,让我们看看最后一组包装器,即alert函数。greetRust中的函数使用标准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)
}

哇!事实证明,这里发生了相当多的事情,我们greet在JS中有一个相对较长的链接到alert浏览器。但不要害怕,关键wasm-bindgen是所有这些基础设施都是隐藏的!你只需要在#[wasm_bindgen]这里和那里写一些Rust代码。那么你的JS可以像使用另一个JS包或模块一样使用Rust。

还能wasm-bindgen做什么?

该wasm-bindgen项目范围雄心勃勃,我们没有足够的时间在这里详述所有细节。探索功能的一个好方法wasm-bindgen是探索示例目录,其中包括Hello,World!就像我们上面看到的那样,完全在Rust中操作DOM节点。

在高层次上的特征wasm-bindgen是:

  • 导入JS结构,函数,对象等,以调用ism。您可以在结构和访问属性上调用JS方法,一旦#[wasm_bindgen]注释全部连接,就会给您编写的Rust带来一些“本机”感觉。
  • 将Rust结构和函数导出到JS。而不是让JS只使用数字,你可以导出一个struct变成classJS 的Rust 。然后你可以传递结构,而不是只需要传递整数。该smorgasboard例子给出了支持互操作的好味道。
  • 允许其他各种功能,例如从全局范围(也称为alert函数)导入,使用ResultRust 捕获JS异常,以及模拟在Rust程序中存储JS值的通用方法。

如果您想要看到更多功能,请特别关注问题跟踪器!

下一步是wasm-bindgen什么?

在我们结束之前,我想花点时间描述未来的愿景,wasm-bindgen因为我认为这是当今项目中最令人兴奋的方面之一。

支持不仅仅是Rust

从第1天开始,wasm-bindgenCLI工具的设计考虑了多种语言支持。虽然Rust是当今唯一支持的语言,但该工具也可以插入C或C ++。该#[wasm_bindgen]属性创建输出*.wasm文件的自定义部分,wasm-bindgen工具将解析该部分并稍后删除。本节描述要生成的JS绑定以及它们的接口。关于此描述没有特定于Rust的内容,因此C ++编译器插件可以轻松创建该部分并由该wasm-bindgen工具进行处理。

我发现这方面特别令人兴奋,因为我相信它使工具wasm-bindgen成为WebAssembly和JS集成的标准实践。希望对所有编译为WebAssembly并由捆绑器自动识别的语言都有益,以避免上面几乎所有的配置和构建工具。

自动绑定JS生态系统

今天使用#[wasm_bindgen]宏导入功能时的一个缺点是你必须把所有内容都写出来,并确保你没有犯任何错误。这有时可能是一个繁琐(且容易出错)的过程,对于自动化而言已经成熟。

所有Web API都使用WebIDL指定,从WebIDL 生成#[wasm_bindgen]注释应该非常可行。这意味着您不需要alert像上面那样定义函数,而只需要编写类似的东西:

#[wasm_bindgen]
pub fn greet(s: &str) {
webapi::alert(&format!("Hello, {}!", s));
}

在这种情况下,webapicrate可以完全从Web API的WebIDL描述自动生成,保证没有错误。

我们甚至可以更进一步,利用TypeScript社区的出色工作,并从TypeScript 生成#[wasm_bindgen]。这将允许任何包与npm上免费提供的TypeScript自动绑定!

比JS DOM更快的性能

最后,但并非最不重要的是,wasm-bindgen即将出现:超快的DOM操作 - 许多JS框架的圣杯。今天,当从JavaScript转换到C ++引擎实现时,调用DOM函数必须经历昂贵的垫片。但是,使用WebAssembly,这些垫片不是必需的。众所周知,WebAssembly是很好的类型......而且,有类型!

从第1天wasm-bindgen开始,代码生成就考虑了未来的主机绑定提议。只要这是WebAssembly中提供的功能,我们就能够直接调用导入的函数而不需要任何wasm-bindgenJS填充程序。此外,这将允许JS引擎积极优化WebAssembly操作DOM,因为调用是良好类型的,不再需要从JS调用需要的参数验证检查。此时,wasm-bindgen不仅可以轻松使用字符串等更丰富的类型,而且还可以提供同类最佳的DOM操作性能。

结束

我个人发现WebAssembly令人难以置信的兴奋,不仅因为社区,而且还有如此快速的进步的突飞猛进。该wasm-bindgen工具前景光明。它使JS和Rust之类的语言之间的互操作性成为一流的体验,同时随着WebAssembly的不断发展,也提供了长期的好处。

尝试wasm-bindgen旋转,为功能请求打开一个问题,否则继续参与 Rust和WebAssembly!

关于Alex Crichton(作者)

Alex是Rust核心团队的成员之一,自2012年底以来一直从事于Rust。目前他正在帮助WebAssembly Rust Working Group使得Rust + Wasm成为最佳体验。Alex还帮助维护Cargo(Rust的包管理器),Rust标准库以及Rust的发布和CI的基础架构。

发表评论
留言与评论(共有 0 条评论)
   
验证码:

相关文章

推荐文章

'); })();