🦀

wasmtimeクレートでWASI:CLIコンポーネントを動かすまで

2023/12/31に公開

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

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

TL;DR:

  • wasmtimeクレートに加えて、wasmtime-wasiクレートを利用します
  • component-modelasyncを有効にしてWasmtimeを利用します
  • asyncブロック、もしくはasync関数の中で、コンポーネントを処理します

準備

WASI:Cliワールドの実装を用意します。Rustの場合は、cargo-componentを利用するのが簡単なように思います。cargo-componentのtipsを別途まとめましたので、参考になれば幸いです。

パッケージの準備

wasmtimeクレートを利用するパッケージを用意します。今回はバイナリーパッケージとして用意しました。パッケージを用意したら、次のクレートを依存関係に追加します。

クレート 有効にするフィーチャー 役割
wasmtime async, component-model Wasmの処理系
wasmtime-wasi WasmtimeによるWASIの実装
async-std attributes 非同期プログラム向けライブラリ

上記の依存関係は、cargo addで次のように追加できます。

% cargo add wasmtime -F async -F component-model
% cargo add wasmtime-wasi
% cargo add async-std -F attributes

実行までの流れ

Wasmコンポーネントの実行までの流れは次のようになります:

  1. WasiViewトレイトの実装
  2. 1の実装に加えて、EngineStoreLinkerを初期化
  3. WASI:CLIワールドの実装をLinkerに追加
  4. Componentオブジェクトの作成
  5. コンポーネントインスタンスを作成し、Commandオブジェクトとしてラップ
  6. Commandオブジェクトのメソッドを呼んで実行

順に解説します。なお、wasmtime-wasiはpreview0、preview1、そしてpreview 2に対応しています。この記事ではwasmtime_wasi::preview2以下に存在する、prevew 2のAPIを利用します。

上記のステップに登場する構造体/トレイトをまとめると、次のようになります。

構造体/トレイト 説明
WasiCtx WASIの実行コンテキスト
Table 数値とリソースの対応表。ファイルハンドルなどの表現に利用
WasiViewトレイト WasiCtxTableの組みを表すトレイト
Engine Wasmの評価器
Store Wasmインスタンスと、それぞれの実行状態を保存する構造体
Linker コンポーネントのインポート/エキスポートの解決をするための構造体

WasiViewの実装

WasiViewとは次の2つを組みにして扱うためのトレイトです。

  • WASIの実行コンテキストであるWasiCtx
  • ファイルハンドルに代表される数値と、その操作対象とを対応づけるためのデータ構造Table

WASI:CLIの実装をLinkerに追加する関数add_to_linkerを呼び出すためには、StoreWasiViewを実装した構造体が保持されていることが必要です。一方で、WasiViewを実装した構造体はwasmtime_wasiに含まれません。そのため、自分で実装する必要があります。次のコードは、WasiViewを簡単に実装した例です:

https://github.com/chikoski/wasm-component-rust-snippets/blob/main/wasmtime-wasi-cli/src/wasi_view_impl.rs#L1-L24

main関数の設定

Wasmtimeはコンポーネントのインスタンス作成や、Commandオブジェクトの実行を非同期処理として実装しています。

この記事では、async-stdの提供するマクロmainを使って、main関数で非同期関数を呼び出せるように設定します。次のコードが設定されたmain関数の例です。呼び出されているrun関数に、以降の処理を記述します。

https://github.com/chikoski/wasm-component-rust-snippets/blob/main/wasmtime-wasi-cli/src/main.rs#L8-L13

Engineの初期化

まずEngineを設定をします。Engineの設定はConfigオブジェクトによって与えられます。次のように、標準の設定に加えて、async_supportwasm_composenent_modelを有効にします。

https://github.com/chikoski/wasm-component-rust-snippets/blob/main/wasmtime-wasi-cli/src/main.rs#L30-L35

設定できたConfigオブジェクトを与えて、Engineを初期化します。

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

WasiViewStoreLinkerの初期化

粛々と初期化を行います。StoreLinkerの初期化にはEngineオブジェクトが必要です。またStoreの初期化にWasiViewの実装を与えます。これで、EngineStore、そしてLinkerWasiViewの実装であるWasiViewImplと関連づけられます。

https://github.com/chikoski/wasm-component-rust-snippets/blob/main/wasmtime-wasi-cli/src/main.rs#L17-L19

WasiViewImpl::newは、自作の関数です。次のようにWasiCtxBuilderを使って、WasiCtxを作成しています。inherit_stdoutを呼んでいるのは、Wasmの標準出力を、このプログラムの動くターミナルへ出力するためです。

https://github.com/chikoski/wasm-component-rust-snippets/blob/main/wasmtime-wasi-cli/src/wasi_view_impl.rs#L26-L35

WASI:CLIワールドの実装をLinkerに追加

LinkerWASI:CLIの実装を与えて、WASI:CLIに依存するコンポーネントをインスタンス化できるようにします。

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

コンポーネントのロードとインスタンス化

Wasmファイルをロードし、Componentをインスタンス化します。async_supportを有効にしてEngineを作成しているので、Command::instantiate_asyncを呼んでインスタンスを作成します。

https://github.com/chikoski/wasm-component-rust-snippets/blob/main/wasmtime-wasi-cli/src/main.rs#L23-L24

Command::instantiate_asyncCommandオブジェクトと、Instanceオブジェクトのタプルを返します。Commandオブジェクトがあればコマンドを実行できるので、上記の例ではInstanceオブジェクトは_に束縛しています。

コマンドの実行

Command#wasi_cli_run()を呼ぶと、Runオブジェクトへの参照を取得できます。Runオブジェクトはwasi:cli/runインターフェスの実装です。取得されたRunオブジェクトのcall_runメソッドを呼ぶことで、ロードしたWasmコンポーネントを実行できます。

https://github.com/chikoski/wasm-component-rust-snippets/blob/main/wasmtime-wasi-cli/src/main.rs#L25-L26

作成したプログラムの例

上記のステップを全てまとめたプログラムは、次のようになります:

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

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

実行例

cargo component newで作成される、次のようなコンポーネントがあります。

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

これをコンパイルして得られたWasmコンポーネントを、上述のプログラムで実行した結果です:

% cargo run
   Compiling wasmtime-wasi-cli v0.1.0 (/some/project)
    Finished dev [unoptimized + debuginfo] target(s) in 4.30s
     Running `/some/project`
Hello, world!

まとめと、よくあるエラーのまとめ

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

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

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

あと、いくつか私もハマったよくあるエラーをまとめました。ご参考になれば幸いです。

async_supportを有効にしていない場合

async_supportを有効にしていないEngineを利用している場合、次のような実行時エラーが発生します。

thread 'main' panicked at /some/where/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wasmtime-16.0.0/src/component/linker.rs:320:9:
cannot use `func_wrap_async` without enabling async support in the config
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

WasiViewの実装を与えずに、Storeを初期化した場合

Storeは、任意のデータを与えて初期化できます。下記の例では、()を与えてStoreを初期化しています。

let store = Store::new(&engine, ());

初期化に利用したデータがWasiViewトレイトを実装していない場合、次のようなコンパイルエラーが発生します。

error[E0277]: the trait bound `(): WasiView` is not satisfied

実行時エラーが起きないのに、標準出力に何も出力されない

WasiCtxの作成時にinherit_stdoutを呼ぶと、Wasmコンポーネントの標準出力へ出力された文字列を、wasmtimeが実行しているターミナルの標準出力に出力します。

https://github.com/chikoski/wasm-component-rust-snippets/blob/main/wasmtime-wasi-cli/src/wasi_view_impl.rs#L28

Discussion