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