如果你写了一些 Rust 代码,你可以把它编译成 WebAssembly!这份教程将带你编译 Rust 项目为 wasm 并在一个现存的 web应用中使用它。
Rust 和 WebAssembly 有两大主要用例:
目前,Rust 团队正专注于第二种用例,因此我们也将着重介绍它。对于第一种用例,可以参阅 yew
这类项目。
在本教程中,我们将使用 Rust 的 npm 包构建工具 wasm-pack
来构建一个 npm
包。这个包将只包含 WebAssembly 和 JavaScript 代码,以便此包的用户无需安装
Rust 就能使用。他们甚至不需要知道这里包含 WebAssembly!
让我们看看安装 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
。它会帮助我们把我们的代码编译成
WebAssembly 并制造出正确的 npm
包。使用下面的命令可以下载并安装它:
$ cargo install wasm-pack
在这个例子中我们将会构建一个 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 解决。
万事俱备,来创建一个新的 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);
}
}
我们完全不需要使用这些测试代码,所以继续吧,我们删掉它。
让我们在 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
的功能。事实上,这是下一节的内容!
接下来的部分看起来像这样:
#[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。
最后一部分是这样的:
#[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!” 的消息框。
我们的库写完了,是时候构建它了。
为了能够正确的编译我们的代码,首先我们需要配置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
版本;在本教程中我们不会讲解它的含义。有关更多信息,请参阅Cargo和Rust 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
将:
wasm-bindgen
在该WebAssembly上运行,生成一个JavaScript文件,将该WebAssembly文件包装到npm可以理解的模块中pkg
目录并将该JavaScript文件和您的WebAssembly代码移入其中.Cargo.toml
文件并生成同等的内容的package.json
文件README.md
(如果有的话)文件复制到包装中。最终结果?您在pkg
目录中有一个npm包。
如果您检查生成的WebAssembly代码大小,则可能是几百KB。我们让Rust优化所有代码的大小,这样做减少了大小很多。 这是本教程的主题,但是,如果您想了解更多信息,请查阅Rust WebAssembly Working Group关于Shrinking .wasm Size文档。
把我们的新包发布到 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。