wasm 實用小技巧
最近工作上的開發,對 wasm 的實務又有了更多的掌握,同時發現 wasm 各種資訊的缺乏。 尤其很多技術細節散亂在數十個不同的網頁中,各家編譯器跟 wasm 執行環境又充斥讓人頭痛的小毛病。 所以我決定記錄下各種實用的技巧。
1. Rust 相關的標記屬性
Rust 中的各種標記對編寫能用的 wasm module 置關重要,在其他語言很多 wasm 的特性根本用不出來。 這是目前多家編譯器各自實作 wasm 的碎片問題,不是每個語言都能良好的支援新的 wasm proposal。
1.1. #![feature(wasm_abi)]
跟 extern "wasm"
用 #![feature(wasm_abi)]
開啟特性之後,就不會發生回傳複雜型別的
extern function 被改成用 i32
參數的問題。 例如以下程式就不會變成
=foo(i32) -> ()=,而是正常的 =foo() -> i32=。
extern "C" { fn foo() -> StructType; }
當然,可以看到還是會被轉成用 i32
表示的指標去傳遞資料,但比之前進步很多。
1.2. #[link(wasm_import_module = "mod")]
用 #[link(wasm_import_module = "mod")]
修飾程式的用途是正確的引入其他模組, 舉個範例
#[link(wasm_import_module = "host")] extern "wasm" { fn host_println(str_ptr: *const u8, str_len: usize) -> (); }
用這個標記之後,它就會在 host
尋找 host_println
而不是其他名稱的模組,例如預設的模組 =env=。
1.3. custom section
這可以拿來塞任何你想要的資訊,或是拿去當 buffer,缺點是我看除了 Rust 好像沒有很多編譯器有心要支援。
#[link_section = "hello"] pub static SECTION: [u8; 24] = *b"This is a custom section";
2. WasmEdge SDK
要使用 wasmedge SDK,需要在 Cargo.toml
裡面寫,讓 wasmedge-sys
使用
"*"
是因為兩者有對應,所以還是讓建構工具自己找出合適的依賴就好。
wasmedge-sdk = "0.6.0" wasmedge-sys = "*"
現在專案有了正確的依賴,就可以繼續開發了。
2.1. Host function 傳遞字串
wasm 一個很實際的需求就是傳遞 non-primitive 的資料型別,在 WasmEdge 裡面做這件事的方法其實沒有完善的文件紀錄。 下面的程式碼是 runtime 設定
fn main() -> Result<(), anyhow::Error> { // 設定需要什麼能力 let config = ConfigBuilder::new(CommonConfigOptions::default()) .with_host_registration_config(HostRegistrationConfigOptions::default().wasi(true)) .build()?; // 建立 import object,語意是從 host 引入名為 host_suffix 的函數 let import = ImportObjectBuilder::new() .with_func::<(i32, i32), (i32, i32), !>("host_suffix", host_suffix, None)? .build("host")?; // 建立 vm 並註冊模組 let vm = Vm::new(Some(config))? .register_import_module(import)? .register_module_from_file("app", "app.wasm")?; // 執行叫做 app 模組中名叫 start 的函數並查看結果 let result = vm.run_func(Some("app"), "start", None)?; println!("result: {}", result[0].to_i32()); }
Host function 的定義如下
#[host_function] fn host_suffix(caller: Caller, input: Vec<WasmValue>) -> Result<Vec<WasmValue>, HostFuncError> { let mut mem = caller.memory(0).unwrap(); let addr = input[0].to_i32() as u32; let size = input[1].to_i32() as u32; let data = mem.read(addr, size).expect("fail to get string"); let mut s = String::from_utf8_lossy(&data).to_string(); s.push_str("_suffix"); // 總之 wasm 模組的記憶體肯定不會用到還不存在的位址 let final_addr = mem.size() + 1; // 繼續增加一個 page size 的區塊 mem.grow(1).expect("fail to grow memory"); // 把要傳回去的字串寫入位址 mem.write(s.as_bytes(), final_addr) .expect("fail to write returned string"); Ok(vec![ // 第一個回傳值是指標 WasmValue::from_i32(final_addr as i32), // 第二個回傳值是長度 WasmValue::from_i32(s.len() as i32), ]) }
在 wasm module 那邊則是寫
#![feature(wasm_abi)] #[repr(C)] struct HostString { ptr: *mut u8, size: usize, } #[link(wasm_import_module = "host")] extern "wasm" { fn host_suffix(str_ptr: *const u8, str_len: usize) -> HostString; } #[no_mangle] pub fn start() -> u32 { let s = "hello"; let s2 = unsafe { let HostString { ptr, size } = host_suffix(s.as_ptr(), s.len()); String::from_raw_parts(ptr, size, size) }; s2.len() as u32 }
可以看到 extern "wasm"
裡面 HostString
可以自動從 multi-values
復原回來。
做這個實驗的時候還發生了小插曲,我跟同事發現
host_function
其實沒有正確的遵循 modifier, 所以即使你寫#[host_function] pub fn
它還是會把函數變成 private 的。
2.2. host function 存取 vm
中的 wasm module instance
let cvm = Box::new(vm.clone()); let import = ImportObjectBuilder::new() .with_func::<(i32, i32), (), !>( "grow_module_memory", move |caller: CallingFrame, input: Vec<WasmValue>, _: *mut c_void| -> Result<Vec<WasmValue>, HostFuncError> { let mod_name = load_string(caller, input[0].to_i32() as u32, input[1].to_i32() as u32); let target_mod = cvm.to_owned().named_module(mod_name).unwrap(); // memory 這個名字是 LLVM 預設的生成結果 target_mod.memory("memory").unwrap().grow(1); Ok(vec![]) }, None, )?
因為不能試圖移動 =vm=,只好複製出來使用。
因為又一個插曲, 所以雖然這個技巧理論上能呼叫另一個註冊模組的函數,但目前功能是殘廢的。
3. 結語
總之,這團混亂最大的問題在於 wasm proposal 對實作的要求太少,比如說宣告軟體是否符合標準。 也因此支援度混亂不一,各家編譯器跟執行環境都可以喊說自己有支援,但你頭洗下去才發現其實有很多平台特定的要求跟限制。 當然反過來說這表示 wasm 正在蓬勃發展,很多有用的規範正在推進,希望能夠給大家帶來更多的功能。