從 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 <you@example.com>"]
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://: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://:8080。您應該看到螢幕上出現一個警報框,其中包含 Hello, WebAssembly with npm!。我們已成功地將 Rust 模組與 npm 一起使用!

如果您想在本地開發之外使用 WebAssembly,可以使用 packpublish 命令釋出包

bash
wasm-pack pack
npm notice
npm notice 📦  hello-wasm@0.1.0
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 工作組