从 Rust 编译到 WebAssembly

如果你有一些 Rust 代码,你可以将其编译成 WebAssembly (Wasm)。本教程将向你展示如何将 Rust 项目编译成 WebAssembly 并在现有 Web 应用程序中使用它。

Rust 和 WebAssembly 的用例

Rust 和 WebAssembly 主要有两个用例

  • 构建整个应用程序 — 整个基于 Rust 的 Web 应用程序。
  • 构建应用程序的一部分 — 在现有 JavaScript 前端中使用 Rust。

目前,Rust 团队专注于后者,因此我们在此处介绍这一点。对于前者,请查看 yewleptos 等项目。

在本教程中,我们使用 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 并生成用于浏览器中使用的正确打包。要下载并安装它,请在终端中输入以下命令

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 还在 src/lib.rs 中为我们生成了一些 Rust 代码

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 代码;将其替换为以下内容

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

第一部分如下所示

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

下一部分如下所示

rust
#[wasm_bindgen]
extern "C" {
    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!”的 alert 框。

现在我们的库已经编写完成,让我们构建它。

将我们的代码编译为 WebAssembly

为了正确编译我们的代码,我们首先使用 Cargo.toml 对其进行配置。打开此文件,并将其内容更改为如下所示

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 版本;我们不会在本教程中深入探讨这意味着什么。有关更多信息,请查阅 CargoRust Linkage 文档。

最后一部分是 [dependencies] 部分。我们在此处告诉 Cargo 我们想要依赖哪个版本的 wasm-bindgen;在这种情况下,它是任何 0.2.z 版本(但不包括 0.3.0 或更高版本)。

构建包

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

首先,在你的 hello-wasm 目录中运行以下命令

bash
wasm-pack build --target web

这做了几件事。要详细了解它们,请查看 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 目录中有一个包。

在 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 文件中

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

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

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

安装 webpack 开发依赖项

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

接下来,我们需要配置 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": "^12.0.2",
    "webpack": "^5.97.1",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^5.1.0"
  }
}

接下来,创建一个名为 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 目录应如下所示

├── node_modules
├── index.html
├── index.js
├── package-lock.json
├── package.json
└── webpack.config.js

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

bash
npm run serve

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

如果你想在本地开发之外使用 WebAssembly,你可以使用 packpublish 命令在你的 hello-wasm 目录中发布包

bash
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

bash
wasm-pack publish

总结

这是我们教程的结尾;希望你觉得它有用。

这个领域正在进行许多激动人心的工作;如果你想帮助它变得更好,请查看 Rust 和 WebAssembly 工作组