WASM Component Modelをまとめてみる
この記事はWebAssembly Advent Calendar 2023 13日目の記事です。
まえおき
本当はもっと発展した記事を書きたかったのですが、進捗ゼロに近いのでこのスクラップの焼き直しです。
また、あれから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の値が異なります。
module ::= magic
version
customsec*
:
magic ::= 0x00 0x61 0x73 0x6D
version ::= 0x01 0x00 0x00 0x00
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 module
やcore-instance
、alias
など見慣れないキーワードがある
(module
(func $addTwoNumbers (param $a i32) (param $b i32) (result i32)
get_local $a
get_local $b
i32.add)
(export "add" (func $addTwoNumbers))
)
(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をエクスポートする
例です。
(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という形式で書くと以下のようになります。
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では構造体型というのは無いので線形メモリを使ってバイト配列をやり取りせねばなりません。構造体をどの様にメモリ上のバイト配列にマッピングして書き込むべきかは実装なり、モジュール提供者が準備するであろう別のドキュメントを参照して理解せざるを得ません。
コンポーネントモデルではその不便さを解消するために様々な型をサポートしています。
このドキュメントを見ていただくとわかりますが、
- 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
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
前置きとしてチュートリアルの構成
チュートリアルはRust言語で記載されています。コンポーネントモデルWASMは言語非依存なのでRust以外の言語でも書いたり実行したりすることはできますが、現時点でBytecode Allianceが提供しているのはまだRust用のツール参照実装だけです。コンポーネントモデルの仕様策定自体がまだ現在進行系なので他(多)言語のツールチェーンが実装されるにはまだ時間がかかると思っておいたほうが良いでしょう。
チュートリアルでは3つのadder, calclator, appコンポーネントがあります。(WIT中のworldが一つのコンポーネントを表すものと考えてください)
図にするとこうなります。
- 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の仕様に行末に;
を必須とするという破壊的変更があったためです。
cargo-componentは0.5.0からこの仕様に対応していますが、肝心のチュートリアルのサンプルコードのWITには;
がついていないためエラーになります。
また、cargo-component-bindingsに関する破壊的変更もありCargo.lockファイルを更新するひつようがあります。詳細はこちらのPRを参照してください。
コンポーネントを作成する
さて、コンポーネントを作成するために最初に活躍するツールがcargo-componentです。
cargo-componentはRustでコンポーネントWASMをビルドするためのツールで、大きく分けて2つの機能があります。- cargoの
cargo component build
サブコマンド - WITファイルからRustのソースを自動生成する
cargo_component_bindings
マクロ
このツールを使ってチュートリアルに沿って以下のことをなぞって行きたいと思います。
- 再利用可能なライブラリコンポーネントとしてadder.wasmを作る
- adder.wasmを利用する他のコンポーネントへの依存関係を持つコンポーネントcalculator.wasmを作る
- 最後に実行可能形式のコンポーネントcommand.wasmを作ってwasmtimeで実行する
再利用可能なadderコンポーネントを作る
まず純粋に利用される側であるadderコンポーネントを作成する過程を見てみます。ソースコードはtutolial/adderを参照してください。
witからgenerateマクロによってある程度のコードが生成される
まず注目すべきはwitファイルのadder worldです。
package docs:calculator@0.1.0;
interface add {
add: func(a: u32, b: u32) -> u32;
}
world adder {
export add;
}
adderコンポーネントはaddインターフェースがエクスポートしており、それは外部からadd関数が利用可能ということを意味します。そのために、コンポーネント実装者はaddインターフェースを実装しadd関数の中身を書いてやる必要があります。
そのコードは次のようになっています。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が登場します。
$ 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)しています。
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
をモックとして生成してくれるところにあります。
このadd関数の実装は別のコンポーネント(つまりadderコンポーネント)によって提供されますが、calculatorコンポーネントとしてはインターフェースだけ分かればビルドを通すことができます。generateマクロはWIT定義をもとに関数インターフェース生成して提供してくれます。
ちなみにこの例ではcalclateインターフェースに含まれるop列挙型がRustの列挙型として生成されているのも確認できます。
コンポーネントを実行する
前項ではコンポーネントを作る手順をなぞってきましたが、作られたコンポーネントを実行するためにはどうしたら良いのでしょうか? 現時点では2つの方法があります。
- wasmtimeのようなWASIランタイムを使ってコマンドアプリケーションとして実行する
- ホスト言語内部のwasmランタイムを通じて実行する
何れにせよ現時点でコンポーネントモデルをサポートしているのはwasmtimeだけなので両方ともwasmtimeに依存します。
wasmtime CLIを用いたコンポーネントの実行
実行可能な形式のコンポーネント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のバイナリクレートと似たような考え方だと理解すると良いでしょう。
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サブコマンドが用いられます。
$ 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つのコマンドの意味はわかると思います。
-
calculator.wasm --> adder.wasm
の依存関係をリンクするために2つを合成してcomposed.wasm
を生成します - 次に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です。
cargo-component buildでは main.rsとmain関数を含むプロジェクトをwasi:cliの要件に準拠した形でビルドします。
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に注目です。
wasmtimeは上で触れた様なCLIツールもありますが、Rustから利用できるクレート形式のものもあります。自身のRustコードでこのwasmtimeクレートを利用してコンポーネントを実行します。次に実際のコードです。pub async fn add(path: PathBuf, x: i32, y: i32) -> wasmtime::Result<i32>
の実装として内部で以下の事が行われています。
- wasmtimeクレートを使って動的にコンポーネントWASMファイルをロード
let component = Component::from_file(&engine, path).context("Component file not found")?;
- インスタンス化
let (instance, _) = Example::instantiate_async(&mut store, &component, &linker)
.await
.context("Failed to instantiate the example world")?;
- コンポーネントが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の話を書いて終わろうと思います。
コンポーネントモデルの目指すゴールは「プログラミング言語を超えたコンポーネントの相互利用」です。WITのキーワードの中にpackageというワードが出ているからも分かる通り、コンポーネントはNPMやCrateのようにリモートレジストリに登録され、package.jsonやCargo.tomlに依存を書いて利用できることが計画されています。
cargo-componentはCargo.tomlに依存関係を解決する方法を先取り実装し始めています。
この件について私は多くを語れるほどの知識がないので逃げるようにこの章を終わらせます。
おわりに
本記事では現在進行系で策定中のWASM Component Modelについて書き殴りました。予想より長くなってしまいあまり、読みやすい記事とは思えませんが最後まで読んでくださった方ありがとうございます。
最後に簡単なまとめを記載して終わります。
- コンポーネントモデルでは"コンポーネント"という新しいWASMのバイナリフォーマットが定義された
- コンポーネントはCore WASMに比べてリッチな型(EnumやStructやResultなど)を使用することができる
- コンポーネントは言語非依存である。例えばRustで書いたコンポーネントをPythonやC#で使うことが可能
- コンポーネントモデルをサポートするWASMランタイムはまだごく一部(多分wasmtimeだけ)で、オプション扱いである
- コンポーネントはWASMであるため様々な言語に埋め込んで(embeded)利用が可能
- コンポーネントはNPMやCrateのようにリモートレジストリからダウンロードして使う未来が想定されている
Discussion