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()‘のように書く必要がある

CLI Options for wasmtime

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にやってもらうのが手順が少なくて良さそうなのでこっちにした。勉強中なのでやっているうちに意見は変わるかもしれない

参考: cargo-componentから脱却するには