🦀

wasi:cli/commandをRustで動かすには

2023/12/31に公開

WebAssemblyランタイムの1つであるWasmtimeには、Rustにランタイムを埋め込むためのクレートがあります。そのクレートを使って、wasi:cli/commandワールドを実装したWasmコンポーネントを実行するまでをまとめました。

なお、この記事はWasmtime28.0.0を対象にしています。

TL;DR:

実行用のプログラムの準備

テスト用に実行するwasi:cli/commandの実装を用意します。次のようなバイナリークレートをビルドして用意します:

https://github.com/chikoski/wasm-component-rust-snippets/blob/main/wasi-cli/hello-wasi-cli/src/main.rs

利用するクレート

次のクレートを依存関係に追加します:

  • wasmtime
  • wasmtime-wasi
  • tokio

wasmtime-wasiクレートはwasi:cli/stdinのようなwasi:cli/commandが依存するインターフェースの実装を提供します。またwasi:cli/commandの定義から生成されたラッパーであるCommand構造体も提供します。

Command構造体はいくつかの機能を非同期メソッドとして提供しています。この記事では非同期処理の処理系としてtokioを利用します。ただしtokioでなければならない理由はありません。任意の処理系を利用いただけます。

私の作成したプログラムでは、anyhowclapを上記以外に利用しています:

https://github.com/chikoski/wasm-component-rust-snippets/blob/main/threads/wasi-threads-runtime/Cargo.toml#L6-L12

Wasm処理のための基本的なツール

WasmtimeでWasmを処理するには、次の3つの構造体を利用します:

  • Engine:Wasmの処理エンジン
  • Store:Wasmの実行コンテキスト
  • Linker:Wasmコンポーネントのインスタンス化を実行

Storeオブジェクトはインスタンス化されたWasmモジュールやコンポーネントだけでなく、Wasmを実行するコード(ホスト側)の状態を表したオブジェクトも保持します。次の例では、ホスト側の状態をPreview2Hostという名前のオブジェクトで表しています:

https://github.com/chikoski/wasm-component-rust-snippets/blob/main/threads/wasi-threads-runtime/src/runtime/command_runtime.rs#L53-L55

wasmtime-wasiの利用

wasmtime-wasiクレートが提供する各インターフェースの実装を利用するには、add_to_linker_async関数、もしくはadd_to_linker_sync関数を利用してLinkerオブジェクトにインターフェースの実装を登録します:

https://github.com/chikoski/wasm-component-rust-snippets/blob/main/threads/wasi-threads-runtime/src/runtime/command_runtime.rs#L45

add_to_linker_asyncLinkerオブジェクトが内部で参照しているホスト側の状態を表したオブジェクトがWasiViewトレイトを実装していることを期待しています。最小の状態オブジェクトは次のように定義できます:

https://github.com/chikoski/wasm-component-rust-snippets/blob/main/threads/wasi-threads-runtime/src/runtime/preview2_host.rs#L4-L7

WasiCtxはWASIの実行コンテキストで、ResoruceTableがゲストコードから利用されているリソースと、その実体との対応表です。

WasiCtxオブジェクトはビルダーオブジェクトを利用して作成します。例えばinherit_stdioメソッドを呼ぶことで、ターミナルの標準入出力をWasmコンポーネントから利用できるようになります。

https://github.com/chikoski/wasm-component-rust-snippets/blob/main/threads/wasi-threads-runtime/src/runtime/preview2_host.rs#L11

これにWasiViewトレイトを実装すると、次のようになります:

https://github.com/chikoski/wasm-component-rust-snippets/blob/main/threads/wasi-threads-runtime/src/runtime/preview2_host.rs#L17-L25

非同期処理の有効化

add_to_linker_async関数などの非同期処理を利用するためには、tokioなどの処理系を依存関係に追加する以外に、Engineオブジェクトも非同期処理を有効にします。非同期処理を有効にしたConfigオブジェクトを引数に指定してnew関数を呼ぶことで、非同期処理が有効になったEngineオブジェクトを作成できます。

Configオブジェクトは次のように作成できます:

https://github.com/chikoski/wasm-component-rust-snippets/blob/main/threads/wasi-threads-runtime/src/runtime/command_runtime.rs#L17-L18

Commandオブジェクトの作成

CommandオブジェクトはWasmコンポーネントを表すComponentオブジェクトから作成します:

https://github.com/chikoski/wasm-component-rust-snippets/blob/main/threads/wasi-threads-runtime/src/runtime/command_runtime.rs#L35

Componentオブジェクトはバイト列やファイルから作成できます。ファイルからの作成は次のように行います:

https://github.com/chikoski/wasm-component-rust-snippets/blob/main/threads/wasi-threads-runtime/src/runtime/command_runtime.rs#L29

まとめ

Wasmモジュールの実行とほぼ同じ流れで実行できました。コツは次のの3点でしょうか。

  • wasmtime_wasiを依存関係に追加する
  • WasiViewトレイトを実装する
  • async_supportを有効にしたEngineを作成する

preview1以前の場合は同期的なコードを書くこともできましたが、preview2ではWASI実装の関係で非同期処理向けのコードを書くこととなります。同期的なコードを書かなければならない場合は、prewview2をpreview1のインターフェースに変えるアダプターを使ってpreview1を使うコンポーネントを作成し、それを実行することになるでしょう。

Discussion