wasm-toolsでWebAssemblyコンポーネントを合成する

[wasm-tools](https://github.com/bytecodealliance/wasm-tools) の compose コマンド（wasm-compose）を使った WebAssembly コンポーネントの合成を試してみました。

* [wasm-tools 1.203](https://github.com/bytecodealliance/wasm-tools)

ここでは、```item``` と ```app``` という 2つの WebAssembly コンポーネントを作成し、wasm-compose を使って app へ item を合成します。

なお、WebAssembly コンポーネントの作成には [cargo component](https://github.com/bytecodealliance/cargo-component) を使用します。

# item コンポーネント作成

item 側を作成します。

とりあえず、WIT はこのようにしてみました。
指定した商品の内容を取得する処理を ```itemfind``` インターフェースとして定義し、それを export しています。

```js:item/wit/world.wit
package component:item;

interface itemfind {
    type item-id = string;
    type amount = s32;

    record item {
        id: item-id,
        price: amount,
    }

    find-item: func(id: item-id) -> option<item>;
}

world item {
    export itemfind;
}
```

固定の内容を返すように実装しました。

```rust:item/src/lib.rs
#[allow(warnings)]
mod bindings;

use bindings::exports::component::item::itemfind::Guest;
use bindings::exports::component::item::itemfind::{Amount, Item, ItemId};

struct Component;

impl Guest for Component {
    fn find_item(id: ItemId) -> Option<Item> {
        let price: Amount = 123;
        let item = Item { id, price };

        Some(item)
    }
}

bindings::export!(Component with_types_in bindings);
```

Cargo.toml はこのようになります。

```toml:item/Cargo.toml
[package]
name = "item"
version = "0.1.0"
edition = "2021"

[dependencies]
bitflags = "2.5.0"
wit-bindgen-rt = "0.24.0"

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

[package.metadata.component]
package = "component:item"

[package.metadata.component.dependencies]
```

これをビルドして WebAssembly コンポーネントを作成しておきます。

```sh:itemのビルド
$ cd item
$ cargo component build --target wasm32-unknown-unknown --release
```

# app コンポーネント作成

次に、app 側を作成します。

こちらの WIT では、item 側で export した itemfind を import します。

また、「[cargo componentでWASI Preview 2を使ったコンポーネントを作成](https://qiita.com/fits/items/aaf477de275573658f8a)」と同じ使い方で WASI Preview 2 を利用しています。[^2]

[^2]:wasm32-wasi でビルドして WASI Preview 2 へ自動適応させる方法もあるようですが、ここでは使っていません。また、次の Rust 1.78 から WASI Preview 2 に対応した wasm32-wasip2 ターゲットが導入されるようです。

```js:app/wit/world.wit
package component:app;

world app {
    import component:item/itemfind;

    import wasi:cli/stdout@0.2.0;

    export wasi:cli/run@0.2.0;
}
```

実装はこのようにしてみました。

itemfind インターフェースの ```find-item```（Rust 上は find_item）で取得した item の内容を ```wasi:cli/stdout``` を使って標準出力しています。

```rust:app/src/lib.rs
#[allow(warnings)]
mod bindings;

use bindings::component::item::itemfind::find_item;
use bindings::exports::wasi::cli::run::Guest;
use bindings::wasi::cli::stdout::get_stdout;

struct Component;

impl Guest for Component {
    fn run() -> Result<(), ()> {
        let item = find_item(&"item-1".to_string()).ok_or(())?;

        log(&format!("id={}, price={}", item.id, item.price)).map_err(|_| ())?;

        Ok(())
    }
}

fn log(msg: &str) -> Result<(), Box<dyn std::error::Error>> {
    let stdout = get_stdout();

    stdout.write(msg.as_bytes())?;
    stdout.flush()?;

    Ok(())
}

bindings::export!(Component with_types_in bindings);
```

Cargo.toml はこのようになります。

item の world.wit を参照する必要があるので ```package.metadata.component.target.dependencies``` で設定しています。

```toml:app/Cargo.toml
[package]
name = "app"
version = "0.1.0"
edition = "2021"

[dependencies]
bitflags = "2.5.0"
wit-bindgen-rt = "0.24.0"

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

[package.metadata.component]
package = "component:app"

[package.metadata.component.target.dependencies]
"component:item" = { path = "../item/wit" }

"wasi:cli" = { path = "./WASI/preview2/cli" }
...省略
```

こちらもビルドしておきます。

```sh:appのビルド
$ cd app
$ cargo component build --target wasm32-unknown-unknown --release
```

# 合成処理

それでは、本題の合成を実施します。

wasm-compose では YAML 形式の設定ファイルで合成内容を指定できます。

今回のケースでは次のような設定を行う必要がありました。
こうする事で、指定の import 箇所へ instance で指定した WebAssembly コンポーネントを合成します。

```yaml:合成の設定内容
instantiations:
  root:
    arguments:
      <importの名称>:
        instance: <wasmファイル名（拡張子を除く）>
```

実際はこのようになります。
item.wasm ファイルを見つけられるように ```search-paths``` を設定しています。

```yaml:config.yaml
search-paths:
  - ./item/target/wasm32-unknown-unknown/release

instantiations:
  root:
    arguments:
      component:item/itemfind:
        instance: item
```

instance 部分を省略する事も可能でした。

```yaml:config.yaml（省略形）
search-paths:
  - ./item/target/wasm32-unknown-unknown/release

instantiations:
  root:
    arguments:
      component:item/itemfind: item
```

上記 config.yaml の内容で app.wasm へ合成した結果を ```compose.wasm``` ファイルへ出力してみます。

```sh:コンポーネントの合成
$ wasm-tools compose -c config.yaml -o compose.wasm app/target/wasm32-unknown-unknown/release/app.wasm

...省略
[2024-04-15T13:59:15Z WARN ] instance `wasi:cli/stdout@0.2.0` will be imported because a dependency named `wasi:cli/stdout@0.2.0` could not be found
composed component `compose.wasm`
```

WASI Preview 2 の合成は設定していないので[^1]、WARN ログが出力されていますが特に問題ありません。

[^1]:ランタイム側が提供してくれるので合成する必要がない

## 合成結果の確認

compose.wasm を確認してみます。

wasm-tools で WIT を出力してみると、component:item/itemfind の import が消えており、合成は成功しているように見えます。

```sh:WITの確認
$ wasm-tools component wit compose.wasm

package root:component;

world root {
  import wasi:io/error@0.2.0;
  import wasi:io/streams@0.2.0;
  import wasi:cli/stdout@0.2.0;

  export wasi:cli/run@0.2.0;
}
```

[wasmtime](https://github.com/bytecodealliance/wasmtime) で実行してみると、合成した item から取得した内容が無事に出力されました。

```sh:compose.wasm 実行（動作確認）
$ wasmtime compose.wasm

id=item-1, price=123
```

ちなみに、app.wasm を wasmtime で実行すると次のようなエラーになります。

```sh:app.wasm 実行
$ wasmtime app/target/wasm32-unknown-unknown/release/app.wasm

Error: failed to run main module `app/target/wasm32-unknown-unknown/release/app.wasm`

Caused by:
    0: import `component:item/itemfind` has the wrong type
    1: instance export `find-item` has the wrong type
    2: expected func found nothing
```