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