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

[waPC (WebAssembly Procedure Calls)](https://wapc.io/) による WebAssembly の関数呼び出しを [wapc-rs](https://github.com/wapc/wapc-rs) を使って試してみました。

* [wapc-rs v1.0](https://github.com/wapc/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](https://wapc.io/docs/spec/#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``` によるログ出力を実施したものが下記です。[^2a]

[^2a]: この実装では、リクエストの payload も UTF-8 文字列として扱っています

##### [wasm1/src/lib.rs](https://github.com/fits/try_samples/tree/master/blog/20230207/waPC_rust/wasm1/src/lib.rs)
```rust
// 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 をビルドする際は指定しておいた方が良さそうです。[^2b]

[^2b]: opt-level は "s" より "z" の方が、（少しだけ）ファイルサイズが小さくなるようです

##### [wasm1/Cargo.toml](https://github.com/fits/try_samples/tree/master/blog/20230207/waPC_rust/wasm1/Cargo.toml)
```toml
...省略

[lib]
crate-type = ["cdylib"]

[profile.release]
opt-level = "s"
lto = true
```

##### ビルド例
```sh
$ 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](https://github.com/fits/try_samples/tree/master/blog/20230207/waPC_rust/wapc_host/src/main.rs)
```rust
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](https://github.com/fits/try_samples/tree/master/blog/20230207/waPC_rust/wapc_host/Cargo.toml)
```toml
...省略

[dependencies]
wapc = "1"
wasmtime-provider = "1"
env_logger = "0.10"
```

## 実行

作成した WebAssembly を呼び出してみたところ、次のような結果となりました。

##### 実行結果例
```sh
$ RUST_LOG=info cargo run ../wasm1/target/wasm32-unknown-unknown/release/wasm1.wasm sample abc123
```
```sh
[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](https://github.com/fits/try_samples/tree/master/blog/20230207/waPC_rust/wasm2/src/lib.rs)
```rust
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](https://github.com/fits/try_samples/tree/master/blog/20230207/waPC_rust/wasm2/Cargo.toml)
```toml
...省略

[lib]
crate-type = ["cdylib"]

[dependencies]
wapc-guest = "1"

[profile.release]
opt-level = "s"
lto = true
```

##### ビルド例
```sh
$ cargo build --release --target wasm32-unknown-unknown
```

## 動作確認

作成した Host 処理から呼び出してみます。

##### 実行結果例1
```sh
$ RUST_LOG=info cargo run ../wasm2/target/wasm32-unknown-unknown/release/wasm2.wasm greet waPC
```
```sh
[2023-02-07T10:56:39Z INFO  wapc::wapchost::modulestate] Guest module 1: called operation=greet, payload=waPC
result: Hi, waPC !
```

当然ながら、register_function で登録していない関数名を指定するとエラーになりました。

##### 実行結果例2 （登録していない関数名を指定）
```sh
$ RUST_LOG=info cargo run ../wasm2/target/wasm32-unknown-unknown/release/wasm2.wasm sample 123abc
```
```sh
Error: GuestCallFailure("No handler registered for function sample")
```