😺

WASM Component Modelをまとめてみる

2023/12/13に公開

この記事はWebAssembly Advent Calendar 2023 13日目の記事です。


まえおき

本当はもっと発展した記事を書きたかったのですが、進捗ゼロに近いのでこのスクラップの焼き直しです。
https://zenn.dev/newgyu/scraps/b056627e219fc2

また、あれから2ヶ月ほどたって仕様策定も進んでいると思われるのでそのキャッチアップも試みます。

WASM Component Model is 何?

WASMはプログラミング言語非依存なバイナリーフォーマットで、Rustで書いたWASMモジュールをC#からも、Pythonからも利用するということが論理的には可能です。しかし、従来のWASM Core仕様で定義されるモジュール仕様はとてもプリミティブなもので、現実としては「Rustで書いたWASMモジュールをC#からも、Pythonからも利用する」というシナリオを実現するためには実装者がかなりの注意を払う必要がありました。

例えばCore仕様では文字列型、構造体などはサポートされておらずバイト配列として表現しなくてはなりません。そのため、WASMモジュールの利用者は構造体がどのようにバイト配列化されているかを知った上でPythonなりC#なりの構造体にマッピングするコードを書く必要があります。

このような痒いところに手が届いていなかった部分を補完する目的でCore仕様を拡張したものがComponentモデルとなります。

主な特徴

端的にはコンポーネントモデルではCore WASM仕様と違って以下の特徴があります。

  • モジュールと言う単位とは別に、新たにコンポーネントという単位が定義される
  • コンポーネントはモジュールとは違ったバイナリフォーマットであり、コンポーネントモデルに対応したツールとランタイムが必要になる
  • 整数、浮動小数点などの単純なデータ型だけではなく文字列や構造体などの複雑な型の仕様が定義される
  • それらの複雑な方を線形メモリで表現するための変換仕様が定義される

コンポーネント

その名が指す通りCore WASM仕様でのモジュールと言う単位とは別に新たにコンポーネントという単位が定義されます。 見かけは同じ*.wasmファイルになりますが、モジュールとは異なる定義のバイナリファイルになります。

バイナリ定義上の大きな違いはまず、versionの値が異なります。

Core WASMのバイナリ仕様

module ::= magic
           version
	   customsec*
	   :
magic     ::= 0x00 0x61 0x73 0x6D
version   ::= 0x01 0x00 0x00 0x00

Component Modelのバイナリ仕様

component ::= <preamble> s*:<section>*            => (component flatten(s*))
preamble  ::= <magic> <version> <layer>
magic     ::= 0x00 0x61 0x73 0x6D
version   ::= 0x0a 0x00
layer     ::= 0x01 0x00

WAT(WebAssembly Text Format)形式にデコードして比べてみるとわかりやすいと思います。ともに2つの数を足し算するadd関数がexportされるもの(正確に全く同じでは無いのであくまで雰囲気として)ですが、ぱっと目につくところだけでも以下のような違いが見て取れます。

  • まず(module...で始まるのか、(component...で始まるのかという大きな違いがある
  • core modulecore-instancealiasなど見慣れないキーワードがある

Core WASMのWAT仕様

core wasm
(module
  (func $addTwoNumbers (param $a i32) (param $b i32) (result i32)
    get_local $a
    get_local $b
    i32.add)
  (export "add" (func $addTwoNumbers))
)

Component ModelのWAT仕様

component wasm
(component
  (core module (;0;) ...省略... )
  (core instance (;0;) (instantiate 0))
  (alias core export 0 "memory" (core memory (;0;)))
  (alias core export 0 "cabi_realloc" (core func (;0;)))
  (type (;0;) (func (param "a" u32) (param "b" u32) (result u32)))
  (alias core export 0 "docs:calculator/add@0.1.0#add" (core func (;1;)))
  (func (;0;) (type 0) (canon lift (core func 1)))
  (component (;0;)
    (type (;0;) (func (param "a" u32) (param "b" u32) (result u32)))
    (import "import-func-add" (func (;0;) (type 0)))
    (type (;1;) (func (param "a" u32) (param "b" u32) (result u32)))
    (export (;1;) "add" (func 0) (func (type 1)))
  )
  (instance (;0;) (instantiate 0
      (with "import-func-add" (func 0))
    )
  )
  (export (;1;) "docs:calculator/add@0.1.0" (instance 0))
)

この様にコンポーネントモデル仕様に従ったwasmファイルは従来のシンプルなCore WASMのモジュールとはフォーマットが大きく異なります。そのためコンポーネントとしてアセンブルされたwasmファイルを実行するためにはコンポーネントモデルに対応したランタイムが必要になります。

最もメジャーなwasmtimeでは13.0.0からコンポーネントモデルをサポートしていますが、まだ明示的にフラグを指定する必要があります。

$ wasmtime run --wasm component-model xxxx.wasm
               ^^^^^^^^^^^^^^^^^^^^^^

インターフェースとWIT

Core WASM仕様にはモジュールインターフェースを示すものとしてimportとexportという概念があります。下記の例は、

  • externalFunctionをインポートし、
  • それを利用するinternalFunctionをエクスポートする

例です。

Core WASMのexport/import
(module
  ;; 外部環境から関数をインポート
  (import "env" "externalFunction" (func $externalFunction (param i32)))

  ;; モジュール内の関数を定義し、この関数内でインポートされた関数を呼び出す
  (func $internalFunction (export "internalFunction")(param i32)(result i32)
    local.get 0
    call $externalFunction
  )
)

Core WASMではモジュールのインターフェースをWATから読み取る必要がありました。この例は単純なのですが、実際のWATファイルは関数本体の実装部分が大半となり、インターフェースだけを読み取るには少々労力が必要かもしれません。

一方ほぼ同様の内容をコンポーネントモデルで導入されたWITという形式で書くと以下のようになります。

witの例
package newgyu:example;

interface env {
    external-function: func(param: s32) -> s32;
}

world my-world {
    // envインターフェースをインポート
    import env;
    // モジュール内の関数をエクスポート
    export internal-function: func(param: s32) -> s32;
}

WITはWATとは違ってインターフェースを記述する言語(IDL)なので関数本体の実装は含まれません。

  • コンポーネントがどんな関数を提供(export)するか
  • 逆にそのコンポーネントが必要とするもの(import)は何か

ということだけが記述されています。

上記の例は非常にシンプルな符号付き整数型しか使っていないので正直WATだけで不便は感じないかもしれません。しかし、paramの型がもっと複雑な構造体だった場合にはどうでしょうか? Core WASMでは構造体型というのは無いので線形メモリを使ってバイト配列をやり取りせねばなりません。構造体をどの様にメモリ上のバイト配列にマッピングして書き込むべきかは実装なり、モジュール提供者が準備するであろう別のドキュメントを参照して理解せざるを得ません。
コンポーネントモデルではその不便さを解消するために様々な型をサポートしています。
https://component-model.bytecodealliance.org/design/wit.html#built-in-types
このドキュメントを見ていただくとわかりますが、

  • bool, char, stringなどC言語でおなじみのもの
  • list<T>, option<T>, result<T,E>などジェネリクスに対応した型

などと一般的なプログラミング言語と比べて遜色の無いくらいの種類の型が使用可能です。

複雑な型を使った例
package newgyu:example;

interface env {
    enum colors {
        red,
        green,
        blue,
    }
    record pixel {
        x: s32,
        y: s32,
        color: colors,
    }
    external-function: func(p: pixel) -> s32;
}

world my-world {
    // envインターフェースで定義されたcolorsとpixelをuseする
    use env.{colors, pixel};
    // envインターフェースをインポート
    import env;
    // モジュール内の関数をエクスポート
    export internal-function: func(pixels: list<pixel>) -> s32;
}

Cannonical ABI

https://component-model.bytecodealliance.org/design/canonical-abi.html
Cannonical ABIは先に述べたWITによる豊富なデータ型をバイナリでどのような表現をするかを定義するものです。

この仕様については読み解きが難しく私自身がきちんと説明できるレベルに至っていません。言い訳かもしれませんがこのCannonical ABIの仕様をコンポーネントの実装者や利用者が詳細に理解する必要はないと思われます。なぜなら、後で述べるようにコンポーネントモデルをサポートするランタイムや周辺ツールがこのCannnical ABI仕様に沿ったアダプターを実装してくれて、コンポーネントの実装者や利用者が言語固有の型とそのバイナリ表現の変換をすることはまずないと考えられるからです。

とはいえこれだけではイメージも掴めないので最低限の説明努力はしてみます。
component-modelリポジトリのCannonicalABI.mdではPythonコードを用いてCannonicalABIを説明しています。例えば下記はrecord型(要するに構造体)を線形メモリからPython上の連想配列としてロードする例です。

@dataclass
class Record(ValType):
  fields: [Field]

@dataclass
class Field:
  label: str
  t: ValType

def load_record(cx, ptr, fields):
  record = {}
  for field in fields:
    ptr = align_to(ptr, alignment(field.t))
    record[field.label] = load(cx, ptr, field.t)
    ptr += size(field.t)
  return record

この様にrecordの各フィールドの型情報からバイト配列中の位置と読み取り要素数を指定して値をロードし、フィールド名(label)と紐づけて値を格納しています。

Componentの利用例

コンポーネントモデルは未だ策定中の仕様でまだまだ実用できるものではありませんが、仕様策定を主導しているBytecode Allianceは様々なツールの参照実装を公開しています。

このセクションではチュートリアルに沿って、以下のツールチェーンがどの様に使われるのかを紹介していきたいと思います。

  • cargo-component
  • wasm-tools
  • wasmtime

前置きとしてチュートリアルの構成

https://github.com/bytecodealliance/component-docs/tree/main/component-model/examples/tutorial
チュートリアルはRust言語で記載されています。コンポーネントモデルWASMは言語非依存なのでRust以外の言語でも書いたり実行したりすることはできますが、現時点でBytecode Allianceが提供しているのはまだRust用のツール参照実装だけです。コンポーネントモデルの仕様策定自体がまだ現在進行系なので他(多)言語のツールチェーンが実装されるにはまだ時間がかかると思っておいたほうが良いでしょう。

チュートリアルでは3つのadder, calclator, appコンポーネントがあります。(WIT中のworldが一つのコンポーネントを表すものと考えてください)
https://github.com/bytecodealliance/component-docs/blob/main/component-model/examples/tutorial/wit/calculator.wit

図にするとこうなります。

  • adderコンポーネント
    • 別コンポーネントへの依存はなく、純粋にaddインターフェースエクスポートするのみ
  • calculatorコンポーネント
    • addインターフェースの実装(つまりadderコンポーネント)に依存
    • 自身はcalulateインターフェースをexportして外部に提供する
  • commandコンポーネント(world名はappだが)
    • calculateインターフェースの実装に依存
    • calculateを使用してコマンドライン実行可能なコンポーネント

2023/12/12時点のチュートリアルに関する注意点

これは読み飛ばしていただいても構いませんが、実際にチュートリアルを実行する場合にはこのチュートリアルはエラーを吐くと思われます。

error: failed to create a target world for package `adder` (/workspaces/component-docs/component-model/examples/tutorial/adder/Cargo.toml)

Caused by:
    0: failed to parse local target `/workspaces/component-docs/component-model/examples/tutorial/adder/../wit/calculator.wit`
    1: expected ';', found keyword `interface`
            --> /workspaces/component-docs/component-model/examples/tutorial/adder/../wit/calculator.wit:3:1
             |
           3 | interface calculate {
             | ^

これはWITの仕様に行末に;を必須とするという破壊的変更があったためです。
https://github.com/WebAssembly/component-model/pull/249

cargo-componentは0.5.0からこの仕様に対応していますが、肝心のチュートリアルのサンプルコードのWITには;がついていないためエラーになります。

また、cargo-component-bindingsに関する破壊的変更もありCargo.lockファイルを更新するひつようがあります。詳細はこちらのPRを参照してください。

コンポーネントを作成する

さて、コンポーネントを作成するために最初に活躍するツールがcargo-componentです。
https://github.com/bytecodealliance/cargo-component
cargo-componentはRustでコンポーネントWASMをビルドするためのツールで、大きく分けて2つの機能があります。

  • cargoのcargo component buildサブコマンド
  • WITファイルからRustのソースを自動生成するcargo_component_bindingsマクロ

このツールを使ってチュートリアルに沿って以下のことをなぞって行きたいと思います。

  1. 再利用可能なライブラリコンポーネントとしてadder.wasmを作る
  2. adder.wasmを利用する他のコンポーネントへの依存関係を持つコンポーネントcalculator.wasmを作る
  3. 最後に実行可能形式のコンポーネントcommand.wasmを作ってwasmtimeで実行する

再利用可能なadderコンポーネントを作る

まず純粋に利用される側であるadderコンポーネントを作成する過程を見てみます。ソースコードはtutolial/adderを参照してください。

witからgenerateマクロによってある程度のコードが生成される

まず注目すべきはwitファイルのadder worldです。

adderコンポーネントに関係あるところの抜粋
package docs:calculator@0.1.0;

interface add {
    add: func(a: u32, b: u32) -> u32;
}

world adder {
    export add;
}

adderコンポーネントはaddインターフェースがエクスポートしており、それは外部からadd関数が利用可能ということを意味します。そのために、コンポーネント実装者はaddインターフェースを実装しadd関数の中身を書いてやる必要があります。

そのコードは次のようになっています。
https://github.com/bytecodealliance/component-docs/blob/main/component-model/examples/tutorial/adder/src/lib.rs
ここで、cargo_component_bindings::generate!というマクロが活躍し、bindings::...のモジュールのコードを自動生成してくれます。 マクロが生成するコードの詳細はcargo expandコマンドで確認できるのでここでは省略しますが、Guestトレイトが生成されるのがキモになってきます。

pub trait Guest {
    fn add(a: u32, b: u32) -> u32;
}

前述の

コンポーネント実装者はaddインターフェースを実装しadd関数の中身を書いてやる必要があります。

というのはこのトレイトを実装するということになります。かなり直感的ではないでしょうか。このチュートリアルでは非常にシンプルな型しか扱っていないのでありがたみが伝わりにくいですが、add関数の引数がrecord型だったり、enum型だったりする場合にはgenerateマクロはそれらのRustコードを生成してくれるのでありがたみを感じられるでしょう。

IDLから各言語向けのコードスケルトンを生成するというのは今までにもよくあったパターンですが、コンポーネントモデルのWITはそれを可能にするもので、その実装の一つがcargo-componentのgenerateマクロというわけです。

コンポーネントをビルドする

Rustに慣れた方であれば書いたコードをcargo buildコマンドを使ってビルドすることに慣れていると思いますが、コンポーネントWASMをビルドするのはほとんどそれと同じです。

$ cargo component build --release
   Compiling adder v0.1.0 (/workspaces/component-docs/component-model/examples/tutorial/adder)
    Finished release [optimized] target(s) in 0.06s
    Creating component /workspaces/component-docs/component-model/examples/tutorial/adder/target/wasm32-wasi/release/adder.wasm

とても簡単ですね。

生成されたwasmファイルのWATを確認してみる

ここでwasmに関する低レベルな操作を提供する便利ツールwasm-toolsが登場します。
https://github.com/bytecodealliance/wasm-tools

$ wasm-tools print ./target/wasm32-wasi/release/adder.wasm | less
(component
  (core module (;0;)
    (type (;0;) (func))
    (type (;1;) (func (param i32 i32) (result i32)))
    (type (;2;) (func (param i32 i32 i32 i32) (result i32)))
    :

コンポーネントモデルの仕様に沿ったWASMになっているようです。

他のコンポーネントをに依存するcalclatorコンポーネント

今度はtutorial/calculatorを見てみましょう。

先のadderコンポーネントには他のコンポーネントへの依存はありませんでしたが、calclatorコンポーネントはaddインターフェースを必要(import)しています。

calclatorコンポーネントに関係あるところの抜粋
package docs:calculator@0.1.0;

interface calculate {
    enum op {
        add,
    }
    eval-expression: func(op: op, x: u32, y: u32) -> u32;
}

interface add {
    add: func(a: u32, b: u32) -> u32;
}

world calculator {
    export calculate;
    import add;
}

このケースにおいて注目すべきはgenerateマクロがbindings::docs::calculator::add::addをモックとして生成してくれるところにあります。

https://github.com/bytecodealliance/component-docs/blob/main/component-model/examples/tutorial/calculator/src/lib.rs

このadd関数の実装は別のコンポーネント(つまりadderコンポーネント)によって提供されますが、calculatorコンポーネントとしてはインターフェースだけ分かればビルドを通すことができます。generateマクロはWIT定義をもとに関数インターフェース生成して提供してくれます。

ちなみにこの例ではcalclateインターフェースに含まれるop列挙型がRustの列挙型として生成されているのも確認できます。

コンポーネントを実行する

前項ではコンポーネントを作る手順をなぞってきましたが、作られたコンポーネントを実行するためにはどうしたら良いのでしょうか? 現時点では2つの方法があります。

  1. wasmtimeのようなWASIランタイムを使ってコマンドアプリケーションとして実行する
  2. ホスト言語内部のwasmランタイムを通じて実行する

何れにせよ現時点でコンポーネントモデルをサポートしているのはwasmtimeだけなので両方ともwasmtimeに依存します。

wasmtime CLIを用いたコンポーネントの実行

https://wasmtime.dev/

実行可能な形式のコンポーネントWASMを作る

さて、先にビルドしたadder.wasm, calclator.wasmは再利用可能なライブラリとしてのコンポーネントであって実行可能なコンポーネントではありません。コンポーネントには実行可能(command)コンポーネントとライブラリ(reactor)コンポーネントの2種類があり、その様子はcargo-componentで新規プロジェクトをnewするときのオプションからうかがい知ることができます。

 $ cargo component new --help
Create a new WebAssembly component package at <path>

Usage: cargo component new [OPTIONS] <path>

Arguments:
  <path>  The path for the generated package

Options:
      --command                Create a command component [default]
      --reactor                Create a reactor component

tutorial/commandが実行可能コンポーネントの例です。

実行可能(command)コンポーネントのプロジェクトは以下のような点がライブラリ(reactor)コンポーネントのプロジェクトとは異なります。

  • src/main.rs ファイルを持つ(src/lib.rsではなく)
  • bin.rsにはmain関数があり、main関数がコマンド実行時のエントリポイントとなる

Rustのバイナリクレートと似たような考え方だと理解すると良いでしょう。

https://github.com/bytecodealliance/component-docs/blob/main/component-model/examples/tutorial/command/src/main.rs

Commandコンポーネントのソースを理解するにはコマンドライン処理をサポートしてくれる[Clapクレート]に関する知識が必要になりますが、本質的なコードとしては下記のCommand#run理解するだけで十分です。

impl Command {
    fn run(self) {
        let res = calculate::eval_expression(self.op, self.x, self.y);
        println!("{} {} {} = {res}", self.x, self.op, self.y);
        // Sleep because bug
        sleep(std::time::Duration::from_millis(10))
    }
}

上記のコードではcalculate::eval_expression関数を呼び出して、その結果をprintlnで表示しているだけです。calculate::eval_expression関数はWITでimport宣言されている通り、commandコンポーネント自身はその実装を持たず外部(calculatorコンポーネント)に依存しています。

ランタイムのオプションでコンポーネントモデルを有効化する

さて、このcommandコンポーネントをビルドして実行してみましょう。おそらく次のようなエラーが出ることになります。

$ wasmtime command/target/wasm32-wasi/release/command.wasm 
Error: cannot execute a component without `--wasm component-model`

wasmtimeは13.0.0からコンポーネントモデルをサポートしていますが、デフォルトではまだWASI Preview1のモードで動きます。コンポーネントWASMを動かすにはメッセージどおりに --wasm component-model を指定する必要があります。

外部依存をcomposeする

さて気を取り直して--wasm component-modelを指定して実行してみましょう。

 $ wasmtime --wasm component-model command/target/wasm32-wasi/release/command.wasm 
Error: failed to run main module `command/target/wasm32-wasi/release/command.wasm`

Caused by:
    0: import `docs:calculator/calculate@0.1.0` has the wrong type
    1: instance export `eval-expression` has the wrong type
    2: expected func found nothing

今度はcommand/target/wasm32-wasi/release/command.wasmの中身について文句を言っているようです。
知識のない状態でこのメッセージを理解するのは難しいのですが、「calculateインターフェースとeval-expression関数がおかしい。関数が見つからない」ということを言っています。
wit定義の下記の部分ですね。

package docs:calculator@0.1.0;

interface calculate {
    enum op {
        add,
    }
    eval-expression: func(op: op, x: u32, y: u32) -> u32;
}

world app {
    import calculate;
}

このエラーメッセージが言わんとすることは「eva-expression関数の実装がない」ということです。cargo_component_bindings::generateマクロはimportされたインターフェースのモックを生成してビルドできるようにはくれますが、それはあくまでインターフェース部分だけなので、calculatorコンポーネントに含まれているeval-expression関数の実装をどうにかして繋いでやる必要があります。C言語を扱った事がある人にはわかるかもしれませんが、ヘッダーファイルがあればコンパイルはできるが依存ライブラリをリンクしてあげないと実行時にエラーになるのと全く同じ状況です。

と言う訳で、command.wasmにcalclator.wasmをリンクする必要があり、その方法がcomposeであるということになります。

さて、依存ライブラリのリンク方法には静的リンクと動的リンクの二種類の方法がありますが、composeは前者の静的リンクに等しいものです。つまり依存ライブラリを取り込んで一つのWASMバイナリファイルにエイヤでガッチャンコする方式です。
そして、そのためにwasm-toolsという補助ツールが提供する昨日の一つであるcomposeサブコマンドが用いられます。
https://github.com/bytecodealliance/wasm-tools

 $ wasm-tools compose --help
WebAssembly component composer.

A tool for composing WebAssembly components together.

Usage: wasm-tools compose [OPTIONS] <COMPONENT>

Arguments:
  <COMPONENT>
          The path to the root component to compose

Options:
  -d, --definitions <DEFS>
          Definition components whose exports define import dependencies to fulfill from
  -o, --output <OUTPUT>
          Where to place output.
          If not provided then stdout is used.

実はこのことについてはtutorial/README.mdにしれっと書いてあります。

$ wasm-tools compose calculator/target/wasm32-wasi/release/calculator.wasm -d adder/target/wasm32-wasi/release/adder.wasm -o composed.wasm
$ wasm-tools compose command/target/wasm32-wasi/release/command.wasm -d composed.wasm -o command.wasm

command,calculator,adderという3つのコンポーネント間に推移的な依存関係があることを思い出すと上記の2つのコマンドの意味はわかると思います。

  1. calculator.wasm --> adder.wasmの依存関係をリンクするために2つを合成してcomposed.wasmを生成します
  2. 次にcommand.wasmとcomposed.wasmを合成することで上記3つの推移的な依存が一つのファイルにまとまります。

そしてようやく以下の結果が得られることでしょう。

$ wasmtime --wasm component-model ./command.wasm 1 2 add
1 + 2 = 3

Deep Dive 実行可能コンポーネントとはどういうことか?

コンポーネントには実行可能(command)コンポーネントとライブラリ(reactor)コンポーネントの2種類が

この部分を少し補足したいと思います。

チュートリアルどおりにことを進めたのであればあなたのtutorialディレクトリには2つのwasmファイルが有ることでしょう。

$ tree -L 1
.
├── adder
├── calculator
├── command
├── command.wasm
├── composed.wasm
├── README.md
└── wit

それぞれのコンポーネントWASMに対してwasm-tools component witサブコマンドを使ってみると面白いことがわかると思います。

$ wasm-tools component wit ./composed.wasm 
package root:component;

world root {
  export docs:calculator/calculate@0.1.0;
}

こちらは概ね予想の範疇だと思います。package名やworld名がrootに変わっているという奇妙な点は有りますがひとまず無視すると、もともとのcalculatorコンポーネントのwitとほぼ同じです。

$ wasm-tools component wit ./command.wasm 
package root:component;

world root {
  import wasi:clocks/wall-clock@0.2.0-rc-2023-11-10;
  import wasi:io/poll@0.2.0-rc-2023-11-10;
  import wasi:clocks/monotonic-clock@0.2.0-rc-2023-11-10;
  import wasi:io/error@0.2.0-rc-2023-11-10;
  import wasi:io/streams@0.2.0-rc-2023-11-10;
  import wasi:filesystem/types@0.2.0-rc-2023-11-10;
  import wasi:filesystem/preopens@0.2.0-rc-2023-11-10;
  import wasi:sockets/tcp@0.2.0-rc-2023-11-10;
  import wasi:cli/environment@0.2.0-rc-2023-11-10;
  import wasi:cli/exit@0.2.0-rc-2023-11-10;
  import wasi:cli/stdin@0.2.0-rc-2023-11-10;
  import wasi:cli/stdout@0.2.0-rc-2023-11-10;
  import wasi:cli/stderr@0.2.0-rc-2023-11-10;
  import wasi:cli/terminal-input@0.2.0-rc-2023-11-10;
  import wasi:cli/terminal-output@0.2.0-rc-2023-11-10;
  import wasi:cli/terminal-stdin@0.2.0-rc-2023-11-10;
  import wasi:cli/terminal-stdout@0.2.0-rc-2023-11-10;
  import wasi:cli/terminal-stderr@0.2.0-rc-2023-11-10;

  export wasi:cli/run@0.2.0-rc-2023-11-10;
}

こちらはwitに書いた覚えもないようなインターフェースが列挙されていることに戸惑うと思いますが、wasi:cliというパッケージ名からピンと来る方もいるかも知れません。そうこれは現在仕様策定が進んでいるWASIのCLIとしての要件を示すWITです。
https://github.com/WebAssembly/wasi-cli

cargo-component buildでは main.rsとmain関数を含むプロジェクトをwasi:cliの要件に準拠した形でビルドします。
https://github.com/WebAssembly/wasi-cli/blob/main/wit/command.wit
https://github.com/WebAssembly/wasi-cli/blob/main/wit/run.wit

wasmtime --wasm component-modelではwasi:cli/runインターフェースに定められるrun関数を実行します。

2. wasmtime crateを用いたコンポーネントの実行

先のtutorialでは実行可能なコンポーネントWASMを生成してwasmtimeを用いてコマンドラインで実行する体験をしましたが、WASMの醍醐味として様々なホスト言語にembed(埋め込み)して実行するということがあります。

examples/example-hostにはRustからWASMをライブラリ的に利用する例が示されています。
(Rustで作ったコンポーネントWASMをRustで動かしているので嬉しさがイマイチ伝わりにくいかもしれませんが、今はRustの例しか無いので勘弁してください)

まずはCargo.tomlのdependenciesとして書かれているwasmtimeに注目です。
https://github.com/bytecodealliance/component-docs/blob/main/component-model/examples/example-host/Cargo.toml
wasmtimeは上で触れた様なCLIツールもありますが、Rustから利用できるクレート形式のものもあります。自身のRustコードでこのwasmtimeクレートを利用してコンポーネントを実行します。
https://crates.io/crates/wasmtime

次に実際のコードです。
https://github.com/bytecodealliance/component-docs/blob/main/component-model/examples/example-host/src/add.rs
ここではpub async fn add(path: PathBuf, x: i32, y: i32) -> wasmtime::Result<i32>の実装として内部で以下の事が行われています。

  1. wasmtimeクレートを使って動的にコンポーネントWASMファイルをロード
let component = Component::from_file(&engine, path).context("Component file not found")?;
  1. インスタンス化
 let (instance, _) = Example::instantiate_async(&mut store, &component, &linker)
      .await
      .context("Failed to instantiate the example world")?;
  1. コンポーネントがexportするadd関数を呼ぶ
instance
      .call_add(&mut store, x, y)
      .await
      .context("Failed to call add function")

この実装だと毎回ロードとインスタンス化が行われるオーバーヘッドがあるのはご愛嬌です。例としてわかりやすさを優先したのでしょう。

さて実行してみましょう。このコードは普通のRustプロジェクトですので、cargo componentサブコマンドでなくcargo runで実行可能です。

$ cargo run 3 5 ./add.wasm 
    Finished dev [unoptimized + debuginfo] target(s) in 0.14s
     Running `target/debug/example-host 3 5 ./add.wasm`
3 + 5 = 8

コンポーネントレジストリ

最後にWARGの話を書いて終わろうと思います。
https://warg.io/

コンポーネントモデルの目指すゴールは「プログラミング言語を超えたコンポーネントの相互利用」です。WITのキーワードの中にpackageというワードが出ているからも分かる通り、コンポーネントはNPMやCrateのようにリモートレジストリに登録され、package.jsonやCargo.tomlに依存を書いて利用できることが計画されています。

cargo-componentはCargo.tomlに依存関係を解決する方法を先取り実装し始めています。
https://github.com/bytecodealliance/cargo-component/blob/main/docs/design/registries.md

この件について私は多くを語れるほどの知識がないので逃げるようにこの章を終わらせます。

おわりに

本記事では現在進行系で策定中のWASM Component Modelについて書き殴りました。予想より長くなってしまいあまり、読みやすい記事とは思えませんが最後まで読んでくださった方ありがとうございます。

最後に簡単なまとめを記載して終わります。

  • コンポーネントモデルでは"コンポーネント"という新しいWASMのバイナリフォーマットが定義された
  • コンポーネントはCore WASMに比べてリッチな型(EnumやStructやResultなど)を使用することができる
  • コンポーネントは言語非依存である。例えばRustで書いたコンポーネントをPythonやC#で使うことが可能
  • コンポーネントモデルをサポートするWASMランタイムはまだごく一部(多分wasmtimeだけ)で、オプション扱いである
  • コンポーネントはWASMであるため様々な言語に埋め込んで(embeded)利用が可能
  • コンポーネントはNPMやCrateのようにリモートレジストリからダウンロードして使う未来が想定されている

Discussion