0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

WAC (WebAssembly Compositions) は WebAssembly コンポーネントを合成するためのツールです。

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

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

準備

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

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

WIT の依存関係を wit-deps で処理する場合、こちらの CLI もインストールしておきます。

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

コンポーネントの作成

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

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

[dependencies]
wit-bindgen = "0.42.1"

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

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

item コンポーネント

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

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;
}

実装はこうしました。

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);

ビルドしておきます。

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

cart コンポーネント

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

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

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 の取得元を設定しておきます。

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

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

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

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

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 を定義しました。

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

world app {
    import testapp:cart/types;

    export wasi:cli/run@0.2.3;
}

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

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

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

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の結果)を合成

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

1. cartへitemを合成
$ wac plug ./cart/target/wasm32-wasip2/release/cart.wasm --plug ./item/target/wasm32-wasip2/release/item.wasm -o cart_p.wasm
2. appへ合成
$ wac plug ./app/target/wasm32-wasip2/release/app.wasm --plug cart_p.wasm -o output_1.wasm

動作確認

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

実行結果
$ 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

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 を割り当てています。

また、... を使う事で指定しなかった import2 箇所はそのままとなります。

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;

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

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 ファイルを直接指定しました。

合成
$ 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 による実行結果です。

実行結果
$ 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 の確認結果です。

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 ファイルを次のようにすると取り除けます。

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;
  1. 今回のケースでは item 以外の cart と app でビルド時に付与されました。wasm32-wasip2 と wit-bindgen によるものだと思われます

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

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

0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?