從 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 工作組