從 Rust 編譯到 WebAssembly
如果你有一些 Rust 程式碼,你可以將其編譯成 WebAssembly (Wasm)。本教程將向你展示如何將 Rust 專案編譯成 WebAssembly 並在現有 Web 應用程式中使用它。
Rust 和 WebAssembly 的用例
Rust 和 WebAssembly 主要有兩個用例
- 構建整個應用程式 — 整個基於 Rust 的 Web 應用程式。
- 構建應用程式的一部分 — 在現有 JavaScript 前端中使用 Rust。
目前,Rust 團隊專注於後者,因此我們在此處介紹這一點。對於前者,請檢視 yew 和 leptos 等專案。
在本教程中,我們使用 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 並生成用於瀏覽器中使用的正確打包。要下載並安裝它,請在終端中輸入以下命令
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 還在 src/lib.rs 中為我們生成了一些 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 程式碼;將其替換為以下內容
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 之間通訊
第一部分如下所示
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 中的外部函式
下一部分如下所示
#[wasm_bindgen]
extern "C" {
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!”的 alert 框。
現在我們的庫已經編寫完成,讓我們構建它。
將我們的程式碼編譯為 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 = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
填寫你自己的倉庫,並使用 git 用於 authors 欄位的相同資訊。
要新增的重要部分是 [package]。[lib] 部分告訴 Rust 構建我們包的 cdylib 版本;我們不會在本教程中深入探討這意味著什麼。有關更多資訊,請查閱 Cargo 和 Rust Linkage 文件。
最後一部分是 [dependencies] 部分。我們在此處告訴 Cargo 我們想要依賴哪個版本的 wasm-bindgen;在這種情況下,它是任何 0.2.z 版本(但不包括 0.3.0 或更高版本)。
構建包
現在我們已經完成了設定,讓我們構建包。我們將在原生 ES 模組和 Node.js 中使用生成的程式碼。為此,我們將在 wasm-pack build 中使用 --target 引數 來指定生成的 WebAssembly 和 JavaScript 的型別。
首先,在你的 hello-wasm 目錄中執行以下命令
wasm-pack build --target web
這做了幾件事。要詳細瞭解它們,請檢視 Mozilla Hacks 上的這篇部落格文章。簡而言之,wasm-pack build
- 將你的 Rust 程式碼編譯為 WebAssembly。
- 對該 WebAssembly 執行
wasm-bindgen,生成一個 JavaScript 檔案,該檔案將該 WebAssembly 檔案包裝成瀏覽器可以理解的模組。 - 建立
pkg目錄並將該 JavaScript 檔案和你的 WebAssembly 程式碼移入其中。 - 讀取你的
Cargo.toml並生成一個等效的package.json。 - 將你的
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 檔案中
<!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
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,但從本地路徑安裝對於此演示來說很方便
mkdir site && cd site
npm i ../pkg
安裝 webpack 開發依賴項
npm i -D webpack@5 webpack-cli@5 webpack-dev-server@5 copy-webpack-plugin@12
接下來,我們需要配置 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": "^12.0.2",
"webpack": "^5.97.1",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.1.0"
}
}
接下來,建立一個名為 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 目錄應如下所示
├── node_modules ├── index.html ├── index.js ├── package-lock.json ├── package.json └── webpack.config.js
我們已經完成了檔案的製作。讓我們試一試
npm run serve
這將啟動一個 Web 伺服器並開啟 https://:8080。你會在螢幕上看到一個警報框,其中包含文字 Hello, WebAssembly with npm!。我們已成功將 Rust 模組與 npm 一起使用!
如果你想在本地開發之外使用 WebAssembly,你可以使用 pack 和 publish 命令在你的 hello-wasm 目錄中釋出包
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
wasm-pack publish
總結
這是我們教程的結尾;希望你覺得它有用。
這個領域正在進行許多激動人心的工作;如果你想幫助它變得更好,請檢視 Rust 和 WebAssembly 工作組。