从 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,并生成适合在浏览器中使用的正确打包文件。要下载并安装它,请在您的终端中输入以下命令

bash
cargo install wasm-pack

构建我们的 WebAssembly 包

设置就绪,让我们在 Rust 中创建一个新包。导航到您存放个人项目的目录,然后键入以下内容

bash
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 代码

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

我们不会使用此测试代码,因此请将其删除。

让我们编写一些 Rust 代码

让我们将此代码放入 src/lib.rs

rust
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 之间进行通信

第一部分如下所示

rust
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 中的外部函数

下一部分如下所示

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

#[ ] 内部的部分被称为“属性”,它以某种方式修改下一条语句。在本例中,该语句是一个 extern,它告诉 Rust 我们想要调用一些外部定义的函数。该属性表示“wasm-bindgen 知道如何找到这些函数”。

第三行是函数签名,用 Rust 编写。它表示“alert 函数接受一个参数,一个名为 s 的字符串”。

正如您可能猜到的那样,这是 JavaScript 提供的 alert 函数。我们在下一节中调用此函数。

每当您想要调用 JavaScript 函数时,都可以将其添加到此文件中,wasm-bindgen 会为您完成所有设置工作。并非所有内容都得到支持,但我们正在努力。如果缺少某些内容,请 提交错误报告

生成 JavaScript 可以调用的 Rust 函数

最后一部分是

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 对其进行配置。打开此文件,并将它的内容更改为如下所示

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 版本;我们不会在本教程中详细介绍它的含义。有关更多信息,请参阅 CargoRust 链接 文档。

最后一部分是 [dependencies] 部分。在这里,我们告诉 Cargo 我们希望依赖 wasm-bindgen 的哪个版本;在本例中,它是任何 0.2.z 版本(但不包括 0.3.0 或更高版本)。

构建该包

现在我们已经完成了所有设置,让我们构建该包。我们将在本机 ES 模块和 Node.js 中使用生成的代码。为此,我们将使用 wasm-pack build 中的 --target 参数 指定要生成的 WebAssembly 和 JavaScript 类型。

首先,运行以下命令

bash
wasm-pack build --target web

这会执行许多操作(它们需要花费大量时间,尤其是在您第一次运行 wasm-pack 时)。要详细了解这些操作,请查看 这篇关于 Mozilla Hacks 的博文。简而言之,wasm-pack build

  1. 将您的 Rust 代码编译成 WebAssembly。
  2. 在该 WebAssembly 上运行 wasm-bindgen,生成一个 JavaScript 文件,将该 WebAssembly 文件包装成浏览器可以理解的模块。
  3. 创建一个 pkg 目录,并将该 JavaScript 文件和您的 WebAssembly 代码移动到该目录中。
  4. 读取您的 Cargo.toml 并生成一个等效的 package.json
  5. 复制您的 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 文件中

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 开始

bash
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,但从本地路径安装对于此演示的目的来说很方便

bash
cd ..
mkdir site && cd site
npm i ../pkg

安装 webpack 开发依赖项

bash
npm i -D webpack@5 webpack-cli@5 webpack-dev-server@4 copy-webpack-plugin@11

接下来,我们需要配置 Webpack。创建 webpack.config.js 并将以下内容放入其中

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 中,您可以添加 buildserve 脚本,这些脚本将使用我们刚刚创建的配置文件运行 webpack

json
{
  "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 的文件,并赋予它以下内容

js
import * as wasm from "hello-wasm";

wasm.greet("WebAssembly with npm");

这从 node_modules 文件夹导入模块并调用 greet 函数,将 "WebAssembly with npm" 作为字符串传递。请注意,这里没有什么特别之处,但我们正在调用 Rust 代码。就 JavaScript 代码而言,这只是一个普通模块。

最后,我们需要添加一个 HTML 文件来加载 JavaScript。创建一个 index.html 文件并添加以下内容

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

我们已经完成了文件制作。让我们试试看

bash
npm run serve

这将启动一个 Web 服务器并打开 https://127.0.0.1:8080。您应该看到屏幕上出现一个警报框,其中包含 Hello, WebAssembly with npm!。我们已成功地将 Rust 模块与 npm 一起使用!

如果您想在本地开发之外使用 WebAssembly,可以使用 packpublish 命令发布包

bash
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

bash
wasm-pack publish

结论

这就是我们教程的结尾;我们希望您发现它有用。

在这个领域有很多令人兴奋的工作正在进行;如果您想帮助使其变得更好,请查看 Rust 和 WebAssembly 工作组