从 Rust 编译到 WebAssembly
如果您有一些 Rust 代码,可以将其编译成 WebAssembly (Wasm)。本教程将向您展示如何将 Rust 项目编译成 WebAssembly 并在现有的 Web 应用程序中使用它。
Rust 和 WebAssembly 的用例
Rust 和 WebAssembly 有两个主要的用例
- 构建整个应用程序 - 基于 Rust 的整个 Web 应用程序。
- 构建应用程序的一部分 - 在现有的 JavaScript 前端中使用 Rust。
目前,Rust 团队专注于后一种情况,因此我们在此对其进行介绍。对于前一种情况,请查看 yew
等项目。
在本教程中,我们将使用 wasm-pack
构建一个包,它是一个用于在 Rust 中构建 JavaScript 包的工具。该包将只包含 WebAssembly 和 JavaScript 代码,因此使用该包的用户不需要安装 Rust。他们甚至可能不会注意到它是用 Rust 编写的。
Rust 环境设置
让我们逐步完成设置环境所需的所有步骤。
安装 Rust
访问 安装 Rust 页面并按照说明进行操作以安装 Rust。这将安装一个名为“rustup”的工具,该工具可用于管理多个版本的 Rust。默认情况下,它会安装最新的稳定版 Rust 版本,您可以将其用于一般的 Rust 开发。Rustup 会安装 rustc
(Rust 编译器)、cargo
(Rust 的包管理器)、rust-std
(Rust 的标准库)以及一些有用的文档 - rust-docs
。
注意: 请注意安装后关于系统 PATH
中需要 cargo 的 bin
目录的说明。这会自动添加,但您必须重新启动终端才能使其生效。
wasm-pack
要构建该包,我们需要一个额外的工具,即 wasm-pack
。它有助于将代码编译成 WebAssembly,并生成适合在浏览器中使用的正确打包文件。要下载并安装它,请在您的终端中输入以下命令
cargo install wasm-pack
构建我们的 WebAssembly 包
设置就绪,让我们在 Rust 中创建一个新包。导航到您存放个人项目的目录,然后键入以下内容
cargo new --lib hello-wasm
这将在名为 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
中
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,请查看免费的在线书籍 Rust 编程语言。
使用 wasm-bindgen
在 Rust 和 JavaScript 之间进行通信
第一部分如下所示
use wasm_bindgen::prelude::*;
库在 Rust 中被称为“板条箱”。
明白了吗?Cargo 装载板条箱。
第一行包含一个 use
命令,它将库中的代码导入到您的代码中。在本例中,我们正在导入 wasm_bindgen::prelude
模块中的所有内容。我们将在下一节中使用这些功能。
在我们继续下一节之前,我们应该更多地讨论 wasm-bindgen
。
wasm-pack
使用 wasm-bindgen
(另一个工具)来提供 JavaScript 和 Rust 类型之间的桥梁。它允许 JavaScript 使用字符串调用 Rust API,或者 Rust 函数捕获 JavaScript 异常。
我们在我们的包中使用 wasm-bindgen
的功能。事实上,这就是下一节的内容。
从 Rust 中调用 JavaScript 中的外部函数
下一部分如下所示
#[wasm_bindgen]
extern {
pub fn alert(s: &str);
}
#[ ]
内部的部分被称为“属性”,它以某种方式修改下一条语句。在本例中,该语句是一个 extern
,它告诉 Rust 我们想要调用一些外部定义的函数。该属性表示“wasm-bindgen 知道如何找到这些函数”。
第三行是函数签名,用 Rust 编写。它表示“alert
函数接受一个参数,一个名为 s
的字符串”。
正如您可能猜到的那样,这是 JavaScript 提供的 alert
函数。我们在下一节中调用此函数。
每当您想要调用 JavaScript 函数时,都可以将其添加到此文件中,wasm-bindgen
会为您完成所有设置工作。并非所有内容都得到支持,但我们正在努力。如果缺少某些内容,请 提交错误报告。
生成 JavaScript 可以调用的 Rust 函数
最后一部分是
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}
我们再次看到 #[wasm_bindgen]
属性。在本例中,它不是修改 extern
块,而是修改 fn
;这意味着我们希望此 Rust 函数能够被 JavaScript 调用。它与 extern
相反:这些不是我们需要的函数,而是我们提供给外部世界的函数。
此函数名为 greet
,它接受一个参数,一个字符串 (&str
),name
。然后,它调用我们在上面的 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"
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
填写您自己的存储库,并使用 git
用于 authors
字段的相同信息。
需要添加的主要部分是 [package]
。[lib]
部分告诉 Rust 构建我们包的 cdylib
版本;我们不会在本教程中详细介绍它的含义。有关更多信息,请参阅 Cargo 和 Rust 链接 文档。
最后一部分是 [dependencies]
部分。在这里,我们告诉 Cargo 我们希望依赖 wasm-bindgen
的哪个版本;在本例中,它是任何 0.2.z
版本(但不包括 0.3.0
或更高版本)。
构建该包
现在我们已经完成了所有设置,让我们构建该包。我们将在本机 ES 模块和 Node.js 中使用生成的代码。为此,我们将使用 wasm-pack build
中的 --target
参数 指定要生成的 WebAssembly 和 JavaScript 类型。
首先,运行以下命令
wasm-pack build --target web
这会执行许多操作(它们需要花费大量时间,尤其是在您第一次运行 wasm-pack
时)。要详细了解这些操作,请查看 这篇关于 Mozilla Hacks 的博文。简而言之,wasm-pack build
会
- 将您的 Rust 代码编译成 WebAssembly。
- 在该 WebAssembly 上运行
wasm-bindgen
,生成一个 JavaScript 文件,将该 WebAssembly 文件包装成浏览器可以理解的模块。 - 创建一个
pkg
目录,并将该 JavaScript 文件和您的 WebAssembly 代码移动到该目录中。 - 读取您的
Cargo.toml
并生成一个等效的package.json
。 - 复制您的
README.md
(如果您有的话)到该包中。
最终结果是什么?您将在 pkg
目录中有一个包。
关于代码大小的旁白
如果你查看生成的 WebAssembly 代码大小,它可能会有几百 KB。我们还没有指示 Rust 对大小进行优化,这样做会大大缩减大小。这超出了本教程的范围,但如果你想了解更多信息,请查看 Rust WebAssembly 工作组关于 缩减 .wasm 大小 的文档。
在网络上使用该包
现在我们已经得到了一个编译后的 Wasm 模块,让我们在浏览器中运行它。让我们从在项目的根目录中创建一个名为 index.html
的文件开始,这样最终我们会得到以下项目结构
├── Cargo.lock ├── Cargo.toml ├── index.html <-- new index.html file ├── pkg │ ├── hello_wasm.d.ts │ ├── hello_wasm.js │ ├── hello_wasm_bg.wasm │ ├── hello_wasm_bg.wasm.d.ts │ └── package.json ├── src │ └── lib.rs └── target ├── CACHEDIR.TAG ├── release └── wasm32-unknown-unknown
将以下内容放在 index.html
文件中
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>hello-wasm example</title>
</head>
<body>
<script type="module">
import init, { greet } from "./pkg/hello_wasm.js";
init().then(() => {
greet("WebAssembly");
});
</script>
</body>
</html>
此文件中的脚本将导入 JavaScript 粘合代码,初始化 Wasm 模块,并调用我们在 Rust 中编写的 greet
函数。
使用本地 Web 服务器提供项目的根目录(例如,python3 -m http.server
)。如果您不确定如何操作,请参考 运行简单的本地 HTTP 服务器。
注意:确保使用支持 application/wasm
MIME 类型的最新 Web 服务器。较旧的 Web 服务器可能还不支持它。
从 Web 服务器加载 index.html
(如果您使用 Python3 示例:https://127.0.0.1:8000
)。屏幕上将出现一个警报框,其中包含 Hello, WebAssembly!
。我们已成功地从 JavaScript 调用 Rust,并从 Rust 调用 JavaScript。
使我们的包可供 npm 使用
我们正在构建一个 npm 包,因此您需要安装 Node.js 和 npm。
要获取 Node.js 和 npm,请访问 获取 npm! 页面并按照说明操作。本教程面向 node 20。如果您需要在 node 版本之间切换,可以使用 nvm。
如果您想将 WebAssembly 模块与 npm 一起使用,我们需要做一些更改。让我们从使用 bundler
选项作为目标重新编译 Rust 开始
wasm-pack build --target bundler
现在我们有一个用 Rust 编写的 npm 包,但编译成了 WebAssembly。它已准备好从 JavaScript 使用,并且不需要用户安装 Rust;包含的代码是 WebAssembly 代码,而不是 Rust 源代码。
在 Web 上使用 npm 包
让我们构建一个使用我们的新 npm 包的网站。许多人通过各种捆绑器工具使用 npm 包,在本教程中,我们将使用其中之一,webpack
。它只是有点复杂,并且展示了一个真实的用例。
让我们从 pkg
目录中退回,并创建一个新目录 site
来尝试一下。我们还没有将包发布到 npm 注册表,因此我们可以使用 npm i /path/to/package
从本地版本安装它。您可以使用 npm link
,但从本地路径安装对于此演示的目的来说很方便
cd ..
mkdir site && cd site
npm i ../pkg
安装 webpack
开发依赖项
npm i -D webpack@5 webpack-cli@5 webpack-dev-server@4 copy-webpack-plugin@11
接下来,我们需要配置 Webpack。创建 webpack.config.js
并将以下内容放入其中
const CopyPlugin = require("copy-webpack-plugin");
const path = require("path");
module.exports = {
entry: "./index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "index.js",
},
mode: "development",
experiments: {
asyncWebAssembly: true,
},
plugins: [
new CopyPlugin({
patterns: [{ from: "index.html" }],
}),
],
};
在您的 package.json
中,您可以添加 build
和 serve
脚本,这些脚本将使用我们刚刚创建的配置文件运行 webpack
{
"scripts": {
"build": "webpack --config webpack.config.js",
"serve": "webpack serve --config webpack.config.js --open"
},
"dependencies": {
"hello-wasm": "file:../pkg"
},
"devDependencies": {
"copy-webpack-plugin": "^11.0.0",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
}
}
接下来,创建一个名为 index.js
的文件,并赋予它以下内容
import * as wasm from "hello-wasm";
wasm.greet("WebAssembly with npm");
这从 node_modules
文件夹导入模块并调用 greet
函数,将 "WebAssembly with npm"
作为字符串传递。请注意,这里没有什么特别之处,但我们正在调用 Rust 代码。就 JavaScript 代码而言,这只是一个普通模块。
最后,我们需要添加一个 HTML 文件来加载 JavaScript。创建一个 index.html
文件并添加以下内容
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>hello-wasm example</title>
</head>
<body>
<script src="./index.js"></script>
</body>
</html>
hello-wasm/site
目录应如下所示
├── index.html ├── index.js ├── node_modules ├── package-lock.json ├── package.json └── webpack.config.js
我们已经完成了文件制作。让我们试试看
npm run serve
这将启动一个 Web 服务器并打开 https://127.0.0.1:8080
。您应该看到屏幕上出现一个警报框,其中包含 Hello, WebAssembly with npm!
。我们已成功地将 Rust 模块与 npm 一起使用!
如果您想在本地开发之外使用 WebAssembly,可以使用 pack
和 publish
命令发布包
wasm-pack pack
npm notice
npm notice 📦 [email protected]
npm notice === Tarball Contents ===
npm notice 1.6kB README.md
npm notice 2.5kB hello_wasm_bg.js
npm notice 17.5kB hello_wasm_bg.wasm
npm notice 115B hello_wasm.d.ts
npm notice 157B hello_wasm.js
npm notice 531B package.json
...
hello-wasm-0.1.0.tgz
[INFO]: 🎒 packed up your package!
要发布到 npm,您需要一个 npm 帐户,并使用 npm adduser
授权您的机器。准备就绪后,您可以使用 wasm-pack
发布,它会在幕后调用 npm publish
wasm-pack publish
结论
这就是我们教程的结尾;我们希望您发现它有用。
在这个领域有很多令人兴奋的工作正在进行;如果您想帮助使其变得更好,请查看 Rust 和 WebAssembly 工作组。