LoginSignup
1
0

More than 1 year has passed since last update.

Rust で waPC を使った WebAssembly の関数呼び出し

Posted at

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")
  1. この実装では、リクエストの payload も UTF-8 文字列として扱っています

  2. opt-level は "s" より "z" の方が、(少しだけ)ファイルサイズが小さくなるようです

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0