wit-bindgen で WebAssembly Component Model のコンポーネントを作って実行してみました。
WebAssembly Component Model は、WebAssembly で作ったコンポーネントを組み合わせてアプリケーションを作るための仕組みで、基本的には WIT(Wasm Interface Type)と呼ばれる IDL を使ってコンポーネントのインターフェースを定義するようになっているようです。
個人的には、以前からマイクロサービスの次は WebAssembly によるビジネスロジック等のコンポーネント化が来るだろうなと考えていたので、この技術には期待しています。
(a) コンポーネントの作成
とりあえず現時点では、次のような手順でコンポーネントを作れば良さそうです。
- wit-bindgen を使ってコンポーネントの元となる WebAssembly を作成
- 作成した WebAssembly を wasm-tools で処理してコンポーネントを作成
WIT の作成
wit-bindgen はデフォルトで ./wit
ディレクトリを参照するようになっているようなので、そこへ次のようなファイルを作成しました。
import で外部からインポートする関数を、export で外部へエクスポートする関数を定義します。
なお、今回のバージョンでは関数名の一部に -
や _
を使うと上手くいかなかったので使わないようにしています。
sample/wit/sample.wit
default world sample {
enum log-level {
debug,
info,
}
record item {
id: string,
value: u32,
}
import log: func(level: log-level, msg: string)
import getitem: func(id: string) -> item
export run: func()
}
実装
wit_bindgen::generate!
マクロで WIT ファイルに基づいたコードを生成してくれるので、これを使ってコンポーネントの処理を実装します。
sample/src/lib.rs
wit_bindgen::generate!("sample");
struct Component;
impl Sample for Component {
// エクスポートする関数の実装
fn run() {
// インポート関数 log の呼び出し
log(LogLevel::Debug, "called run");
// インポート関数 getitem の呼び出し
let r = getitem("item-1");
log(LogLevel::Info, &format!("item: {:?}", r));
}
}
export_sample!(Component);
ビルドとコンポーネント作成
次のような Cargo.toml を使用しました。
sample/Cargo.toml
[package]
name = "sample"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wit-bindgen = "0.3"
[profile.release]
opt-level = "s"
lto = true
今回は WASI を使っていないので wasm32-unknown-unknown
でビルドします。
ビルド例
$ cargo build --release --target wasm32-unknown-unknown
ビルド結果の WebAssembly を wasm-tools component new
で処理してコンポーネントを作成します。
コンポーネント作成例
$ wasm-tools component new target/wasm32-unknown-unknown/release/sample.wasm -o sample-component.wasm
作成された sample-component.wasm
がコンポーネントとなります。
(b) コンポーネントの実行
作成したコンポーネントを実行してみます。
現時点では WebAssembly Component Model に対応したランタイムを使う必要があるようなので、今回は wasmtime を使いました。
wasmtime v6.0 では実行できなかったので github から取得(現時点では v7.0)するようにしました。
また、コンポーネントを実行するには features で component-model
を指定する必要がありました。
run_component/Cargo.toml
[package]
name = "run_component"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1"
wasmtime = { git = 'https://github.com/bytecodealliance/wasmtime', features = ['component-model'] }
wit_bindgen と同様に wasmtime::component::bindgen!
マクロで WIT ファイルに基づいたコードを生成してくれるので、それを使って実装します。
こちらもデフォルトでは ./wit
ディレクトリを参照するようになっていますが、in
を使って任意のディレクトリを指定できます。
コンポーネントを実行するには Config の wasm_component_model
を true に設定します。
run_component/src/main.rs
use anyhow::Result;
use wasmtime::component::{Component, Linker};
use wasmtime::{Config, Engine, Store};
use std::env;
wasmtime::component::bindgen!("sample" in "../sample/wit");
#[derive(Default)]
struct State {}
// コンポーネントがインポートする関数の実装
impl SampleImports for State {
fn log(&mut self, level: LogLevel, msg: String) -> anyhow::Result<()> {
println!("[{:?}] {}", level, msg);
Ok(())
}
fn getitem(&mut self, id: String,) -> wasmtime::Result<Item> {
Ok(Item { id: id.clone(), value: 123 })
}
}
fn main() -> Result<()> {
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 mut linker = Linker::new(&engine);
Sample::add_to_linker(&mut linker, |s: &mut State| s)?;
let mut store = Store::new(&engine, State::default());
let (bindings, _) = Sample::instantiate(&mut store, &component, &linker)?;
// コンポーネントの run 関数呼び出し
bindings.call_run(&mut store)?;
Ok(())
}
(a) で作成したコンポーネントを実行すると次のようになりました。
実行例
$ cargo run ../sample/sample-component.wasm
[LogLevel::Debug] called run
[LogLevel::Info] item: Item { id: "item-1", value: 123 }