Hello, world! with Wasm Component(合成編)
これまでのあらすじ:
-
第1回
- cargo-componentを使ってWebAssemblyコンポーネント(Wasmコンポーネント)を作りました
- Wasmtimeで実行しました
- wasm-toolsを使って作成したコンポーネントのワールドを出力しました
-
第2回
- cargo-componentを使ってライブラリーとして働くWasmコンポーネントを作成しました
- WebAssembly Interface Typeでライブラリーのインターフェースを定義し、Rustでインターフェースを実装しました
- Wasmコンポーネントレジストリーであるwa.devに、作成したコンポーネントを登録しました
-
第3回
- wit toolを使ってWITパッケージをレポジトリーに登録しました
- レポジトリーに公開されているWITパッケージをRustで実装しました
-
第4回
- 第3回で作成した
username:name/name-provider
に依存するWITパッケージを定義しました - 定義したワールドを実装するように、
hello-wasi-cli
を変更しました
- 第3回で作成した
第4回で作成したhello-wasi-cli
は、実行できません。依存するインターフェースの実装を持たないためです。
コンポーネントの合成とは
第3回と第4回で、合計4つのコンポーネントを作成しました:
username:name
username:hello-wasi
username:name-impl
username:hello-wasi-cli
最初の2つがWITパッケージで、後の2つがコンポーネントパッケージです。コンポーネントパッケージは、それぞれ対応するWITパッケージの実装となっています。この関係を図にすると、図1のようになります:
図1. 作成したコンポーネントの関係
あるインターフェースをインポートするのは、そのインターフェースで定義されている関数かデータ構造を内部で利用するためです。
username:hello-wasi
はusername:name/name-provider
をインポートしています。これはusername:hello-wasi
にはusername:name/name-provider
で定義されているname
関数を呼び出す処理があることを意味しています。別の言い方をすると、name
関数の実体が与えられない限り、username:hello-wasi
を実装したWasmコンポーネントを動かすことはできません。
依存する関数の実態は、インスタンス作成時に決定される
Wasmコンポーネントの提供する関数を実行するには、大まかにいって次の4ステップの処理を行います:
- ファイルを解析してコンポーネントオブジェクトを作成する
- コンポーネントオブジェクトがインポートするインターフェースの実装を用意する
- 1で用意したコンポーネントオブジェクトと、2で用意したインターフェースの実装を使って、コンポーネントのインスタンスを作成する
- インスタンスのエキスポートする関数を実行する
1から3ステップまでをwasmtimeクレートを使って書くと、次のようになります。
let name = Component::from_binary(&engine, &wasm_component_implementing_name)?;
let hello_wasi = Component::from_binary(&engine, &wasm_ocmponent_implementing_hello_wasi)?;
linker.instantiate_async(&mut store, &name).await?;
let hello_wasi_instance = linker.instantiate_async(&mut store, &hello_wasi).await?
インポートされるインターフェースの実体の決定(依存関係の解決と呼びます)は、Wasmコンポーネントのインスタンス化が行われるときに行われるのが基本です。依存性の注入(dependency injection)と似ているといえば、イメージしやすい方もいらっしゃるかもしれません。
合成:コンポーネント+コンポーネント -> 新しいコンポーネント
依存関係の解決をインスタンス化より前の時点で行うのが、コンポーネントの合成です。Wasmコンポーネントは内部に0個以上のコンポーネントを持つことができます。
component ::= (component <id>? <definition>*)
definition ::= core-prefix(<core:module>)
| core-prefix(<core:instance>)
| core-prefix(<core:type>)
| <component>
| <instance>
| <alias>
| <type>
| <canon>
| <start> 🪺
| <import>
| <export>
where core-prefix(X) parses '(' 'core' Y ')' when X parses '(' Y ')'
またコンポーネントには、各コンポーネントのインスタンス化方法を記述することもできます。
この2つを組み合わせることで、例えばusername:hello-wasi-cli
コンポーネントとusername:name-impl
コンポーネントから、次のようなコンポーネントを合成することできます:
- 内部に
username:hello-wasi-cli
コンポーネントのコードと、username:name-impl
コンポーネントのコードを保持する -
username:name-impl
のインスタンスをインポートに指定して、username:hello-wasi-cli
をインスタンス化する -
username:hello-wasi-cli
のエキスポートするインターフェースを、合成されたコンポーネントのエキスポートに設定する -
username:hello-wasi-cli
とusername:name-impl
のインポートのうち、username:name/name-provider
以外のインターフェースをインポートに設定する
ちょうど2つのジグソーパズルのピースをくっつけることで1つのピースとして扱えるようになるように、2つ以上のコンポーネントを合成することで1つのまとまったコンポーネントとして扱えるようになります。
図2. コンポーネントの合成イメージ
wasm-toolsを使った合成
2つのコンポーネントパッケージと、ソースコードの位置は次の表のようになっています:
パッケージID | ソースコードの位置 |
---|---|
usernaem:name-impl |
<プロジェクトフォルダ>/crates/name-impl |
usernaem:hello-wasi-cli |
<プロジェクトフォルダ>/ |
ビルド
それぞれのソースコードをビルドします。usernaem:name-impl
はwasi:cli
に依存していないので、wasm32-unknown-unknown
をターゲットにビルドします:
% pwd
/somewhere/hello-wasi-cli
# username-implのビルド
% cd crates/name-impl
% cargo component build -r --target wasm32-unknown-unknown
# hello-wasi-cliのビルド
% cd ../..
% cargo component build -r
_
を-
に置き換え
ビルドされたWasmコンポーネントは、次のパスに出力されます:
target/wasm32-unknown-unknown/release/name_impl.wasm
target/wasm32-wasi/release/hello-wasi-cli.wasm
この2つを合成する際、ファイル名に_
が含まれている場合、合成に失敗します。
error: `name_impl` is not in kebab case (at offset 0x0)
これを回避するために、name_impl.wasm
をコピーして、name-impl.wasm
を作成します:
% cp \
target/wasm32-unknown-unknown/release/name_impl.wasm \
target/wasm32-unknown-unknown/release/name-impl.wasm
wasm-tools compose
コマンド
合成にはwasm-toolsを利用します。次のようにcompose
コマンドを実行すると、2つのWasmコンポーネントを合成しcomposed.wasm
を作成します。
% wasm-tools compose target/wasm32-wasi/release/hello-wasi-cli.wasm \
-d target/wasm32-unknown-unknown/release/name-impl.wasm
-o target/wasm32-wasi/release/composed.wasm
composed component `target/wasm32-wasi/release/composed.wasm`
compose
コマンドは引数に指定したWasmコンポーネントと、-d
オプションで指定されたコンポーネントとを合成し、-o
オプションをで指定されたパスにWasmコンポーネントを出力します。
引数に指定したコンポーネントは、-d
オプションで指定されたコンポーネントに依存すると解釈されます。上記の実行例は、hello-wasi-cli.wasm
はname-impl.wasm
に依存していると解釈されます。
実行
username:hello-wasi-cli
を次のように実装したとします:
mod bindings;
use bindings::username::name::name_provider::name;
fn main() {
let n = name();
println!("Hello, {}!", n);
}
そしてusername:name-impl
を次のように実装したとします:
#[allow(warnings)]
mod bindings;
use bindings::exports::chikoski::name::name_provider::Guest;
struct Component;
impl Guest for Component {
fn name() -> String {
"world".to_string()
}
}
bindings::export!(Component with_types_in bindings);
この2つを合成したコンポーネントを実行した結果は次のようになります:
% wasmtime target/wasm32-wasi/release/composed.wasm
Hello, world!
まとめ
- Wasmコンポーネントを合成して、新しいコンポーネントを作成できます
- wasm-toolsを利用することで、コンポーネントを合成できます
- 合成したコンポーネントもwasmtimeで実行できます
第1回から今回までの内容を全て実装したコードはこちらでご覧いただけます。
Discussion
複数のcomponentをエイヤでガッチャンコするcomposeはwacに移行する動きがでてきました。
ありがとうございます。ファイル名の変更なんかもしなくていいので、そっちを使うように促すのが良いかもしれないですね。