wasmtimeクレートを使って、WasmコンポーネントのRustバインドを作成して動かすまで
wasmtimeクレートでWASI:CLIコンポーネントを動かすまででは、WASI:CLIのcommandワールドを実装したWasmコンポーネントを、wasmtimeクレートを使って動かす方法について述べました。この記事ではより一般的な内容を扱います。つまり、WITで定義されたワールドの実装をwasmtimeクレートを使って、Rustプログラムに埋め込む方法について述べます。
なお、WITについてはRustで学ぶWebAssembly Interface Type入門GitHubで開くという記事を別途書いています。WITの雰囲気をつかんでいただく助けになれば幸いです。
TL;DR
-
wasmtime::component::bindgenマクロで、WITファイルの定義に従ったコンポーネントのラッパーコードを生成できます - インターフェース定義に利用されているデーター構造もあわせて生成されます
- 生成されたデーター構造をRustで初期化し、参照をラッパーコード経由でWasmに与えることでエクスポートされた関数を呼び出します
シナリオ
ファイルから指定された条件に適合する行を抜き出して表示する、grepコマンドのようなプログラムを作ります。grepコマンドでは条件を正規表現か文字列で与えますが、作成するプログラムは次の定義に従うWasmコンポーネントを条件として利用します。
参照されているfilterインターフェースの定義は以下のとおりです。行番号と行の内容の組みであるlineとlineのリストであるline-listが定義されています。
パッケージの作成と依存関係
バイナリーパッケージを作成します。また次のクレートを依存関係に追加します。この中で、記事の内容に関係するクレートはwasmtimeのみです。他のものはコマンドライン引数の処理や、エラー処理のために追加しています。
wasmtimeはcomponent-modelのフィーチャーを有効にしてください。もろもろ設定したCargo.tomlは次のようになります。
プロジェクトのファイル構造は次のようになっています。witフォルダには上述のwitファイルが配置されています。またsrc/wasmにはデフォルトのguestワールドの実装が配置されています。
.
├── Cargo.toml
├── src
│ ├── main.rs
│ └── wasm
└── wit
├── filter.wit
└── guest.wit
WITからのコード生成
wasmtime::component::bindgenマクロを実行すると、WITファイルを読み、その定義に従ってコードが生成されます。
前述したファイルレイアウトだと、WITファイルの場所を指定しなくてもbindgenマクロを実行できます。次のように記述すると、WITファイルの場所や、コード生成するワールドを指定できます。下記の例ではother/wit/folder内に定義されているfooワールドを参照して、コード生成を行います。
bindgen!("foo" in "other/wit/folder")
なお、ワールドやインターフェースの名前が重複すると、次のようなエラーが発生します。
error: failed to parse package: /grep/like/project/wit
Caused by:
duplicate item named `filter`
--> /grep/like/project/wit/guest.wit:3:7
|
3 | world filter {
| ^-----
生成されたラッパーコードの利用
上記のWITではGuestというワールドが定義されています。bindgenマクロは、この定義からGuestというラッパー構造体を定義します。この構造体に実装されているinstantiate関数をつかえば、ロードしたComponentオブジェクトをラップしたGuestオブジェクトを作成できます。
WITで定義されているデータ構造の利用
前述のように、example:grep/typeには、2つのデーター構造が定義されています。lineはファイルの行を表し、line-listは行のリストを表します。
この2つのデーター構造から、bindgenマクロはLineとLineListという2つの構造体定義を生成します。生成された構造体は、Rustのコードの中で利用できます。次の例では、読み込んだファイルを行ごとに分割し、最終的にLineListを作成します。
エクスポートされている関数の呼び出し
guestワールドはfilterインターフェースに定義されている関数をエクスポートすると定めています:
そしてfilterインターフェースには、applyという関数のみが定義されています。この関数はline-listを引数に取り、line-listもしくはnullを返す関数として定義されています。
これらの定義から、bindgenマクロはGuestオブジェクトのメソッドを生成します。その結果、guestワールドから参照されているfilterインターフェースで定義されているapply関数は、guest.interface0.call_applyメソッドとして生成されます。
上記の例では、self.filterとself.filter.interface0の2つのオブジェクトが参照されています。それぞれの型は次のようになっています。Filterというのはexample:grep/filterから生成されたインターフェースとなっています。
| シンボル | 型 |
|---|---|
self.filter |
Guest |
self.filter.interface0 |
impl Filter |
なおinterface0という属性名は、filterがGuestワールドで最初にエクスポートされたインターフェースであることからきています。エクスポートされるインターフェスが増えるたびにに、属性も増えます。それらの属性名はinterface1、interface2のように添えられている数字が増える形で生成されます。
インターフェースで定義されている関数は、call_関数名のような名前でメソッド化されます。applyはcall_applyとして生成されているため、上記のようにメソッド呼び出しが行えます。
applyは(line-list) -> option<line-list>として定義されていますが、生成されたcall_applyは次のように定義されています。Storeオブジェクトの変更可能な参照と、LineListオブジェクトへの参照を与えることでWITに定義されたapply関数を呼び出せます。
pub fn call_apply<S: wasmtime::AsContextMut>(& self, mut store: S, arg0: &LineList) -> wasmtime::Result<Option<LineList>>
Wasmが利用する線形メモリーへのデーター転送や、メモリー表現に合わせたデータ変換といったことは気にせず、生成されたコードを使ってWasmコンポーネントに定義された関数を利用できていることに注意してください。
呼び出しの結果は、Result型で返ります。これはWasmコンポーネントの中に実装が存在しないといった、呼び出しが失敗する可能性が常に存在するためだと理解しています。
まとめと感想
この記事ではWITに定義されたワールドの実装を、wasmtimeクレートを使ってRustのコードから利用する方法について述べました。bindgenマクロによって生成されたコードを利用することによって、wasmファイルをほとんど操作することなく、Wasmコンポーネントに定義された関数を透過的に利用できます。
使ってみて、やはりbindgenマクロはとても有用なように感じました。wit-bindgenとも挙動が異なるような感じがしますが、慣れの問題なようにも感じます。
ただ、interface0のような名前の付け方には違和感は感じました。インターフェースの名前はパッケージの中で一意なのだから、意味ある名前をインターフェース名から生成できるのではないかとも感じます。コントリビューションチャンスなのかもしれません。
Discussion