當前位置: 妍妍網 > 碼農

在 WASM WASI 上執行 Rust 的九條規則

2024-09-30碼農

在受限環境中執行 Rust 會帶來挑戰。你的程式碼可能無法存取完整的作業系統,例如 Linux、Windows 或 macOS。你可能對檔、網路、時間、隨機數甚至記憶體的存取許可權有限(或根本沒有)。我們將探索解決方法和解決方案。

本文的第一部份重點介紹在 「WASM WASI」 上執行程式碼,這是一種類似容器的環境。我們將看到 WASM WASI 本身可能(也可能不)有用。但是,它作為在瀏覽器或嵌入式系統中執行 Rust 的第一步很有價值。

將程式碼移植到 WASM WASI 上需要許多步驟和選擇。瀏覽這些選擇可能很耗時。錯過一步會導致失敗。我們將透過提供九條規則來減少這種復雜性,我們將在後面詳細探討:

規則 1:準備好失望:WASM WASI 很容易,但 - 現在 - 基本上沒用 - 除了作為墊腳石。

2019 年,Docker 聯合創始人 Solomon Hykes 釋出了一條推文 [1]

如果 WASM+WASI 在 2008 年就存在,我們就無需建立 Docker。這就是它如此重要的原因。伺服器上的 Webassembly 是計算的未來。標準化的系統介面是缺失的一環。讓我們希望 WASI 能勝任這項任務。

如今,如果你關註科技新聞,你就會看到像這樣的樂觀標題:

如果 WASM WASI 真正準備就緒並有用,每個人都會使用它。我們不斷看到這些標題的事實表明它還沒有準備好。換句話說,如果 WASM WASI 真的準備好了,他們就不需要不斷強調它已經準備好了。

截至 WASI 預覽版 1,現狀如下:你可以存取一些檔操作、環境變量,並可以存取時間和隨機數生成。但是,不支持網路功能。

WASM WASI 可能 對某些 AWS Lambda 風格的 Web 服務有用,但即使那也還不確定。因為,與 WASM WASI 相比,你難道不更願意將你的 Rust 程式碼本地編譯並以一半的成本執行兩倍的速度嗎?

也許 WASM WASI 對外掛程式和擴充套件有用。在基因組學領域,我有一個用於 Python 的 Rust 擴充套件,我為 25 種不同的組合編譯它(5 個版本的 Python 跨 5 個作業系統目標)。即使這樣,我也沒有涵蓋所有可能的作業系統和芯片系列。我能用 WASM WASI 替換這些作業系統目標嗎?不能,它會太慢。我能將 WASM WASI 作為一個第六個「萬能」目標添加進去嗎?也授權以,但如果我真的需要可移植性,我已經被要求支持 Python,應該直接使用 Python。

那麽,WASM WASI 到底有什麽用?目前,它的主要價值在於它是將程式碼執行在瀏覽器或嵌入式系統中的第一步。

規則 2:了解 Rust 目標。

在規則 1 中,我順便提到了「作業系統目標」。讓我們更深入地了解 Rust 目標 - 這不僅對於 WASM WASI 來說是必要的資訊,而且對於一般的 Rust 開發也是如此。

在我的 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 驗證程式碼是否可以編譯,但構建一個功能齊全的可執行檔需要額外的工具。要從 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 系列、供應商、作業系統和環境(例如,GNU 與 LVMM):

目標名稱部份 - 來自作者的圖片

現在我們對目標有所了解,讓我們繼續安裝我們需要的 WASM WASI 目標。

規則 3:安裝 wasm32-wasip1 目標和 WASMTIME,然後建立「Hello, WebAssembly!」。

要將我們的 Rust 程式碼在瀏覽器之外的 WASM 上執行,我們需要將目標設定為 wasm32-wasip1 (使用 WASI 預覽版 1 的 32 位 WebAssembly)。我們還將安裝 WASMTIME,這是一個允許我們在瀏覽器之外使用 WASI 執行 WebAssembly 模組的執行時。

rustup target add wasm32-wasip1
cargo install wasmtime-cli

為了測試我們的設定,讓我們使用 cargo new 建立一個新的「Hello, WebAssembly!」 Rust 計畫。這將初始化一個新的 Rust 包:

cargo new hello_wasi
cd hello_wasi

編輯 src/main.rs 使其內容如下:

fnmain() {
#[cfg(not(target_arch = "wasm32"))]
println!("Hello, world!");
#[cfg(target_arch = "wasm32")]
println!("Hello, WebAssembly!");
}

旁白:我們將在規則 4 中更深入地了解 #[cfg(...)] 內容,該內容允許條件編譯。

現在,使用 cargo run 執行計畫,你應該看到 Hello, world! 打印到控制台上。

接下來,建立一個 .cargo/config.toml 檔,該檔指定 Rust 在針對 WASM WASI 時應該如何執行和測試計畫。

[target.wasm32-wasip1]
runner = "wasmtime run --dir ."

旁白:這個 .cargo/config.toml 檔與主 Cargo.toml 檔不同,後者定義了你的計畫的依賴項和後設資料。

現在,如果你輸入:

cargo run --target wasm32-wasip1

你應該看到 Hello, WebAssembly! 。恭喜!你剛剛成功地在類似容器的 WASM WASI 環境中執行了一些 Rust 程式碼。

規則 4:了解條件編譯。

現在,讓我們研究一下 #[cfg(...)] - 這是在 Rust 中條件編譯程式碼的重要工具。在規則 3 中,我們看到了:

fnmain() {
#[cfg(not(target_arch = "wasm32"))]
println!("Hello, world!");
#[cfg(target_arch = "wasm32")]
println!("Hello, WebAssembly!");
}

#[cfg(...)] 行告訴 Rust 編譯器根據特定條件包含或排除某些程式碼項。一個「程式碼項」指的是程式碼單元,例如函式、語句或運算式。

使用 #[cfg(…)] 行,你可以條件編譯你的程式碼。換句話說,你可以為不同的情況建立程式碼的不同版本。例如,在為 wasm32 目標編譯時,編譯器會忽略 #[cfg(not(target_arch = "wasm32"))] 塊,只包含以下內容:

fnmain() {
println!("Hello, WebAssembly!");
}

你透過運算式指定條件,例如 target_arch = "wasm32" 。支持的鍵包括 target_os target_arch 。有關支持的鍵的完整列表,請參閱 Rust 參考手冊 完整列表 [2] 。你還可以使用 Cargo 功能建立運算式,我們將在規則 6 中學習。

你可以使用邏輯運算子 not any all 來組合運算式。Rust 的條件編譯不使用傳統的 if...then...else 語句。相反,你必須使用 #[cfg(...)] 及其否定來處理不同的情況:

#[cfg(not(target_arch = "wasm32"))]
...
#[cfg(target_arch = "wasm32")]
...

要條件編譯整個檔,請將 #![cfg(...)] 放置在檔的頂部。(註意「!」)。當一個檔只與特定目標或配置相關時,這很有用。

你也可以在 Cargo.toml 中使用 cfg 運算式來條件包含依賴項。這允許你根據不同的目標客製依賴項。例如,這表示「當不針對 wasm32 時,依賴於具有 Rayon 的 Criterion」。

[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
criterion = { version = "0.5.1", features = ["rayon"] }

規則 5:執行常規測試,但使用 WASM WASI 目標。

現在,讓我們嘗試在 WASM WASI 上執行 你的 計畫。如規則 3 中所述,為你的計畫建立一個 .cargo/config.toml 檔。它告訴 Cargo 如何在 WASM WASI 上執行和測試你的計畫。

[target.wasm32-wasip1]
runner = "wasmtime run --dir ."

接下來, 你的計畫 - 就像所有好的程式碼一樣 - 應該已經包含測試 [3] 。我的 range-set-blaze 計畫包含以下範例測試:

#[test]
fninsert_255u8() {
let range_set_blaze = RangeSetBlaze::<u8>::from_iter([255]);
assert!(range_set_blaze.to_string() == "255..=255");
}

現在,讓我們嘗試在 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 功能。

規則 6:了解 Cargo 功能。

為了解決像規則 5 中的 Rayon 錯誤這樣的問題,了解 Cargo 功能如何工作非常重要。

Cargo.toml 中,一個可選的 [features] 部份允許你根據啟用的功能或禁用的功能來定義計畫的不同配置或版本。例如,以下是 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 。由於每個功能都可以包含或排除,因此這四個功能建立了計畫的 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 程式碼中,你可以根據啟用的 Cargo 功能包含/排除程式碼項。語法使用 #cfg(…) ,如規則 4 所示:

#[cfg(feature = "html_reports")]
SOME_CODE_ITEM

了解了 Cargo 功能之後,我們現在可以嘗試修復在 WASM WASI 上執行測試時遇到的 Rayon 錯誤。

規則 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 功能以確保相容性。有時這個過程很簡單,但有時它可能很困難 - 甚至不可能,正如我們將在規則 8 中討論的那樣。

旁白:在本系列的下一篇文章中 - 關於瀏覽器中的 WASM - 我們將更深入地探討修復依賴項的策略。

再次執行測試後,我們越過了之前的錯誤,卻遇到了一個新的錯誤,這是一種進步!

#[test]
fntest_demo_i32_len() {
assert_eq!(demo_i32_len(i32::MIN..=i32::MAX), u32::MAX asusize + 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 位環境,因此 usize u32 相同,該運算式大了一個。

這裏的解決方法是用 u64 替換 usize ,確保運算式不會溢位。更一般地說,編譯器不會總是捕獲這些問題,因此審查你對 usize isize 的使用非常重要。如果你指的是 Rust 數據結構的大小或索引, usize 是正確的。但是,如果你處理的值超過了 32 位限制,你應該使用 u64 i64

旁白:在 32 位環境中,Rust 陣列、 Vec BTreeSet 等只能容納最多 2³²−1=4,294,967,295 個元素。

因此,我們已經解決了依賴問題並解決了 usize 溢位問題。但是,我們能修復所有問題嗎?不幸的是,答案是否定的。

規則 8:接受你無法更改所有東西:網路、Tokio、Rayon 等。

WASM WASI 預覽版 1(目前版本)支持檔存取(在指定目錄內)、讀取環境變量以及處理時間和隨機數。但是,與你可能從完整作業系統中期望的功能相比,它的功能有限。

如果你的計畫需要存取網路、使用 Tokio 進行異步任務或使用 Rayon 進行多執行緒,不幸的是,這些功能在預覽版 1 中不受支持。

幸運的是,WASM WASI 預覽版 2 預計將改進這些限制,提供更多功能,包括對網路和可能異步任務的更好支持。

規則 9:將 WASM WASI 添加到你的 CI(持續整合)測試中。

因此,你的測試在 WASM WASI 上透過了,你的計畫也成功執行了。你完成了?還沒有。因為,正如我喜歡說的:

如果不在 CI 中,它就不存在。

持續整合 (CI) 是一個系統,它可以在你每次更新程式碼時自動執行你的測試,確保你的程式碼能夠繼續按預期工作。透過將 WASM WASI 添加到你的 CI 中,你可以保證未來的更改不會破壞你的計畫與 WASM WASI 目標的相容性。

在我的情況下,我的計畫托管在 GitHub 上,我使用 GitHub Actions 作為我的 CI 系統。以下是我添加到 .github/workflows/ci.yml 中的配置,用於在我的計畫上測試 WASM WASI:

test_wasip1:
name:TestWASIP1
runs-on:ubuntu-latest
steps:
-name:Checkout
uses:actions/checkout@v4
-name:SetupRust
uses:dtolnay/rust-toolchain@master
with:
toolchain:stable
targets:wasm32-wasip1
-name:InstallWasmtime
run:|
curl https://wasmtime.dev/install.sh -sSf | bash
echo "${HOME}/.wasmtime/bin" >> $GITHUB_PATH
-name:RunWASItests
run:cargotest--verbose--targetwasm32-wasip1

透過將 WASM WASI 整合到 CI 中,我可以放心地向我的計畫添加新程式碼。CI 將自動測試所有程式碼在未來繼續支持 WASM WASI。

因此,這就是將你的 Rust 程式碼移植到 WASM WASI 的九條規則。以下是我對移植到 WASM WASI 的感受:

不好之處:

  • 在 WASM WASI 上執行在今天幾乎沒有實用價值。但是,它有潛力在明天變得有用。

  • 在 Rust 中,有一句常見的說法:「如果它可以編譯,它就可以工作。」不幸的是,這並不總是適用於 WASM WASI。如果你使用了不支持的功能,比如網路功能,編譯器將不會捕獲錯誤。相反,它將在執行時失敗。例如,這段程式碼可以在 WASM WASI 上編譯和執行,但始終返回錯誤,因為不支持網路功能。

  • use std::net::TcpStream;
    fnmain() {
    match TcpStream::connect("crates.io:80") {
    Ok(_) => println!("Successfully connected."),
    Err(e) => println!("Failed to connect: {e}"),
    }
    }

    好之處:

  • 在 WASM WASI 上執行是將程式碼執行在瀏覽器和嵌入式系統中的一個很好的第一步。

  • 你可以在 WASM WASI 上執行 Rust 程式碼,而無需移植到 no_std 。(移植到 no_std 是本系列文章的第三部份的主題。)

  • 你可以在 WASM WASI 上執行標準的 Rust 測試,這使得驗證你的程式碼變得很容易。

  • .cargo/config.toml 檔和 Rust 的 --target 選項使得在不同的目標上配置和執行你的程式碼變得非常簡單 - 包括 WASM WASI。

  • 參考資料

    [1]

    釋出了一條推文: https://x.com/solomonstre/status/1111004913222324225

    [2]

    完整列表: https://doc.rust-lang.org/reference/conditional-compilation.html#set-configuration-options

    [3]

    你的計畫 - 就像所有好的程式碼一樣 - 應該已經包含測試: https://doc.rust-lang.org/rust-by-example/testing.html

    文章精選