Rust 社区在 2018 年的一个重大目标是成为一种 Web 语言。通过面向 WebAssembly,Rust 可以像 JavaScript 一样在 Web 上运行。但这意味着什么呢?这意味着 Rust 试图取代 JavaScript 吗?
这个问题的答案是否定的。我们并不期望 Rust WebAssembly 应用完全用 Rust 编写。事实上,我们期望大多数 Rust WebAssembly 应用中的大部分应用程序代码仍然是 JS。
这是因为 JS 在大多数情况下都是一个不错的选择。使用 JavaScript 快速轻松地启动和运行。最重要的是,有一个充满活力的生态系统,其中充满了 JavaScript 开发人员,他们为 Web 上的不同问题创造了令人难以置信的创新方法。
但有时对于应用程序的特定部分,Rust+WebAssembly 是合适的工具……比如当你解析源代码映射时,或者弄清楚对 DOM 进行哪些更改,比如Ember.
因此对于 Rust+WebAssembly,前进的道路并不止于将 Rust 编译成 WebAssembly。我们需要确保 WebAssembly 能够融入 JavaScript 生态系统。Web 开发人员需要能够像使用 JavaScript 一样使用 WebAssembly。
但 WebAssembly 还没有达到那个程度。为了实现这一点,我们需要构建工具来使 WebAssembly 更容易加载,以及更容易从 JS 中进行交互。这项工作将帮助 Rust。但它也将帮助所有其他以 WebAssembly 为目标的语言。
我们正在解决哪些 WebAssembly 易用性挑战?以下是一些例子:
- 如何轻松地在 WebAssembly 和 JS 之间传递对象?
- 如何将所有内容打包到 npm 中?
- 开发人员如何在捆绑器或浏览器中轻松地组合 JS 和 WASM 包?
但首先,我们在 Rust 中实现了什么?
Rust 将能够调用 JavaScript 函数。JavaScript 将能够调用 Rust 函数。Rust 将能够调用来自宿主平台的函数,比如 alert
。Rust 包将能够依赖 npm 包。并且在所有这些过程中,Rust 和 JavaScript 将以对它们双方都有意义的方式传递对象。
因此,这就是我们在 Rust 中实现的目标。现在让我们看看我们需要解决的 WebAssembly 易用性挑战。
问:如何轻松地在 WebAssembly 和 JS 之间传递对象?
答:<a href="https://github.com/alexcrichton/wasm-bindgen">wasm-bindgen</a>
使用 WebAssembly 最困难的部分之一是将不同类型的值传入和传出函数。这是因为 WebAssembly 目前只有两种类型:整数和浮点数。
这意味着你不能直接将字符串传入 WebAssembly 函数。相反,你必须经过一系列步骤:
- 在 JS 端,将字符串编码为数字(使用类似 TextEncoder API 的东西)。
- 将这些数字放入 WebAssembly 的内存中,它基本上是一个数字数组。
- 将字符串第一个字母的数组索引传递给 WebAssembly 函数。
- 在 WebAssembly 端,使用该整数作为指针来取出数字。
这仅仅是字符串所需的步骤。如果你有更复杂的类型,那么你将需要一个更复杂的流程来获取数据。
如果你使用了大量的 WebAssembly 代码,你可能会将这种胶水代码抽象到一个库中。但如果不用编写所有这些胶水代码,岂不是更好吗?如果能够跨语言边界传递复杂值,并让它们神奇地工作,岂不是更好吗?
这就是 wasm-bindgen
的作用。如果你在 Rust 代码中添加一些注释,它将自动创建在两端都需要执行的代码,以使更复杂的类型工作起来。
这意味着使用这些函数期望的任何类型,从 Rust 中调用 JS 函数。
#[wasm_bindgen] extern { type console; #[wasm_bindgen(static = console)] fn log(s: &str); }
#[wasm_bindgen] pub fn foo() { console::log("hello!"); }
… 或者在 Rust 中使用结构体,并在 JS 中将其作为类使用。
// Rust #[wasm_bindgen] pub struct Foo { contents: u32, } #[wasm_bindgen] impl Foo { pub fn new() -> Foo { Foo { contents: 0 } }
pub fn add(&mut self, amt: u32) -> u32 { self.contents += amt; return self.contents } }
// JS import { Foo } from "./js_hello_world";
let foo = Foo.new(); assertEq(foo.add(10), 10); foo.free();
… 或者许多其他好处。
在幕后,wasm-bindgen
的设计是语言无关的。这意味着随着工具的稳定,它应该可以扩展对其他语言(如 C/C++)中的结构的支持。
Alex Crichton 将在几周后写更多关于 wasm-bindgen
的内容,所以请关注那篇文章。
问:如何将所有内容打包到 npm 中?
答:<a href="https://github.com/ashleygwilliams/wasm-pack/">wasm-pack</a>
一旦我们将其组合在一起,我们就会有一堆文件。有一个编译后的 WebAssembly 文件。然后是所有 JavaScript——依赖项和 wasm-bindgen
生成的 JS。我们需要一种方法将它们全部打包起来。此外,如果我们添加了任何 npm 依赖项,我们需要将其放入 package.json
清单文件中。
同样,如果这能够自动完成,那就太好了。这就是 wasm-pack
的作用。它是一站式商店,从编译后的 WebAssembly 文件到 npm 包。
它将为你运行 wasm-bindgen
。然后,它将获取所有文件并将其打包起来。它将在顶部添加一个 package.json
,填充来自 Rust 代码的所有 npm 依赖项。然后,你只需要执行 npm publish
即可。
同样,该工具的基础是语言无关的,因此我们预计它将支持多种语言生态系统。
Ashley Williams 将在下个月写更多关于 wasm-pack
的内容,所以那将是另一篇值得关注的文章。
问:开发人员如何在捆绑器、浏览器或 Node 中轻松地组合 JS 和 WASM?
答:ES 模块
现在我们已经将 WebAssembly 发布到 npm,我们如何让它在 JS 应用程序中轻松使用?
让将 WebAssembly 包添加为依赖项变得更容易……将其包含在 JS 模块依赖项图中。
目前,WebAssembly 有一个用于创建模块的命令式 JS API。你必须编写代码来执行每个步骤,从获取文件到准备依赖项。这是一项艰苦的工作。
但现在,浏览器中已经有了原生模块支持,我们可以添加一个声明式 API。具体来说,我们可以使用 ES 模块 API。有了它,使用 WebAssembly 模块应该就像导入它们一样简单。
我们正在与 TC39 和 WebAssembly 社区小组合作,对此进行标准化。
但我们不仅仅需要标准化 ES 模块支持。即使浏览器和 Node 支持 ES 模块,开发人员也可能仍然使用捆绑器。这是因为捆绑器减少了你必须为模块文件发出的请求数量,这意味着下载代码需要更少的时间。
捆绑器通过将来自不同文件的一堆模块组合成一个文件,然后在顶部添加一些运行时来加载它们,从而实现这一点。
捆绑器仍然需要使用 JS API 来创建模块,至少在短期内是这样。但用户将使用 ES 模块语法进行创作。这些用户期望他们的模块的行为就像 ES 模块一样。我们需要在 WebAssembly 中添加一些功能,使其更容易让捆绑器模拟 ES 模块。
我将写更多关于将 ES 模块集成到 WebAssembly 规范中的工作。我还会在接下来的几个月里深入探讨捆绑器及其对 WebAssembly 的支持。
结论
为了成为一种有用的 Web 语言,Rust 需要与 JavaScript 生态系统很好地协同工作。我们需要做一些工作才能实现这一目标,幸运的是,这项工作将帮助其他语言。你想帮助让 WebAssembly 更好地服务于所有语言吗?加入我们吧!我们很乐意帮助你开始 :)
关于 Lin Clark
Lin 在 Mozilla 的高级开发部门工作,专注于 Rust 和 WebAssembly。
10 条评论