编译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) [email protected]

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

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 我们想调用一些外部定义的函数。这个属性告诉我们”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 <[email protected]>"]
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 或者其他版本)。

构建包

现在我们已经完成了所有的设置,让我们开始吧!在您的终端中输入:

$ wasm-pack build --scope mynpmusername

这会做很多事情(而且要花很多时间,尤其是第一次运行wasm-pack)。要详细了解它们,请查看this blog post on Mozilla Hacks。简而言之,wasm-pack build将:

  1. 将您的Rust代码编译为WebAssembly.
  2. wasm-bindgen在该WebAssembly上运行,生成一个JavaScript文件,将该WebAssembly文件包装到npm可以理解的模块中
  3. 创建一个pkg目录并将该JavaScript文件和您的WebAssembly代码移入其中.
  4. 读取Cargo.toml文件并生成同等的内容的package.json文件
  5. 将您的README.md(如果有的话)文件复制到包装中。

最终结果?您在pkg目录中有一个npm包。

对代码体积的一些说明

如果您检查生成的WebAssembly代码大小,则可能是几百KB。我们让Rust优化所有代码的大小,这样做减少了大小很多。 这是本教程的主题,但是,如果您想了解更多信息,请查阅Rust WebAssembly Working Group关于Shrinking .wasm Size文档。

把我们的包发布到 npm

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

$ cd pkg
$ npm publish --access=public

现在,我们有了一个用Rust编写的npm包,但已编译为WebAssembly。它可以在JavaScript中使用,并且不需要用户安装Rust。其中包含的代码是WebAssembly代码,而不是Rust源代码!

在网站上使用我们的包

让我们建立一个使用我们新软件包的网站!许多人通过各种捆绑工具使用npm软件包,在本教程中,我们将使用其中一个webpack。它只是稍微复杂一点,并展示了更实际的用例。

让我们移出pkg目录,并创建一个新目录site,在以下位置进行尝试:

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

创建package.json文件,并输入一下代码:

{
  "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"
  }
}

请注意,您需要在依赖项部分的@之后填写自己的用户名。

接下来,我们需要配置Webpack。创建webpack.config.js文件,并输入一下代码:

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

现在我们需要一个HTML文件。创建index.html文件,并输入以下内容:

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

最后,我们创建在HTML文件中创建引用的index.js文件,并为输入以下内容:

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

请注意,您需要再次填写您的npm用户名。

这将从node_modules文件夹中导入我们的模块。这不是最佳做法,但这是一个演示,因此我们现在就使用它。加载后,它将从该模块调用函数greet,并”WebAssembly”以字符串形式传递。注意这里没有什么特别的,但是我们正在调用Rust代码!就JavaScript代码而言,这只是一个普通模块。

我们已经完成了文件制作!让我们试一下:

$ npm install
$ npm run serve

这将启动Web服务器。查看http://localhost:8080,您应该会看到一个警告框出现在屏幕上Hello, WebAssembly!!,我们已经成功地从JavaScript调用了Rust,并从Rust调用了JavaScript。