UP | HOME

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 正在蓬勃發展,很多有用的規範正在推進,希望能夠給大家帶來更多的功能。

Date: 2022-11-12 Sat 00:00

Author: Lîm Tsú-thuàn