RustでWebAssembly Componentを作る - cliでHello Worldする
Rustの開発環境はある状態を前提とします。WebAssemblyは以下、wasmと書きます。
※WebAssembly 勉強中です。この記事は間違った情報や不必要な手順を含む可能性があります。
この記事のコード例
開発に必要なツールをインストールする
cargo install --locked cargo-component
cargo install --locked wasm-tools
curl https://wasmtime.dev/install.sh -sSf | bash
https://component-model.bytecodealliance.org/language-support/rust.html
vscode
witファイルを見やすくするExtensionを入れる。cursorで使う場合はMarketplaceにない可能性があり、手動で入れる。
- WIT IDL
ビルドターゲットの設定
wasm32-wasip2を追加する
rustup target add wasm32-wasip2
ref: The wasm32-wasip2 Target Has Reached Tier 2 Support
プロジェクトを作成する
cargo component new sample --lib
## buildできるか確認
cargo component build -r --target wasm32-wasip2
実行する
hello-worldを動かしてみる
wasmtime run --invoke 'hello-world()' target/wasm32-wasip2/release/sample.wasm
"Hello, World!"
wasmtimeで動かす場合、wasi:cliのcommandが実装されていないケースではinvokeを使う必要がある。今回はwasm componentを作っているので関数名を’ ‘で囲い、‘hello-world()‘のように書く必要がある
wsi:cli/runをexportしてcommand lineからの実行を可能にする
上記の手順でbuildしたwasmをinvokeなしでwasm-toolで実行するとエラーになる。
wasmtime target/wasm32-wasip2/release/sample.wasm
Error: failed to run main module `target/wasm32-wasip2/release/sample.wasm`
Caused by:
no exported instance named `wasi:cli/[email protected]`
wasm-toolsでwasmのwitを確認する
wasm-tools component wit target/wasm32-wasip2/release/sample.wasm
package root:component;
world root {
import wasi:cli/[email protected];
import wasi:cli/[email protected];
import wasi:io/[email protected];
import wasi:io/[email protected];
import wasi:cli/[email protected];
import wasi:cli/[email protected];
import wasi:cli/[email protected];
import wasi:clocks/[email protected];
import wasi:filesystem/[email protected];
import wasi:filesystem/[email protected];
export hello-world: func() -> string;
}
// ...省略
exportにhello-worldはあるがwasi:cli/[email protected]
はない。
witからexportする
wit/world.witにwasi:cliを追加する。エラーのバージョンと必ずしもバージョンを合わせなくて良いみたいなので最新を入れている(0.2.7)
package component:sample;
use wasi:cli/[email protected];
/// An example world for the component to target.
world example {
export run;
export hello-world: func() -> string;
}
cargo.tomlに[package.metadata.component.target.depenencies]を作り、依存を追加する
[package.metadata.component.target.dependencies]
"wasi:cli" = "0.2.7"
runの実装
runをexportしたがその実装がないのでsrc/lib.rsを修正する
#[allow(warnings)]
mod bindings;
use bindings::Guest;
struct Component;
impl bindings::exports::wasi::cli::run::Guest for Component {
fn run() -> Result<(),()> {
println!("Hello, World! from cmd");
Ok(())
}
}
impl Guest for Component {
/// Say hello!
fn hello_world() -> String {
"Hello, World!".to_string()
}
}
bindings::export!(Component with_types_in bindings);
ここでbinding::exports周りでエラーが出る場合、自動生成されたbindings.rsを見て、exportsを確認する
#[rustfmt::skip]
#[allow(dead_code, clippy::all)]
pub mod exports {
pub mod wasi {
pub mod cli {
#[allow(dead_code, async_fn_in_trait, unused_imports, clippy::all)]
pub mod run {
lib.rsではbindings.rsにこういうコードが生成されている想定。できていない場合はcargo cleanなどしてもう一度生成してみる(cargo component checkなどする)
実行する
wasmtime run target/wasm32-wasip2/release/sample.wasm
Hello, World! from run
wasmtime run --invoke 'hello-world()' target/wasm32-wasip2/release/sample.wasm
"Hello, World!"
作成したwasmが wasi:cli/runをexportするようになったので先ほどのエラーは無くなった
wasm-tools component wit target/wasm32-wasip2/release/sample.wasm
# ...省略
export hello-world: func() -> string;
export wasi:cli/[email protected];
}
# ...省略
cargo-componentの場合、wit-depsではなくcargo.tomlに記載すれば動く
今回大きくハマったのはwit周りで、wasi:cliといったwasiの機能をどう扱えば良いのか迷走した。wasmはツールもたくさんあって何が必須かわからず時間だけがかかった
今回の方法とは別に、wit配下にwit.depsを作成し、wit-depsツールを使う方法もある。wasi:cliを手動でローカルにダウンロードしてきてそれをimportするイメージとなる。
今のところ、cargo.tomlに記載してcargo-componentにやってもらうのが手順が少なくて良さそうなのでこっちにした。勉強中なのでやっているうちに意見は変わるかもしれない