TL;DR
cargo component new --libで、WIT に定義したインタフェースを実装するライブラリとしての WebAssembly Component を作れる。- ライブラリの Component は単体では実行できないので、Wasmtime の API や
bindgen!マクロを使って、ホスト側の Rust プログラムから呼び出す。
内容
プロジェクトの作成
cargo component new --lib greet を実行すると、第3章と異なり、wit/world.wit という WIT ファイルが生成される。src/lib.rs も cargo new --lib とは中身が違い、mod bindings と Guest トレイトの実装が書かれている。
bindings モジュールは、cargo component build の実行時に wit/world.wit から src/bindings.rs として生成される。その中に Guest トレイトが定義されており、これを実装すると、WIT の定義に沿ったライブラリとしての Component を作れる。
WIT の修正とインタフェースの実装
wit/world.wit を次のように書き換える。
package radish:greet;
interface greetable {
name: func() -> string;
greet: func(name: string) -> string;
}
world greetable-provider {
export greetable;
}もう一度 cargo component build を実行し、src/bindings.rs を作り直す。この時点ではビルドは失敗するが、気にしなくてよい。そのあと、src/lib.rs で Component 構造体に新しい Guest トレイトを実装する。
use crate::bindings::exports::radish::greet::greetable::Guest;
struct Component;
impl Guest for Component {
fn name() -> String {
"WebAssembly Component".to_string()
}
fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
}
bindings::export!(Component with_types_in bindings);cargo component build --target wasm32-unknown-unknown でビルドが通る。
Component を利用するプログラム
ライブラリとして作った Component は、他のプログラムの一部として実行される前提なので、Wasmtime の CLI から直接は実行できない。そこで、Component を呼び出すプログラム greet-user を作り、clap、anyhow、wasmtime を追加する。
呼び出しは次の手順で行う。
Component::from_fileで Wasm ファイルを読み込む。引数にはEngineを渡す- 読み込んだ
Componentは構文解析の結果にすぎず、そのままでは実行できない。Linkerを使って WebAssembly Instance を作る。Storeは作成した Instance と、その実行状態を管理する。Store::newの第 2 引数はStoreが持つデータの初期値で、今回は()を渡す Instanceのget_exportで、インタフェースや関数を指すComponentExportIndexを取得する。これは配列の添字やHashMapのキーのような役割を持つget_typed_funcで、関数への参照を持つTypedFuncを作るTypedFuncのcallで関数を呼び出す。関数は複数の値を返しうるので、戻り値はタプルになる。呼び出したあとは、戻り値の変換が終わったことを伝えるためにpost_returnを呼ぶ
fn start(args: Args) -> anyhow::Result<()> {
let engine = Engine::default();
let component = Component::from_file(&engine, &args.wasm_file)?;
let linker = Linker::new(&engine);
let mut store = Store::new(&engine, ());
let instance = linker.instantiate(&mut store, &component)?;
let greetable_index = instance
.get_export(&mut store, None, "radish:greet/greetable")
.unwrap();
let greet_index = instance
.get_export(&mut store, Some(&greetable_index), "greet")
.unwrap();
let greet: TypedFunc<(String,), (String,)> =
instance.get_typed_func(&mut store, greet_index).unwrap();
let (return_value,) = greet.call(&mut store, ("world!".to_string(),))?;
greet.post_return(&mut store)?;
println!("{return_value}");
Ok(())
}WIT から生成したコードを使う
関数を名前で探す代わりに、WIT ファイルからコードを生成して呼び出すこともできる。wasmtime::component::bindgen マクロは、ビルド時に WIT の World の定義から、Component をラップする構造体とメソッドを生成する。world には World の名前を、path には WIT ファイルのあるディレクトリを指定する。
bindgen!({
world: "greetable-provider",
path: "../greet/wit"
});World 名の構造体(ここでは GreetableProvider)の instantiate 関数で Instance を作る。{名前空間}_{パッケージ名}_{インタフェース名} のメソッド(ここでは radish_greet_greetable)でインタフェースのラッパーを取得し、call_{関数名} で関数を呼び出す。
let provider = GreetableProvider::instantiate(&mut store, &component, &linker)?;
let greetable = provider.radish_greet_greetable();
let message = greetable.call_greet(&mut store, "world")?;
println!("{message}");