WACでWebAssemblyコンポーネントを合成する

[WAC (WebAssembly Compositions)](https://github.com/bytecodealliance/wac) は WebAssembly コンポーネントを合成するためのツールです。

特徴は、WIT のスーパーセットとされる WAC 言語で合成方法を記述できる点です。

ここでは、次のような構成の 3つのコンポーネントを WAC で合成して 1つにします。

```mermaid
flowchart LR

app -->|create| cart -->|find| item
app -->|add-item| cart
```

# 準備

WAC の CLI ツールをインストールしておきます。

```sh:wac-cliインストール
$ cargo install wac-cli
```
```sh:wac-cliバージョン確認
$ wac -V
wac-cli 0.6.1
```

WIT の依存関係を [wit-deps](https://github.com/bytecodealliance/wit-deps) で処理する場合、こちらの CLI もインストールしておきます。

```sh:wit-deps-cliインストール
$ cargo install wit-deps-cli
```

# コンポーネントの作成

コンポーネントを wit-bindgen で作成するよう Cargo.toml は次の内容にしました。

```toml:Cargo.toml例
[lib]
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = "0.42.1"
```

ビルドは個々のコンポーネントで次のように実行します。

```sh:ビルド例
$ cargo build --release --target wasm32-wasip2
```

## item コンポーネント

WIT は次のように定義しました。

```java:item/wit/item.wit
package testapp:item;

interface types {
    type item-id = string;
    type price = s32;

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

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

world item {
    export types;
}
```

実装はこうしました。

```rust:item/src/lib.rs
use exports::testapp::item::types::{Guest, Item, ItemId};

wit_bindgen::generate!("item");

struct Component;

impl Guest for Component {
    fn find(id: ItemId) -> Option<Item> {
        let price = (id.len() as i32) * 1000;
        Some(Item {
            id,
            unit_price: price,
        })
    }
}

export!(Component);
```

ビルドしておきます。

```sh:item ビルド
$ cd item
$ cargo build --release --target wasm32-wasip2
```

## cart コンポーネント

WIT は次のように定義しました。

item.wit の types を利用できるように import しています。
また、```variant``` 型で代数的データ型（ADT）の直和型（sum types）を表現できます。

```java:cart/wit/cart.wit
package testapp:cart;

interface types {
    use testapp:item/types.{item-id, item};

    type cart-id = string;
    type quantity = u32;

    record cart-item {
        item: item,
        qty: quantity,
    }

    variant cart-state {
        empty(cart-id),
        active(tuple<cart-id, list<cart-item>>),
    }

    create: func(id: cart-id) -> cart-state;
    add-item: func(state: cart-state, item: item-id, qty: quantity) -> option<cart-state>;
}

world cart {
    import testapp:item/types;

    export types;
}
```

wit/deps.toml を作成し、item.wit の取得元を設定しておきます。

```toml:cart/wit/deps.toml
item = "../../item/wit"
```

```wit-deps``` コマンドを実行すると ```wit/deps/item/item.wit``` が生成されます。

```sh:WITの依存関係解決
$ cd cart
$ wit-deps
```

実装はこのようにしました。
item コンポーネントの ```find``` を呼び出しています。

```rust:cart/src/lib.rs
use exports::testapp::cart::types::{CartId, CartItem, CartState, Guest, ItemId, Quantity};
use testapp::item::types::find;

wit_bindgen::generate!({
    world: "cart",
    with: {
        "testapp:item/types": generate, // item.wit の types に関する型を生成
    }
});

struct Component;

impl Guest for Component {
    fn create(id: CartId) -> CartState {
        CartState::Empty(id)
    }

    fn add_item(state: CartState, item_id: ItemId, qty: Quantity) -> Option<CartState> {
        if qty == 0 {
            return None;
        }

        find(&item_id).and_then(|item| {
            let item = CartItem { item, qty };

            add_cart_item(state, item)
        })
    }
}

export!(Component);

fn add_cart_item(state: CartState, item: CartItem) -> Option<CartState> {
    match state {
        CartState::Empty(id) => Some(CartState::Active((id, vec![item]))),
        CartState::Active((id, items)) => {
            let new_items = insert_or_update(&items, item);
            Some(CartState::Active((id, new_items)))
        }
    }
}

fn insert_or_update(src: &Vec<CartItem>, item: CartItem) -> Vec<CartItem> {
    ...省略
}
```

item コンポーネントと同様にビルドしておきます。

## app コンポーネント

コマンドライン実行するための WIT を定義しました。

```java:app/wit/app.wit
package testapp:app;

world app {
    import testapp:cart/types;

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

deps.toml はこのようにしました。

```toml:app/wit/deps.toml
cli = "https://github.com/WebAssembly/wasi-cli/archive/refs/tags/v0.2.3.tar.gz"
cart = "../../cart/wit"
```

cart の処理をいくつか呼び出すような実装にしました。

```rust:app/src/lib.rs
use exports::wasi::cli::run::Guest;
use testapp::cart::types::{add_item, create};

wit_bindgen::generate!({
    world: "app",
    with: {
        "testapp:cart/types": generate,
        "testapp:item/types": generate,
        "wasi:cli/run@0.2.3": generate,
    }
});

struct Component;

impl Guest for Component {
    fn run() -> Result<(), ()> {
        let s1 = create("cart-1");
        println!("1. {:?}", s1);

        let s2 = add_item(&s1, "item1", 1);
        println!("2. {:?}", s2);

        let s3 = add_item(&s2.unwrap(), "item-22", 2);
        println!("3. {:?}", s3);

        let s4 = add_item(&s3.unwrap(), "item1", 3);
        println!("4. {:?}", s4);

        Ok(())
    }
}

export!(Component);
```

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

# plugコマンドで合成

```wac plug``` コマンドを使って、次の手順で合成します。

1. cart コンポーネントへ item コンポーネントを合成し cart_p.wasm へ出力
2. app コンポーネントへ cart_p.wasm（1の結果）を合成

この方法では細かく指定しなくてもインターフェースがマッチする部分へ勝手に合成してくれます。

```sh:1. cartへitemを合成
$ wac plug ./cart/target/wasm32-wasip2/release/cart.wasm --plug ./item/target/wasm32-wasip2/release/item.wasm -o cart_p.wasm
```

```sh:2. appへ合成
$ wac plug ./app/target/wasm32-wasip2/release/app.wasm --plug cart_p.wasm -o output_1.wasm
```

## 動作確認

合成した結果を wasmtime で実行すると次のようになりました。

```sh:実行結果
$ wasmtime run output_1.wasm 
1. CartState::Empty("cart-1")
2. Some(CartState::Active(("cart-1", [CartItem { item: Item { id: "item1", unit-price: 5000 }, qty: 1 }])))
3. Some(CartState::Active(("cart-1", [CartItem { item: Item { id: "item1", unit-price: 5000 }, qty: 1 }, CartItem { item: Item { id: "item-22", unit-price: 7000 }, qty: 2 }])))
4. Some(CartState::Active(("cart-1", [CartItem { item: Item { id: "item1", unit-price: 5000 }, qty: 4 }, CartItem { item: Item { id: "item-22", unit-price: 7000 }, qty: 2 }])))
```

wasm-tools で WIT を確認した結果です。

```wasi:cli/environment``` 等を import していますが、これは WAC による合成時ではなくビルド時に付与されたものです。[^1]

[^1]: 今回のケースでは item 以外の cart と app でビルド時に付与されました。wasm32-wasip2 と wit-bindgen によるものだと思われます

```sh:WIT確認結果
$ wasm-tools component wit output_1.wasm 
package root:component;

world root {
  import testapp:item/types;
  import wasi:cli/environment@0.2.3;
  import wasi:cli/exit@0.2.3;
  import wasi:io/error@0.2.3;
  import wasi:io/streams@0.2.3;
  import wasi:cli/stdin@0.2.3;
  import wasi:cli/stdout@0.2.3;
  import wasi:cli/stderr@0.2.3;
  import wasi:clocks/wall-clock@0.2.3;
  import wasi:filesystem/types@0.2.3;
  import wasi:filesystem/preopens@0.2.3;

  export wasi:cli/run@0.2.3;
}
...省略
```

# composeコマンドで合成

次に wac ファイルを使って合成します。

```new``` を使う事で WIT の world で定義した export を参照したり、import への割り当てを指定できます。

例えば、以下では cart が import している ```testapp:item/types``` へ item が export している ```types``` を割り当てています。

また、```...``` を使う事で指定しなかった import[^2] 箇所はそのままとなります。

[^2]: cart や app は wasi:cli/environment 等も import しているため、```...``` を使用しないと ```missing argument `wasi:cli/environment@0.2.3``` 等のエラーが発生して合成に失敗します

```java:compose.wac
package testapp:composition;

let item = new testapp:item {};

let cart = new testapp:cart { "testapp:item/types": item.types, ... };

let app = new testapp:app { "testapp:cart/types": cart.types, ... };

export app.run;
```

なお、以下のようにしても同じ結果となります。

```java:compose_1.wac
package testapp:composition;

let item = new testapp:item {};

let cart = new testapp:cart { ...item, ... };

let app = new testapp:app { ...cart, ... };

export app.run;
```

```wac compose``` コマンドで合成します。

カレントの deps ディレクトリへ配置した WebAssembly を使う方法もありますが[^3]、ここでは合成で使用する WebAssembly ファイルを直接指定しました。

[^3]: この場合は ```deps/<namespace>/<package>.wasm``` へ配置します

```sh:合成
$ wac compose \
    --dep testapp:app=./app/target/wasm32-wasip2/release/app.wasm \
    --dep testapp:cart=./cart/target/wasm32-wasip2/release/cart.wasm \
    --dep testapp:item=./item/target/wasm32-wasip2/release/item.wasm \
    -o output_2.wasm \
    compose.wac
```

## 動作確認

wasmtime による実行結果です。

```sh:実行結果
$ wasmtime run output_2.wasm 
1. CartState::Empty("cart-1")
2. Some(CartState::Active(("cart-1", [CartItem { item: Item { id: "item1", unit-price: 5000 }, qty: 1 }])))
3. Some(CartState::Active(("cart-1", [CartItem { item: Item { id: "item1", unit-price: 5000 }, qty: 1 }, CartItem { item: Item { id: "item-22", unit-price: 7000 }, qty: 2 }])))
4. Some(CartState::Active(("cart-1", [CartItem { item: Item { id: "item1", unit-price: 5000 }, qty: 4 }, CartItem { item: Item { id: "item-22", unit-price: 7000 }, qty: 2 }])))
```

WIT の確認結果です。

```sh:WIT確認結果
$ wasm-tools component wit output_2.wasm
package root:component;

world root {
  import wasi:cli/environment@0.2.3;
  import wasi:cli/exit@0.2.3;
  import wasi:io/error@0.2.3;
  import wasi:io/streams@0.2.3;
  import wasi:cli/stdin@0.2.3;
  import wasi:cli/stdout@0.2.3;
  import wasi:cli/stderr@0.2.3;
  import wasi:clocks/wall-clock@0.2.3;
  import wasi:filesystem/types@0.2.3;
  import wasi:filesystem/preopens@0.2.3;
  import testapp:item/types;

  export wasi:cli/run@0.2.3;
}
...省略
```

ちなみに、上記の ```import testapp:item/types``` は合成で付与されたものではなく、app のビルド時に付与されたものです。

この import は本来不要なはずで、wac ファイルを次のようにすると取り除けます。

```java:compose_2.wac
package testapp:composition;

let item = new testapp:item {};

let cart = new testapp:cart { ...item, ... };

let app = new testapp:app { ...cart, ...item, ... };

export app.run;
```