waPC (WebAssembly Procedure Calls) による WebAssembly の関数呼び出しを wapc-rs を使って試してみました。
waPC とは
waPC (WebAssembly Procedure Calls) はその名の通り、WebAssembly でプロシージャコールを実現するための仕様です。
通常、WebAssembly とその呼び出し側(ランタイム等)との間で文字列を直接受け渡したりはできないため、共有するメモリへデータを書き込み、そのポインタやバイトサイズをやり取りする事になります。
そのような手続きをプロシージャコールに特化して仕様化したのが waPC のようです。
https://wapc.io/docs/spec/ によると、呼び出される WebAssembly を Guest、その呼び出し側を Host と表現しています。
名称 | 概要 |
---|---|
Host | WebAssembly を呼び出す側 |
Guest | Host から呼び出される WebAssembly モジュール |
Guest となる WebAssembly では、次のような関数を import/export する事になっているようです。
- import する関数
- __host_call
- __host_response
- __host_response_len
- __host_error
- __host_error_len
- __guest_request
- __guest_response
- __guest_error
- __console_log
- export する関数
- __guest_call
__guest_xxx
系の関数は Host が Guest を呼び出す Guest call
で使用し、__host_xxx
系はその逆の Host call
で使用する事になります。
waPC Guest の実装1
まずは、wapc-rs の助けを借りずに waPC Guest となる WebAssembly を自前で実装してみます。
RPC Exchange flow の内容を参考に、単純な Guest call
の処理を実装する事にします。
この場合、__guest_call
では次のような処理を実施する事になります。
- (1) リクエスト(operation と payload)の内容を書き込む領域(メモリ)を確保
- (2)
__guest_request
を呼び出し、Host にリクエストの内容を書き込んでもらう - (3) operation(UTF-8 文字列)と payload(バイト配列)の内容を取得し、処理を実施
- (4)
__guest_response
を呼び出し、レスポンスが格納されている場所(ポインタ)とサイズを Host へ通知 - (5) 処理の成否を戻り値として返す(1 = 成功、0 = 失敗)
これらの処理に加えて、__console_log
によるログ出力を実施したものが下記です。1
wasm1/src/lib.rs
// wapc から import する関数の定義(ここでは実際に使うものだけに限定)
#[link(wasm_import_module = "wapc")]
extern {
fn __console_log(ptr: *const u8, len: usize);
fn __guest_request(op_ptr: *mut u8, ptr: *mut u8);
fn __guest_response(ptr: *const u8, len: usize);
}
#[no_mangle]
extern fn __guest_call(op_len: i32, msg_len: i32) -> i32 {
// (1)
let mut op_buf = vec![0; op_len as _];
let mut msg_buf = vec![0; msg_len as _];
unsafe {
// (2)
__guest_request(op_buf.as_mut_ptr(), msg_buf.as_mut_ptr());
}
// (3)
let op = String::from_utf8(op_buf).unwrap_or(String::default());
let payload = String::from_utf8(msg_buf).unwrap_or(String::default());
// ログメッセージ
let log_msg = format!("called operation={}, payload={}", op, payload);
unsafe {
// ログ出力
__console_log(log_msg.as_ptr(), log_msg.len());
}
// レスポンスの内容
let res = format!("response-{}-{}", op, payload);
unsafe {
// (4)
__guest_response(res.as_ptr(), res.len());
}
// (5)
1
}
ビルド
次のような Cargo.toml でビルドしました。
opt-level 等をデフォルト値から変更する事で、生成される .wasm ファイルのサイズが大幅に減少したりするので WebAssembly をビルドする際は指定しておいた方が良さそうです。2
wasm1/Cargo.toml
...省略
[lib]
crate-type = ["cdylib"]
[profile.release]
opt-level = "s"
lto = true
ビルド例
$ cargo build --release --target wasm32-unknown-unknown
waPC Host の実装
この WebAssembly を動作確認するため、waPC Host の処理を wapc-rs を使って実装してみました。
コマンドライン引数で実行対象の WebAssembly ファイルや関数名(オペレーション)等を渡すようにしています。
wapc-rs では __console_log
された内容をログ出力するようになっており、デフォルトでは(標準出力へ)出力しなかったので env_logger
を使いました。
wapc_host/src/main.rs
use wapc::{WapcHost, errors::Error};
use wasmtime_provider::WasmtimeEngineProviderBuilder;
use std::env;
use std::fs;
fn error(msg: &str) -> Error {
Error::General(msg.to_string())
}
fn main() -> Result<(), Error> {
env_logger::init();
let mut args = env::args().skip(1);
let wasm_file = args.next().ok_or(error("wasm file"))?;
let operation = args.next().ok_or(error("operation"))?;
let payload = args.next().ok_or(error("payload"))?;
let buf = fs::read(wasm_file)?;
let engine = WasmtimeEngineProviderBuilder::new()
.module_bytes(&buf)
.build()?;
let host = WapcHost::new(Box::new(engine), None)?;
// WebAssembly の __guest_call 呼び出し
let res = host.call(&operation, payload.as_bytes())?;
if let Ok(r) = String::from_utf8(res) {
// 結果の出力
println!("result: {}", r);
}
Ok(())
}
wapc_host/Cargo.toml
...省略
[dependencies]
wapc = "1"
wasmtime-provider = "1"
env_logger = "0.10"
実行
作成した WebAssembly を呼び出してみたところ、次のような結果となりました。
実行結果例
$ RUST_LOG=info cargo run ../wasm1/target/wasm32-unknown-unknown/release/wasm1.wasm sample abc123
[2023-02-07T10:39:42Z INFO wapc::wapchost::modulestate] Guest module 1: called operation=sample, payload=abc123
result: response-sample-abc123
waPC Guest の実装2
ついでに、wapc-rs の wapc-guest
を使って WebAssembly を実装してみました。
こちらは register_function
で関数を登録するだけなので非常に簡単でした。
__guest_call 等の処理は wapc-guest の内部で上手い事やってくれます。リクエストの内容を関数の引数として渡してくれるので、処理結果としてのレスポンスを関数の戻り値として返します。
wasm2/src/lib.rs
use wapc_guest as wapc;
#[no_mangle]
fn wapc_init() {
wapc::register_function("greet", greet);
}
fn greet(req: &[u8]) -> wapc::CallResult {
let payload = std::str::from_utf8(req)?;
// ログ出力
wapc::console_log(&format!("called operation=greet, payload={}", payload));
// レスポンスの内容
let res = format!("Hi, {} !", payload);
Ok(res.into())
}
wasm2/Cargo.toml
...省略
[lib]
crate-type = ["cdylib"]
[dependencies]
wapc-guest = "1"
[profile.release]
opt-level = "s"
lto = true
ビルド例
$ cargo build --release --target wasm32-unknown-unknown
動作確認
作成した Host 処理から呼び出してみます。
実行結果例1
$ RUST_LOG=info cargo run ../wasm2/target/wasm32-unknown-unknown/release/wasm2.wasm greet waPC
[2023-02-07T10:56:39Z INFO wapc::wapchost::modulestate] Guest module 1: called operation=greet, payload=waPC
result: Hi, waPC !
当然ながら、register_function で登録していない関数名を指定するとエラーになりました。
実行結果例2 (登録していない関数名を指定)
$ RUST_LOG=info cargo run ../wasm2/target/wasm32-unknown-unknown/release/wasm2.wasm sample 123abc
Error: GuestCallFailure("No handler registered for function sample")