cargo component で WebAssembly Component Model のコンポーネントを作成

[cargo component](https://github.com/bytecodealliance/cargo-component) を使って WebAssembly Component Model のコンポーネントを作ってみました。

* [cargo component v0.5](https://github.com/bytecodealliance/cargo-component)

# はじめに

cargo component をインストールしておきます。

```sh:インストール例
$ cargo install cargo-component
```

# コンポーネント作成

cargo component new コマンドを使ってプロジェクトを作成します。

```sh:プロジェクト作成
$ cargo component new --reactor sample
```

以下のファイルが生成され、Cargo.toml の内容は次のようになりました。

* src/lib.rs
* wit/world.wit
* Cargo.toml

なお、```--reactor``` を付けない場合はコマンド実行タイプのコンポーネントになるようです。（WIT ファイルは作成されず、lib.rs の代わりに main.rs になる）

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

[dependencies]
cargo-component-bindings = "0.5.0"

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

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

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

## WIT の定義

ここでは、単純なショッピングカートの処理を定義してみました。

* カート（```cart```）は ```empty``` と ```active``` の 2種類の状態で構成（variant で定義可能）
* ```create``` 関数でカートを作成
* ```change-qty``` でカート内の商品数量を変更

また、[以前](https://qiita.com/fits/items/6e142cd0e7fd200476e8)に wit-bindgen で試した頃とは WIT の仕様が変更されています。

```scala:sample/wit/world.wit
package component:sample;

interface types {
    type cart-id = string;
    type item-id = string;
    type quantity = u32;

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

    record empty-cart {
        id: cart-id,
    }

    record active-cart {
        id: cart-id,
        items: list<cart-item>,
    }

    variant cart {
        empty(empty-cart),
        active(active-cart),
    }
}

world sample {
    use types.{ cart, cart-id, item-id, quantity };

    export create: func(id: cart-id) -> cart;
    export change-qty: func(state: cart, item: item-id, qty: quantity) -> cart;
}
```

## 処理の実装

WIT で export 定義した create と change-qty 関数を Rust で実装します。

WIT ファイルに基づいた Rust 用のコードが ```target/bindings/{name}/bindings.rs``` として生成されるので、この中で定義されている Guest トレイトを実装する事になります。

```rust:sample/src/lib.rs
cargo_component_bindings::generate!();

use bindings::component::sample::types::{ActiveCart, CartItem, EmptyCart};
use bindings::{Cart, CartId, Guest, ItemId, Quantity};

struct Component;

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

    fn change_qty(state: Cart, item: ItemId, qty: Quantity) -> Cart {
        let (id, items) = match state {
            Cart::Empty(EmptyCart { ref id }) => (id.clone(), Vec::<CartItem>::new()),
            Cart::Active(ActiveCart { ref id, ref items }) => (id.clone(), items.clone()),
        };

        let new_items = update_items(items, item, qty);

        if new_items.is_empty() {
            Cart::Empty(EmptyCart { id })
        } else {
            Cart::Active(ActiveCart {
                id,
                items: new_items,
            })
        }
    }
}

fn update_items(mut items: Vec<CartItem>, item: ItemId, qty: Quantity) -> Vec<CartItem> {
    match items.iter().position(|v| v.item == item) {
        Some(index) => {
            if qty == 0 {
                items.remove(index);
            } else {
                items[index].qty = qty;
            }
        }
        None => {
            if qty > 0 {
                items.push(CartItem { item, qty });
            }
        }
    };

    items
}
```

## ビルド

cargo component のデフォルトは ```wasm32-wasi``` でビルドするようになっていますが、今回 WASI は使っていないので ```wasm32-unknown-unknown```　でビルドします。

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

## 検証

ビルドで生成された sample.wasm を [wasm-tools](https://github.com/bytecodealliance/wasm-tools) で検証してみたところ、問題は無さそうでした。

```sh:検証例
$ wasm-tools validate -f all target/wasm32-unknown-unknown/release/sample.wasm 
```

ついでに、sample.wasm の WIT の内容も確認してみました。

```sh:WIT確認例
$ wasm-tools component wit target/wasm32-unknown-unknown/release/sample.wasm

package root:component;

world root {
  import component:sample/types;
  use component:sample/types.{cart, cart-id, item-id, quantity};

  export create: func(id: cart-id) -> cart;
  export change-qty: func(state: cart, item: item-id, qty: quantity) -> cart;
}
```

# コンポーネント実行

[wasmtime](https://github.com/bytecodealliance/wasmtime) を使って実行してみます。

基本的な処理内容は「[wit-bindgen で WebAssembly Component Model のコンポーネントを作ってみる](https://qiita.com/fits/items/6e142cd0e7fd200476e8)」と同じです。

export 定義した create と change-qty を ```call_create``` と ```call_change_qty``` を使ってそれぞれ呼び出しています。

```rust:run_component/src/main.rs
use wasmtime::component::{Component, Linker};
use wasmtime::{Config, Engine, Store};

use std::env;

wasmtime::component::bindgen!("sample" in "../sample/wit");

#[derive(Default)]
struct State {}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let file = env::args().skip(1).next().unwrap_or_default();

    let mut config = Config::new();
    config.wasm_component_model(true);

    let engine = Engine::new(&config)?;

    let component = Component::from_file(&engine, file)?;

    let linker = Linker::new(&engine);

    let mut store = Store::new(&engine, State::default());

    let (bindings, _) = Sample::instantiate(&mut store, &component, &linker)?;
    // create 関数の呼び出し
    let s1 = bindings.call_create(&mut store, &"cart1".to_string())?;
    println!("{:?}", s1);
    // change-qty 関数の呼び出し
    let s2 = bindings.call_change_qty(&mut store, &s1, &"item-1".to_string(), 2)?;
    println!("{:?}", s2);

    let s3 = bindings.call_change_qty(&mut store, &s2, &"item-2".to_string(), 1)?;
    println!("{:?}", s3);

    let s4 = bindings.call_change_qty(&mut store, &s3, &"item-2".to_string(), 5)?;
    println!("{:?}", s4);

    let s5 = bindings.call_change_qty(&mut store, &s4, &"item-1".to_string(), 0)?;
    println!("{:?}", s5);

    let s6 = bindings.call_change_qty(&mut store, &s5, &"item-2".to_string(), 0)?;
    println!("{:?}", s6);

    Ok(())
}
```

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

[dependencies]
wasmtime = { version = "15", features = ['component-model'] }
```

実行結果はこのようになりました。

```sh:実行結果
$ cargo run ../sample/target/wasm32-unknown-unknown/release/sample.wasm
...省略
Cart::Empty(EmptyCart { id: "cart1" })
Cart::Active(ActiveCart { id: "cart1", items: [CartItem { item: "item-1", qty: 2 }] })
Cart::Active(ActiveCart { id: "cart1", items: [CartItem { item: "item-1", qty: 2 }, CartItem { item: "item-2", qty: 1 }] })
Cart::Active(ActiveCart { id: "cart1", items: [CartItem { item: "item-1", qty: 2 }, CartItem { item: "item-2", qty: 5 }] })
Cart::Active(ActiveCart { id: "cart1", items: [CartItem { item: "item-2", qty: 5 }] })
Cart::Empty(EmptyCart { id: "cart1" })
```