wasmtimeクレートでWASI:CLIコンポーネントを動かすまで
WebAssemblyランタイムの1つであるWasmtimeには、Rustにランタイムを埋め込むためのクレートがあります。そのクレートを使って、WASI CLIワールドを実装したWasmコンポーネントを実行するまでをまとめました。
なお、この記事はWasmtime16.0.0を対象にしています。
TL;DR:
- wasmtimeクレートに加えて、wasmtime-wasiクレートを利用します
-
component-model
とasync
を有効にして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コンポーネントの実行までの流れは次のようになります:
- WasiViewトレイトの実装
- 1の実装に加えて、
Engine
、Store
、Linker
を初期化 - WASI:CLIワールドの実装を
Linker
に追加 -
Component
オブジェクトの作成 - コンポーネントインスタンスを作成し、
Command
オブジェクトとしてラップ -
Command
オブジェクトのメソッドを呼んで実行
順に解説します。なお、wasmtime-wasiはpreview0、preview1、そしてpreview 2に対応しています。この記事ではwasmtime_wasi::preview2
以下に存在する、prevew 2のAPIを利用します。
上記のステップに登場する構造体/トレイトをまとめると、次のようになります。
構造体/トレイト | 説明 |
---|---|
WasiCtx |
WASIの実行コンテキスト |
Table |
数値とリソースの対応表。ファイルハンドルなどの表現に利用 |
WasiView トレイト |
WasiCtx とTable の組みを表すトレイト |
Engine |
Wasmの評価器 |
Store |
Wasmインスタンスと、それぞれの実行状態を保存する構造体 |
Linker |
コンポーネントのインポート/エキスポートの解決をするための構造体 |
WasiViewの実装
WasiViewとは次の2つを組みにして扱うためのトレイトです。
WASI:CLIの実装をLinker
に追加する関数add_to_linker
を呼び出すためには、Store
にWasiView
を実装した構造体が保持されていることが必要です。一方で、WasiView
を実装した構造体はwasmtime_wasi
に含まれません。そのため、自分で実装する必要があります。次のコードは、WasiView
を簡単に実装した例です:
main関数の設定
Wasmtimeはコンポーネントのインスタンス作成や、Command
オブジェクトの実行を非同期処理として実装しています。
この記事では、async-std
の提供するマクロmain
を使って、main
関数で非同期関数を呼び出せるように設定します。次のコードが設定されたmain
関数の例です。呼び出されているrun
関数に、以降の処理を記述します。
Engine
の初期化
まずEngine
を設定をします。Engine
の設定はConfig
オブジェクトによって与えられます。次のように、標準の設定に加えて、async_support
とwasm_composenent_model
を有効にします。
設定できたConfig
オブジェクトを与えて、Engine
を初期化します。
WasiView
、Store
、Linker
の初期化
粛々と初期化を行います。Store
とLinker
の初期化にはEngine
オブジェクトが必要です。またStore
の初期化にWasiView
の実装を与えます。これで、Engine
、Store
、そしてLinker
がWasiView
の実装であるWasiViewImpl
と関連づけられます。
WasiViewImpl::new
は、自作の関数です。次のようにWasiCtxBuilder
を使って、WasiCtx
を作成しています。inherit_stdout
を呼んでいるのは、Wasmの標準出力を、このプログラムの動くターミナルへ出力するためです。
Linker
に追加
WASI:CLIワールドの実装をLinker
にWASI:CLI
の実装を与えて、WASI:CLI
に依存するコンポーネントをインスタンス化できるようにします。
コンポーネントのロードとインスタンス化
Wasmファイルをロードし、Component
をインスタンス化します。async_support
を有効にしてEngine
を作成しているので、Command::instantiate_async
を呼んでインスタンスを作成します。
Command::instantiate_async
はCommand
オブジェクトと、Instance
オブジェクトのタプルを返します。Command
オブジェクトがあればコマンドを実行できるので、上記の例ではInstance
オブジェクトは_
に束縛しています。
コマンドの実行
Command#wasi_cli_run()
を呼ぶと、Run
オブジェクトへの参照を取得できます。Run
オブジェクトはwasi:cli/runインターフェスの実装です。取得されたRun
オブジェクトのcall_run
メソッドを呼ぶことで、ロードしたWasmコンポーネントを実行できます。
作成したプログラムの例
上記のステップを全てまとめたプログラムは、次のようになります:
実行例
cargo component new
で作成される、次のようなコンポーネントがあります。
これをコンパイルして得られた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_support
とwasm_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が実行しているターミナルの標準出力に出力します。
Discussion