在 WASM WASI 上運行 Rust 的九條規(guī)則,你知道哪條?
在受限環(huán)境中運行 Rust 會帶來挑戰(zhàn)。你的代碼可能無法訪問完整的操作系統(tǒng),例如 Linux、Windows 或 macOS。你可能對文件、網(wǎng)絡、時間、隨機數(shù)甚至內(nèi)存的訪問權限有限(或根本沒有)。我們將探索解決方法和解決方案。
本文的第一部分重點介紹在 “WASM WASI” 上運行代碼,這是一種類似容器的環(huán)境。我們將看到 WASM WASI 本身可能(也可能不)有用。但是,它作為在瀏覽器或嵌入式系統(tǒng)中運行 Rust 的第一步很有價值。
將代碼移植到 WASM WASI 上需要許多步驟和選擇。瀏覽這些選擇可能很耗時。錯過一步會導致失敗。我們將通過提供九條規(guī)則來減少這種復雜性,我們將在后面詳細探討:
規(guī)則 1:準備好失望:WASM WASI 很容易,但 - 現(xiàn)在 - 基本上沒用 - 除了作為墊腳石。
2019 年,Docker 聯(lián)合創(chuàng)始人 Solomon Hykes 發(fā)布了一條推文[1]:
如果 WASM+WASI 在 2008 年就存在,我們就無需創(chuàng)建 Docker。這就是它如此重要的原因。服務器上的 Webassembly 是計算的未來。標準化的系統(tǒng)接口是缺失的一環(huán)。讓我們希望 WASI 能勝任這項任務。
如今,如果你關注科技新聞,你就會看到像這樣的樂觀標題:
如果 WASM WASI 真正準備就緒并有用,每個人都會使用它。我們不斷看到這些標題的事實表明它還沒有準備好。換句話說,如果 WASM WASI 真的準備好了,他們就不需要不斷強調它已經(jīng)準備好了。
截至 WASI 預覽版 1,現(xiàn)狀如下:你可以訪問一些文件操作、環(huán)境變量,并可以訪問時間和隨機數(shù)生成。但是,不支持網(wǎng)絡功能。
WASM WASI 可能 對某些 AWS Lambda 風格的 Web 服務有用,但即使那也還不確定。因為,與 WASM WASI 相比,你難道不更愿意將你的 Rust 代碼本地編譯并以一半的成本運行兩倍的速度嗎?
也許 WASM WASI 對插件和擴展有用。在基因組學領域,我有一個用于 Python 的 Rust 擴展,我為 25 種不同的組合編譯它(5 個版本的 Python 跨 5 個操作系統(tǒng)目標)。即使這樣,我也沒有涵蓋所有可能的操作系統(tǒng)和芯片系列。我能用 WASM WASI 替換這些操作系統(tǒng)目標嗎?不能,它會太慢。我能將 WASM WASI 作為一個第六個“萬能”目標添加進去嗎?也許可以,但如果我真的需要可移植性,我已經(jīng)被要求支持 Python,應該直接使用 Python。
那么,WASM WASI 到底有什么用?目前,它的主要價值在于它是將代碼運行在瀏覽器或嵌入式系統(tǒng)中的第一步。
規(guī)則 2:了解 Rust 目標。
在規(guī)則 1 中,我順便提到了“操作系統(tǒng)目標”。讓我們更深入地了解 Rust 目標 - 這不僅對于 WASM WASI 來說是必要的信息,而且對于一般的 Rust 開發(fā)也是如此。
在我的 Windows 機器上,我可以編譯一個 Rust 項目以在 Linux 或 macOS 上運行。類似地,從 Linux 機器上,我可以編譯一個 Rust 項目以針對 Windows 或 macOS。以下是我用于將 Linux 目標添加到 Windows 機器并檢查它的命令:
rustup target add x86_64-unknown-linux-gnu
cargo check --target x86_64-unknown-linux-gnu
旁白:雖然 cargo check 驗證代碼是否可以編譯,但構建一個功能齊全的可執(zhí)行文件需要額外的工具。要從 Windows 交叉編譯到 Linux (GNU),你還需要安裝 Linux GNU C/C++ 編譯器和相應的工具鏈。這可能很棘手。幸運的是,對于我們關心的 WASM 目標,所需的工具鏈很容易安裝。
要查看 Rust 支持的所有目標,請使用以下命令:
rustc --print target-list
它將列出超過 200 個目標,包括 x86_64-unknown-linux-gnu、wasm32-wasip1 和 wasm32-unknown-unknown。
目標名稱包含最多四個部分:CPU 系列、供應商、操作系統(tǒng)和環(huán)境(例如,GNU 與 LVMM):
目標名稱部分 - 來自作者的圖片
現(xiàn)在我們對目標有所了解,讓我們繼續(xù)安裝我們需要的 WASM WASI 目標。
規(guī)則 3:安裝 wasm32-wasip1 目標和 WASMTIME,然后創(chuàng)建“Hello, WebAssembly!”。
要將我們的 Rust 代碼在瀏覽器之外的 WASM 上運行,我們需要將目標設置為 wasm32-wasip1(使用 WASI 預覽版 1 的 32 位 WebAssembly)。我們還將安裝 WASMTIME,這是一個允許我們在瀏覽器之外使用 WASI 運行 WebAssembly 模塊的運行時。
rustup target add wasm32-wasip1
cargo install wasmtime-cli
為了測試我們的設置,讓我們使用 cargo new 創(chuàng)建一個新的“Hello, WebAssembly!” Rust 項目。這將初始化一個新的 Rust 包:
cargo new hello_wasi
cd hello_wasi
編輯 src/main.rs 使其內(nèi)容如下:
fn main() {
#[cfg(not(target_arch = "wasm32"))]
println!("Hello, world!");
#[cfg(target_arch = "wasm32")]
println!("Hello, WebAssembly!");
}
旁白:我們將在規(guī)則 4 中更深入地了解 #[cfg(...)] 屬性,該屬性允許條件編譯。
現(xiàn)在,使用 cargo run 運行項目,你應該看到 Hello, world! 打印到控制臺上。
接下來,創(chuàng)建一個 .cargo/config.toml 文件,該文件指定 Rust 在針對 WASM WASI 時應該如何運行和測試項目。
[target.wasm32-wasip1]
runner = "wasmtime run --dir ."
旁白:這個 .cargo/config.toml 文件與主 Cargo.toml 文件不同,后者定義了你的項目的依賴項和元數(shù)據(jù)。
現(xiàn)在,如果你輸入:
cargo run --target wasm32-wasip1
你應該看到 Hello, WebAssembly!。恭喜!你剛剛成功地在類似容器的 WASM WASI 環(huán)境中運行了一些 Rust 代碼。
規(guī)則 4:了解條件編譯。
現(xiàn)在,讓我們研究一下 #[cfg(...)] - 這是在 Rust 中條件編譯代碼的重要工具。在規(guī)則 3 中,我們看到了:
fn main() {
#[cfg(not(target_arch = "wasm32"))]
println!("Hello, world!");
#[cfg(target_arch = "wasm32")]
println!("Hello, WebAssembly!");
}
#[cfg(...)] 行告訴 Rust 編譯器根據(jù)特定條件包含或排除某些代碼項。一個“代碼項”指的是代碼單元,例如函數(shù)、語句或表達式。
使用 #[cfg(…)] 行,你可以條件編譯你的代碼。換句話說,你可以為不同的情況創(chuàng)建代碼的不同版本。例如,在為 wasm32 目標編譯時,編譯器會忽略 #[cfg(not(target_arch = "wasm32"))] 塊,只包含以下內(nèi)容:
fn main() {
println!("Hello, WebAssembly!");
}
你通過表達式指定條件,例如 target_arch = "wasm32"。支持的鍵包括 target_os 和 target_arch。有關支持的鍵的完整列表,請參閱 Rust 參考手冊 完整列表[2]。你還可以使用 Cargo 功能創(chuàng)建表達式,我們將在規(guī)則 6 中學習。
你可以使用邏輯運算符 not、any 和 all 來組合表達式。Rust 的條件編譯不使用傳統(tǒng)的 if...then...else 語句。相反,你必須使用 #[cfg(...)] 及其否定來處理不同的情況:
#[cfg(not(target_arch = "wasm32"))]
...
#[cfg(target_arch = "wasm32")]
...
要條件編譯整個文件,請將 #![cfg(...)] 放置在文件的頂部。(注意“!”)。當一個文件只與特定目標或配置相關時,這很有用。
你也可以在 Cargo.toml 中使用 cfg 表達式來條件包含依賴項。這允許你根據(jù)不同的目標定制依賴項。例如,這表示“當不針對 wasm32 時,依賴于具有 Rayon 的 Criterion”。
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
criterion = { version = "0.5.1", features = ["rayon"] }
規(guī)則 5:運行常規(guī)測試,但使用 WASM WASI 目標。
現(xiàn)在,讓我們嘗試在 WASM WASI 上運行 你的 項目。如規(guī)則 3 中所述,為你的項目創(chuàng)建一個 .cargo/config.toml 文件。它告訴 Cargo 如何在 WASM WASI 上運行和測試你的項目。
[target.wasm32-wasip1]
runner = "wasmtime run --dir ."
接下來,你的項目 - 就像所有好的代碼一樣 - 應該已經(jīng)包含測試[3]。我的 range-set-blaze 項目包含以下示例測試:
#[test]
fn insert_255u8() {
let range_set_blaze = RangeSetBlaze::<u8>::from_iter([255]);
assert!(range_set_blaze.to_string() == "255..=255");
}
現(xiàn)在,讓我們嘗試在 WASM WASI 上運行你的項目的測試。使用以下命令:
cargo test --target wasm32-wasip1
如果這能正常工作,你可能就完成了 - 但它可能不會正常工作。當我在 range-set-blaze 上嘗試這個命令時,我得到了一條錯誤消息,抱怨在 WASM 上使用 Rayon。
error: Rayon cannot be used when targeting wasi32. Try disabling default features.
--> C:\Users\carlk\.cargo\registry\src\index.crates.io-6f17d22bba15001f\criterion-0.5.1\src\lib.rs:31:1
|
31 | compile_error!("Rayon cannot be used when targeting wasi32. Try disabling default features.");
要修復此錯誤,我們首先需要了解 Cargo 功能。
規(guī)則 6:了解 Cargo 功能。
為了解決像規(guī)則 5 中的 Rayon 錯誤這樣的問題,了解 Cargo 功能如何工作非常重要。
在 Cargo.toml 中,一個可選的 [features] 部分允許你根據(jù)啟用的功能或禁用的功能來定義項目的不同配置或版本。例如,以下是 Criterion 基準測試項目 的 Cargo.toml 文件的簡化部分:
[features]
default = ["rayon", "plotters", "cargo_bench_support"]
rayon = ["dep:rayon"]
plotters = ["dep:plotters"]
html_reports = []
cargo_bench_support = []
[dependencies]
#...
# 可選依賴項
rayon = { version = "1.3", optional = true }
plotters = { version = "^0.3.1", optional = true, default-features = false, features = [
"svg_backend",
"area_series",
"line_series",
] }
這定義了四個 Cargo 功能:rayon、plotters、html_reports 和 cargo_bench_support。由于每個功能都可以包含或排除,因此這四個功能創(chuàng)建了項目的 16 種可能的配置。還要注意特殊的默認 Cargo 功能。
一個 Cargo 功能可以包含其他 Cargo 功能。在上面的示例中,特殊的 default Cargo 功能包含了另外三個 Cargo 功能 - rayon、plotters 和 cargo_bench_support。
一個 Cargo 功能可以包含一個依賴項。上面的 rayon Cargo 功能包含 rayon 箱子作為依賴包。
此外,依賴包可能擁有自己的 Cargo 功能。例如,上面的 plotters Cargo 功能包含 plotters 依賴包,并啟用了以下 Cargo 功能:svg_backend、area_series 和 line_series。
你可以在運行 cargo check、cargo build、cargo run 或 cargo test 時指定要啟用或禁用的 Cargo 功能。例如,如果你正在使用 Criterion 項目并只想檢查 html_reports 功能,而不使用任何默認功能,你可以運行:
cargo check --no-default-features --features html_reports
此命令告訴 Cargo 不要默認包含任何 Cargo 功能,而是專門啟用 html_reports Cargo 功能。
在你的 Rust 代碼中,你可以根據(jù)啟用的 Cargo 功能包含/排除代碼項。語法使用 #cfg(…),如規(guī)則 4 所示:
#[cfg(feature = "html_reports")]
SOME_CODE_ITEM
了解了 Cargo 功能之后,我們現(xiàn)在可以嘗試修復在 WASM WASI 上運行測試時遇到的 Rayon 錯誤。
規(guī)則 7:更改你能更改的東西:通過選擇 Cargo 功能解決依賴問題,64 位/32 位問題。
當我們嘗試運行 cargo test --target wasm32-wasip1 時,錯誤消息的一部分指出:Criterion ... Rayon cannot be used when targeting wasi32. Try disabling default features. 這表明我們應該在針對 WASM WASI 時禁用 Criterion 的 rayon Cargo 功能。
為此,我們需要在 Cargo.toml 中進行兩個更改。首先,我們需要在 [dev-dependencies] 部分禁用 Criterion 的 rayon 功能。因此,這個起始配置:
[dev-dependencies]
criterion = { version = "0.5.1", features = ["html_reports"] }
變成了這個,我們顯式地關閉 Criterion 的默認功能,然后啟用除 rayon 之外的所有 Cargo 功能。
[dev-dependencies]
criterion = { version = "0.5.1", features = [
"html_reports",
"plotters",
"cargo_bench_support"
],
default-features = false }
接下來,為了確保 rayon 仍然用于非 WASM 目標,我們在 Cargo.toml 中添加了一個條件依賴項,如下所示:
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
criterion = { version = "0.5.1", features = ["rayon"] }
一般來說,在針對 WASM WASI 時,你可能需要修改你的依賴項及其 Cargo 功能以確保兼容性。有時這個過程很簡單,但有時它可能很困難 - 甚至不可能,正如我們將在規(guī)則 8 中討論的那樣。
旁白:在本系列的下一篇文章中 - 關于瀏覽器中的 WASM - 我們將更深入地探討修復依賴項的策略。
再次運行測試后,我們越過了之前的錯誤,卻遇到了一個新的錯誤,這是一種進步!
#[test]
fn test_demo_i32_len() {
assert_eq!(demo_i32_len(i32::MIN..=i32::MAX), u32::MAX as usize + 1);
^^^^^^^^^^^^^^^^^^^^^ attempt to compute
`usize::MAX + 1_usize`, which would overflow
}
編譯器抱怨 u32::MAX as usize + 1 溢出了。在 64 位 Windows 上,該表達式不會溢出,因為 usize 與 u64 相同,并且可以容納 u32::MAX as usize + 1。但是,WASM 是一個 32 位環(huán)境,因此 usize 與 u32 相同,該表達式大了一個。
這里的解決方法是用 u64 替換 usize,確保表達式不會溢出。更一般地說,編譯器不會總是捕獲這些問題,因此審查你對 usize 和 isize 的使用非常重要。如果你指的是 Rust 數(shù)據(jù)結構的大小或索引,usize 是正確的。但是,如果你處理的值超過了 32 位限制,你應該使用 u64 或 i64。
“
旁白:在 32 位環(huán)境中,Rust 數(shù)組、Vec、BTreeSet 等只能容納最多 232?1=4,294,967,295 個元素。
因此,我們已經(jīng)解決了依賴問題并解決了 usize 溢出問題。但是,我們能修復所有問題嗎?不幸的是,答案是否定的。
規(guī)則 8:接受你無法更改所有東西:網(wǎng)絡、Tokio、Rayon 等。
WASM WASI 預覽版 1(當前版本)支持文件訪問(在指定目錄內(nèi))、讀取環(huán)境變量以及處理時間和隨機數(shù)。但是,與你可能從完整操作系統(tǒng)中期望的功能相比,它的功能有限。
如果你的項目需要訪問網(wǎng)絡、使用 Tokio 進行異步任務或使用 Rayon 進行多線程,不幸的是,這些功能在預覽版 1 中不受支持。
幸運的是,WASM WASI 預覽版 2 預計將改進這些限制,提供更多功能,包括對網(wǎng)絡和可能異步任務的更好支持。
規(guī)則 9:將 WASM WASI 添加到你的 CI(持續(xù)集成)測試中。
因此,你的測試在 WASM WASI 上通過了,你的項目也成功運行了。你完成了?還沒有。因為,正如我喜歡說的:
“
如果不在 CI 中,它就不存在。
持續(xù)集成 (CI) 是一個系統(tǒng),它可以在你每次更新代碼時自動運行你的測試,確保你的代碼能夠繼續(xù)按預期工作。通過將 WASM WASI 添加到你的 CI 中,你可以保證未來的更改不會破壞你的項目與 WASM WASI 目標的兼容性。
在我的情況下,我的項目托管在 GitHub 上,我使用 GitHub Actions 作為我的 CI 系統(tǒng)。以下是我添加到 .github/workflows/ci.yml 中的配置,用于在我的項目上測試 WASM WASI:
test_wasip1:
name: Test WASI P1
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
targets: wasm32-wasip1
- name: Install Wasmtime
run: |
curl https://wasmtime.dev/install.sh -sSf | bash
echo "${HOME}/.wasmtime/bin" >> $GITHUB_PATH
- name: Run WASI tests
run: cargo test --verbose --target wasm32-wasip1
通過將 WASM WASI 集成到 CI 中,我可以放心地向我的項目添加新代碼。CI 將自動測試所有代碼在未來繼續(xù)支持 WASM WASI。
因此,這就是將你的 Rust 代碼移植到 WASM WASI 的九條規(guī)則。以下是我對移植到 WASM WASI 的感受:
不好之處:
- 在 WASM WASI 上運行在今天幾乎沒有實用價值。但是,它有潛力在明天變得有用。
- 在 Rust 中,有一句常見的說法:“如果它可以編譯,它就可以工作。”不幸的是,這并不總是適用于 WASM WASI。如果你使用了不支持的功能,比如網(wǎng)絡功能,編譯器將不會捕獲錯誤。相反,它將在運行時失敗。例如,這段代碼可以在 WASM WASI 上編譯和運行,但始終返回錯誤,因為不支持網(wǎng)絡功能。
use std::net::TcpStream;
fn main() {
match TcpStream::connect("crates.io:80") {
Ok(_) => println!("Successfully connected."),
Err(e) => println!("Failed to connect: {e}"),
}
}
好之處:
- 在 WASM WASI 上運行是將代碼運行在瀏覽器和嵌入式系統(tǒng)中的一個很好的第一步。
- 你可以在 WASM WASI 上運行 Rust 代碼,而無需移植到 no_std。(移植到 no_std 是本系列文章的第三部分的主題。)
- 你可以在 WASM WASI 上運行標準的 Rust 測試,這使得驗證你的代碼變得很容易。
- .cargo/config.toml 文件和 Rust 的 --target 選項使得在不同的目標上配置和運行你的代碼變得非常簡單 - 包括 WASM WASI。
參考資料
[1] 發(fā)布了一條推文: https://x.com/solomonstre/status/1111004913222324225
[2] 完整列表: https://doc.rust-lang.org/reference/conditional-compilation.html#set-configuration-options
[3] 你的項目 - 就像所有好的代碼一樣 - 應該已經(jīng)包含測試: https://doc.rust-lang.org/rust-by-example/testing.html