编译 Rust 为 WebAssembly

如果你写了一些 Rust 代码,你可以把它编译成 WebAssembly!这份教程将带你编译 Rust 项目为 wasm 并在一个现存的 web 应用中使用它。

Rust 和 WebAssembly 用例

Rust 和 WebAssembly 有两大主要用例:

  • 构建完整应用 —— 整个 Web 应用都基于 Rust 开发!
  • 构建应用的组成部分 —— 在现存的 JavaScript 前端中使用 Rust。

目前,Rust 团队正专注于第二种用例,因此我们也将着重介绍它。对于第一种用例,可以参阅 yew 这类项目。

在本教程中,我们将使用 Rust 的 npm 包构建工具 wasm-pack 来构建一个 npm 包。这个包只包含 WebAssembly 和 JavaScript 代码,以便包的用户无需安装 Rust 就能使用。他们甚至不需要知道这里包含 WebAssembly!

安装 Rust 环境

让我们看看安装 Rust 环境的所有必要步骤。

安装Rust

前往 Install Rust 页面并跟随指示安装 Rust。这里会安装一个名为 “rustup” 的工具,这个工具能让你管理多个不同版本的 Rust。默认情况下,它会安装用于惯常Rust开发的 stable 版本 Rust Release。Rustup 会安装 Rust 的编译器 rustc、Rust 的包管理工具 cargo、Rust 的标准库 rust-std 以及一些有用的文档 rust-docs

Note: 需要注意,在安装完成后,你需要把 cargo 的 bin 目录添加到你系统的 PATH 。一般来说它会自动添加,但需要你重启终端后才会生效。 

wasm-pack

要构建我们的包,我们需要一个额外工具 wasm-pack。它会帮助我们把我们的代码编译成 WebAssembly 并制造出正确的 npm 包。使用下面的命令可以下载并安装它:

$ cargo install wasm-pack

安装 Node.js 并获取 npm 账户

在这个例子中我们将会构建一个 npm 包,因此你需要确保安装 Node.js 和 npm 已经安装。另外,我们将会把包发布到 npm 上,因此你还需要一个 npm 账号。它们是免费的。发布这个包并不是必须的,但是发布它非常简单,因此在本例中我们默认你会发布这个包。

Get npm! 页面按照说明下载并安装 Node.js 和 npm。在选择版本时,选择一个你喜欢的版本;本例不限定特定版本。

npm signup page 注册 npm 账户,并填写表格。

接下来,在命令行中运行 npm adduser:

> npm adduser
Username: yournpmusername
Password:
Email: (this IS public) you@example.com

你需要完善你的用户名,密码和邮箱。如果成功了,你将会看到:

Logged in as yournpmusername on https://registry.npmjs.org/.

如果并未正常运行,请联系 npm 解决。

构建我们的 WebAssembly npm 包

万事俱备,来创建一个新的 Rust 包吧。打开你用来存放你私人项目的目录,做这些事:

$ cargo new --lib hello-wasm
     Created library `hello-wasm` project

这里会在名为 hello-wasm 的子目录里创建一个新的库,里面有下一步之前你所需要的一切:

+-- Cargo.toml
+-- src
    +-- lib.rs

首先,我们有一个 Cargo.toml 文件,这是我们配置构建的方式。如果你用过 Bundler 的 Gemfile 或者 npm 的 package.json,你应该会感到很熟悉。Cargo 的用法和它们类似。

接下来,Cargo 在 src/lib.rs 生成了一些 Rust 代码:

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}

我们完全不需要使用这些测试代码,所以继续吧,我们删掉它。

来写点 Rust 代码吧!

让我们在 src/lib.rs 写一些代码替换掉原来的:

extern crate wasm_bindgen;

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern {
    pub fn alert(s: &str);
}

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

这就是我们这个 Rust 项目的内容。它有三个主要部分,让我们按顺序来讲。这里将会给出一个缺少部分细节的高级说明;如果想要了解更多 Rust 知识,请查看在线书籍 The Rust Programming Language

使用 wasm-bindgen 在 Rust 与 JavaScript 之间通信

第一部分看起来像这样:

extern crate wasm_bindgen;

use wasm_bindgen::prelude::*;

第一行就像在说 “哇 Rust,我们在用一个叫做 wasm_bindgen 的库”。在 Rust 当中,库被称为 “crates”,因为我们使用的是一个外部库,所以有  "extern"。

明白了吗? Cargo ships crates.

第三行包括了一个将库中的代码引入到你的代码中的使用命令。在这个情况下,将会引入 wasm_bindgen::prelude 的全部模块。我们将在下一节中使用这些内容。

在我们开始下一节之前,我们将讲一讲 wasm-bindgen.

wasm-pack 使用 wasm-bindgen,其它工具,去提供一个连接 JavaScript 和 Rust 的桥。它允许 JavaScript 使用 string 调用 Rust API,或者调用一个 Rust function 去捕获 JavaScript 异常。

我们将在我们的包中使用 wasm-bindgen 的功能。事实上,这是下一节的内容!

在 JavaScript 中调用来自 Rust 的外部函数

接下来的部分看起来像这样:

#[wasm_bindgen]
extern {
    pub fn alert(s: &str);
}

#[] 中的内容叫做 "属性",并以某种方式改变下面的语句。在这种情况下,下面的语句是一个 extern,它将告诉 Rust that 我们想调用一些外部定义的函数。这个属性告诉我们 "wasm-bindgen 知道如何找到这些函数"。

第三行是用 Rust 写的函数签名。它告诉我们 "alert 函数接受一个叫做 s 的字符串作为参数。"

你可能会疑惑这个函数是什么,你的疑惑可能是正确的:这是 the alert function provided by JavaScript!我们将在下一节中调用这个函数。

当你想调用新的 JavaScript 函数时,你可以在这里写他们,wasm-bindgen 将负责为您设置一切。并非一切都得到支持,但我们正在努力!如果缺少某些内容,file bugs

编写能够在JavaScript中调用的 Rust 函数

最后一部分是这样的:

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

我们又看到了 #[wasm_bindgen] 属性。在这里,它并非定义一个 extern 块,而是 fn,这代表我们希望能够在 JavaScript 中使用这个 Rust 函数。 这和 extern 正相反:我们并非引入函数,而是要把函数给外部世界使用。

这个函数的名字是 greet,它需要一个参数,一个字符串 (写作 &str)。它调用了我们前面在 extern 块中引入的 alert 函数。它传递了一个让我们串联字符串的 format! 宏的调用。

format! 在这里有两个参数,一个格式化字符串和一个要填入的变量。格式化字符串是 "Hello, {}!" 部分。它可以包含一个或多个 {},变量将会被填入其中。传递的变量是 name,也就是这个函数的参数。所以当我们调用 greet("Steve")时我们就能看到 "Hello, Steve!"。

这个传递到了 alert(),所以当我们调用这个函数时,我们应该能看到他谈弹出了一个带有 "Hello, Steve!" 的消息框。

我们的库写完了,是时候构建它了。

把我们的代码编译到 WebAssembly

为了能够正确的编译我们的代码,首先我们需要配置 Cargo.toml。打开这个文件,将内容改为如下所示:

[package]
name = "hello-wasm"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
description = "A sample project with wasm-pack"
license = "MIT/Apache-2.0"
repository = "https://github.com/yourgithubusername/hello-wasm"

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

[dependencies]
wasm-bindgen = "0.2"

你需要改为自己的仓库,同时 Cargo 需要通过 git 来完善 authors 部分。

最重要的是添加底下的部分。第一个部分 — [lib] — 告诉 Rust 为我们的包建立一个 cdylib 版本;在本教程中我们不会讲解它的含义。有关更多信息,请参阅 CargoRust Linkage 文档。

第二个部分是 [dependencies] 部分。在这里我们告诉 Cargo 我们需要依赖哪个版本的 wasm-bindgen ;在这个例子中,它是 0.2.z 版本的 (不是 0.3.0 或者其他版本)。

构建包

Now that we've got everything set up, let's build! Type this into your terminal:

$ wasm-pack build --scope mynpmusername

This will do a number of things (and they take a lot of time, especially the first time you run wasm-pack). To learn about them in detail, check out this blog post on Mozilla Hacks. In short, wasm-pack build will:

  1. Compile your Rust code to WebAssembly.
  2. Run wasm-bindgen on that WebAssembly, generating a JavaScript file that wraps up that WebAssembly file into a module npm can understand.
  3. Create a pkg directory and move that JavaScript file and your WebAssembly code into it.
  4. Read your Cargo.toml and produce an equivalent package.json.
  5. Copy your README.md (if you have one) into the package.

The end result? You have an npm package inside of the pkg directory.

对代码体积的一些说明

If you check out the generated WebAssembly code size, it may be a few hundred kilobytes. We haven't instructed Rust to optimize for size at all, and doing so cuts down on the size a lot. This is off-topic for this tutorial, but if you'd like to learn more, check out the Rust WebAssembly Working Group's documentation on Shrinking .wasm Size.

把我们的包发布到 npm

把我们的新包发布到 npm registry:

$ cd pkg
$ npm publish --access=public

We now have an npm package, written in Rust, but compiled to WebAssembly. It's ready for use from JavaScript, and doesn't require the user to have Rust installed; the code included was the WebAssembly code, not the Rust source!

在网站上使用我们的包

Let's build a website that uses our new package! Many people use npm packages through various bundler tools, and we'll be using one of them, webpack, in this tutorial. It's only slightly more complex, and shows off a more realistic use-case.

Let's move back out of the pkg directory, and make a new directory, site, to try this out in:

$ cd ../..
$ mkdir site
$ cd site

Create a new file, package.json, and put the following code in it:

{
  "scripts": {
    "serve": "webpack-dev-server"
  },
  "dependencies": {
    "@mynpmusername/hello-wasm": "^0.1.0"
  },
  "devDependencies": {
    "webpack": "^4.25.1",
    "webpack-cli": "^3.1.2",
    "webpack-dev-server": "^3.1.10"
  }
}

Note that you'll need to fill in your own username, after the @, in the dependencies section.

Next, we need to configure Webpack. Create webpack.config.js and put the following in it:

const path = require('path');
module.exports = {
  entry: "./index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "index.js",
  },
  mode: "development"
};

Now we need an HTML file; create an index.html and give it the following contents:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>hello-wasm example</title>
  </head>
  <body>
    <script src="./index.js"></script>
  </body>
</html>

Finally, create the index.js referenced in the HTML file and give it these contents:

const js = import("./node_modules/@yournpmusername/hello-wasm/hello_wasm.js");
js.then(js => {
  js.greet("WebAssembly");
});

Note that you need to fill in your npm username again.

This imports our module from the node_modules folder. This isn't considered a best practice, but this is a demo, so we'll work with it for now. Once it's loaded, it calls the greet function from that module, passing "WebAssembly" as a string. Note how there's nothing special here, yet we're calling into Rust code! As far as the JavaScript code can tell, this is just a normal module.

We're done making files! Let's give this a shot:

$ npm install
$ npm run serve

This will start up a web server. Load up http://localhost:8080 and you should see an alert box come on the screen, with Hello, WebAssembly! in it! We've successfully called from JavaScript into Rust, and from Rust into JavaScript.

结论

本教程到此结束。希望你觉得它有用。

在这个领域,有很多工作正在推进当中。如果你希望它变得更好,可以参阅  Rust Webassembly 工作组