🧩

Hello, world! with Wasm Component(合成編)

2024/05/01に公開2

これまでのあらすじ:

  • 第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を変更しました

第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-wasiusername:name/name-providerをインポートしています。これはusername:hello-wasiにはusername:name/name-providerで定義されているname関数を呼び出す処理があることを意味しています。別の言い方をすると、name関数の実体が与えられない限り、username:hello-wasiを実装したWasmコンポーネントを動かすことはできません。

依存する関数の実態は、インスタンス作成時に決定される

Wasmコンポーネントの提供する関数を実行するには、大まかにいって次の4ステップの処理を行います:

  1. ファイルを解析してコンポーネントオブジェクトを作成する
  2. コンポーネントオブジェクトがインポートするインターフェースの実装を用意する
  3. 1で用意したコンポーネントオブジェクトと、2で用意したインターフェースの実装を使って、コンポーネントのインスタンスを作成する
  4. インスタンスのエキスポートする関数を実行する

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-cliusername: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-implwasi: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.wasmname-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

chikoskichikoski

ありがとうございます。ファイル名の変更なんかもしなくていいので、そっちを使うように促すのが良いかもしれないですね。