WAC (WebAssembly Compositions) は WebAssembly コンポーネントを合成するためのツールです。
特徴は、WIT のスーパーセットとされる WAC 言語で合成方法を記述できる点です。
ここでは、次のような構成の 3つのコンポーネントを WAC で合成して 1つにします。
準備
WAC の CLI ツールをインストールしておきます。
$ cargo install wac-cli
$ wac -V
wac-cli 0.6.1
WIT の依存関係を wit-deps で処理する場合、こちらの CLI もインストールしておきます。
$ cargo install wit-deps-cli
コンポーネントの作成
コンポーネントを wit-bindgen で作成するよう Cargo.toml は次の内容にしました。
[lib]
crate-type = ["cdylib"]
[dependencies]
wit-bindgen = "0.42.1"
ビルドは個々のコンポーネントで次のように実行します。
$ cargo build --release --target wasm32-wasip2
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;
}
実装はこうしました。
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);
ビルドしておきます。
$ cd item
$ cargo build --release --target wasm32-wasip2
cart コンポーネント
WIT は次のように定義しました。
item.wit の types を利用できるように import しています。
また、variant
型で代数的データ型(ADT)の直和型(sum types)を表現できます。
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 の取得元を設定しておきます。
item = "../../item/wit"
wit-deps
コマンドを実行すると wit/deps/item/item.wit
が生成されます。
$ cd cart
$ wit-deps
実装はこのようにしました。
item コンポーネントの find
を呼び出しています。
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 を定義しました。
package testapp:app;
world app {
import testapp:cart/types;
export wasi:cli/run@0.2.3;
}
deps.toml はこのようにしました。
cli = "https://github.com/WebAssembly/wasi-cli/archive/refs/tags/v0.2.3.tar.gz"
cart = "../../cart/wit"
cart の処理をいくつか呼び出すような実装にしました。
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
コマンドを使って、次の手順で合成します。
- cart コンポーネントへ item コンポーネントを合成し cart_p.wasm へ出力
- app コンポーネントへ cart_p.wasm(1の結果)を合成
この方法では細かく指定しなくてもインターフェースがマッチする部分へ勝手に合成してくれます。
$ wac plug ./cart/target/wasm32-wasip2/release/cart.wasm --plug ./item/target/wasm32-wasip2/release/item.wasm -o cart_p.wasm
$ 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
$ 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 箇所はそのままとなります。
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;
なお、以下のようにしても同じ結果となります。
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 の確認結果です。
$ 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 ファイルを次のようにすると取り除けます。
package testapp:composition;
let item = new testapp:item {};
let cart = new testapp:cart { ...item, ... };
let app = new testapp:app { ...cart, ...item, ... };
export app.run;