cargo component を使って WebAssembly Component Model のコンポーネントを作ってみました。
はじめに
cargo component をインストールしておきます。
$ cargo install cargo-component
コンポーネント作成
cargo component new コマンドを使ってプロジェクトを作成します。
$ cargo component new --reactor sample
以下のファイルが生成され、Cargo.toml の内容は次のようになりました。
- src/lib.rs
- wit/world.wit
- Cargo.toml
なお、--reactor
を付けない場合はコマンド実行タイプのコンポーネントになるようです。(WIT ファイルは作成されず、lib.rs の代わりに main.rs になる)
[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
でカート内の商品数量を変更
また、以前に wit-bindgen で試した頃とは 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 トレイトを実装する事になります。
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
でビルドします。
$ cargo component build --target wasm32-unknown-unknown --release
検証
ビルドで生成された sample.wasm を wasm-tools で検証してみたところ、問題は無さそうでした。
$ wasm-tools validate -f all target/wasm32-unknown-unknown/release/sample.wasm
ついでに、sample.wasm の 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 を使って実行してみます。
基本的な処理内容は「wit-bindgen で WebAssembly Component Model のコンポーネントを作ってみる」と同じです。
export 定義した create と change-qty を call_create
と call_change_qty
を使ってそれぞれ呼び出しています。
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(())
}
[package]
name = "run_component"
version = "0.1.0"
edition = "2021"
[dependencies]
wasmtime = { version = "15", features = ['component-model'] }
実行結果はこのようになりました。
$ 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" })