从 Rust 编译到 WebAssembly
如果你有一些 Rust 代码,你可以将其编译成 WebAssembly (Wasm)。本教程将向你展示如何将 Rust 项目编译成 WebAssembly 并在现有 Web 应用程序中使用它。
Rust 和 WebAssembly 的用例
Rust 和 WebAssembly 主要有两个用例
- 构建整个应用程序 — 整个基于 Rust 的 Web 应用程序。
- 构建应用程序的一部分 — 在现有 JavaScript 前端中使用 Rust。
目前,Rust 团队专注于后者,因此我们在此处介绍这一点。对于前者,请查看 yew
和 leptos 等项目。
在本教程中,我们使用 wasm-pack
构建一个包,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 还在 src/lib.rs
中为我们生成了一些 Rust 代码
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
让我们编写一些 Rust
我们将不会使用上面显示的生成的 src/lib.rs
代码;将其替换为以下内容
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
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 中被称为“crates”。
明白了吗?Cargo 运送 crates。
第一行包含一个 use
命令,它将库中的代码导入到你的代码中。在这种情况下,我们导入了 wasm_bindgen::prelude
模块中的所有内容。我们在下一节中使用这些特性。
在进入下一节之前,我们应该更多地讨论 wasm-bindgen
。
wasm-pack
使用 wasm-bindgen
(另一个工具)来提供 JavaScript 和 Rust 类型之间的桥梁。它允许 JavaScript 使用字符串调用 Rust API,或者 Rust 函数捕获 JavaScript 异常。
我们在包中使用 wasm-bindgen
的功能。事实上,这就是下一节。
从 Rust 调用 JavaScript 中的外部函数
下一部分如下所示
#[wasm_bindgen]
extern "C" {
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!”的 alert 框。
现在我们的库已经编写完成,让我们构建它。
将我们的代码编译为 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"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
填写你自己的仓库,并使用 git
用于 authors
字段的相同信息。
要添加的重要部分是 [package]
。[lib]
部分告诉 Rust 构建我们包的 cdylib
版本;我们不会在本教程中深入探讨这意味着什么。有关更多信息,请查阅 Cargo 和 Rust Linkage 文档。
最后一部分是 [dependencies]
部分。我们在此处告诉 Cargo 我们想要依赖哪个版本的 wasm-bindgen
;在这种情况下,它是任何 0.2.z
版本(但不包括 0.3.0
或更高版本)。
构建包
现在我们已经完成了设置,让我们构建包。我们将在原生 ES 模块和 Node.js 中使用生成的代码。为此,我们将在 wasm-pack build
中使用 --target
参数 来指定生成的 WebAssembly 和 JavaScript 的类型。
首先,在你的 hello-wasm
目录中运行以下命令
wasm-pack build --target web
这做了几件事。要详细了解它们,请查看 Mozilla Hacks 上的这篇博客文章。简而言之,wasm-pack build
- 将你的 Rust 代码编译为 WebAssembly。
- 对该 WebAssembly 运行
wasm-bindgen
,生成一个 JavaScript 文件,该文件将该 WebAssembly 文件包装成浏览器可以理解的模块。 - 创建
pkg
目录并将该 JavaScript 文件和你的 WebAssembly 代码移入其中。 - 读取你的
Cargo.toml
并生成一个等效的package.json
。 - 将你的
README.md
(如果有)复制到包中。
最终结果?你在 pkg
目录中有一个包。
在 Web 上使用包
现在我们已经有了一个编译好的 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://:8000
)。屏幕上会出现一个警报框,其中包含 Hello, WebAssembly!
。我们已经成功地从 JavaScript 调用到 Rust,并从 Rust 调用到 JavaScript。
让我们的包可用于 npm
我们正在构建一个 npm 包,因此你需要安装 Node.js 和 npm。
要获取 Node.js 和 npm,请访问 获取 npm! 页面并按照说明进行操作。本教程的目标是 node 20。要在节点版本之间切换,你可以使用 nvm。
要将 WebAssembly 模块与 npm 一起使用,我们需要进行一些更改。让我们首先使用 bundler
选项作为目标重新编译我们的 Rust
wasm-pack build --target bundler
我们现在有一个用 Rust 编写但编译为 WebAssembly 的 npm 包。它已准备好从 JavaScript 中使用,并且不需要用户安装 Rust;包含的代码是 WebAssembly 代码,而不是 Rust 源代码。
在 Web 上使用 npm 包
让我们构建一个使用我们新 npm 包的网站。许多人通过各种打包工具使用 npm 包,我们将在本教程中使用其中一个工具 webpack
。它只是一点点复杂,并展示了一个真实的用例。
让我们在 hello-wasm
目录中创建一个名为 site
的新目录来试用它。我们尚未将包发布到 npm 注册表,因此我们可以使用 npm i /path/to/package
从本地版本安装它。你可以使用 npm link
,但从本地路径安装对于此演示来说很方便
mkdir site && cd site
npm i ../pkg
安装 webpack
开发依赖项
npm i -D webpack@5 webpack-cli@5 webpack-dev-server@5 copy-webpack-plugin@12
接下来,我们需要配置 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": "^12.0.2",
"webpack": "^5.97.1",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.1.0"
}
}
接下来,创建一个名为 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
目录应如下所示
├── node_modules ├── index.html ├── index.js ├── package-lock.json ├── package.json └── webpack.config.js
我们已经完成了文件的制作。让我们试一试
npm run serve
这将启动一个 Web 服务器并打开 https://:8080
。你会在屏幕上看到一个警报框,其中包含文本 Hello, WebAssembly with npm!
。我们已成功将 Rust 模块与 npm 一起使用!
如果你想在本地开发之外使用 WebAssembly,你可以使用 pack
和 publish
命令在你的 hello-wasm
目录中发布包
wasm-pack pack
npm notice
npm notice 📦 hello-wasm@0.1.0
npm notice Tarball Contents
npm notice 2.9kB hello_wasm_bg.js
npm notice 16.7kB hello_wasm_bg.wasm
npm notice 85B hello_wasm.d.ts
npm notice 182B hello_wasm.js
npm notice 549B 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 工作组。