從 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,並生成適合在瀏覽器中使用的正確打包檔案。要下載並安裝它,請在您的終端中輸入以下命令
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 的工作方式類似於它們兩者。
接下來,Cargo 為我們在 src/lib.rs 中生成了一些 Rust 程式碼
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
我們不會使用此測試程式碼,因此請將其刪除。
讓我們編寫一些 Rust 程式碼
讓我們將此程式碼放入 src/lib.rs 中
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 之間進行通訊
第一部分如下所示
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 中的外部函式
下一部分如下所示
#[wasm_bindgen]
extern {
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!”的警報框。
現在我們的庫已編寫完畢,讓我們構建它。
將我們的程式碼編譯成 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 = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
填寫您自己的儲存庫,並使用 git 用於 authors 欄位的相同資訊。
需要新增的主要部分是 [package]。[lib] 部分告訴 Rust 構建我們包的 cdylib 版本;我們不會在本教程中詳細介紹它的含義。有關更多資訊,請參閱 Cargo 和 Rust 連結 文件。
最後一部分是 [dependencies] 部分。在這裡,我們告訴 Cargo 我們希望依賴 wasm-bindgen 的哪個版本;在本例中,它是任何 0.2.z 版本(但不包括 0.3.0 或更高版本)。
構建該包
現在我們已經完成了所有設定,讓我們構建該包。我們將在本機 ES 模組和 Node.js 中使用生成的程式碼。為此,我們將使用 wasm-pack build 中的 --target 引數 指定要生成的 WebAssembly 和 JavaScript 型別。
首先,執行以下命令
wasm-pack build --target web
這會執行許多操作(它們需要花費大量時間,尤其是在您第一次執行 wasm-pack 時)。要詳細瞭解這些操作,請檢視 這篇關於 Mozilla Hacks 的博文。簡而言之,wasm-pack build 會
- 將您的 Rust 程式碼編譯成 WebAssembly。
- 在該 WebAssembly 上執行
wasm-bindgen,生成一個 JavaScript 檔案,將該 WebAssembly 檔案包裝成瀏覽器可以理解的模組。 - 建立一個
pkg目錄,並將該 JavaScript 檔案和您的 WebAssembly 程式碼移動到該目錄中。 - 讀取您的
Cargo.toml並生成一個等效的package.json。 - 複製您的
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 檔案中
<!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 開始
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,但從本地路徑安裝對於此演示的目的來說很方便
cd ..
mkdir site && cd site
npm i ../pkg
安裝 webpack 開發依賴項
npm i -D webpack@5 webpack-cli@5 webpack-dev-server@4 copy-webpack-plugin@11
接下來,我們需要配置 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": "^11.0.0",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
}
}
接下來,建立一個名為 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 目錄應如下所示
├── index.html ├── index.js ├── node_modules ├── package-lock.json ├── package.json └── webpack.config.js
我們已經完成了檔案製作。讓我們試試看
npm run serve
這將啟動一個 Web 伺服器並開啟 https://:8080。您應該看到螢幕上出現一個警報框,其中包含 Hello, WebAssembly with npm!。我們已成功地將 Rust 模組與 npm 一起使用!
如果您想在本地開發之外使用 WebAssembly,可以使用 pack 和 publish 命令釋出包
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
wasm-pack publish
結論
這就是我們教程的結尾;我們希望您發現它有用。
在這個領域有很多令人興奮的工作正在進行;如果您想幫助使其變得更好,請檢視 Rust 和 WebAssembly 工作組。