0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WASI 0.3を先取りしてみる

Posted at

はじめに

当初のアナウンス通り、2025年8月にwasi 0.3(通称wasip3)のベータ版が公開された。
これを書いている2025-09-15時点で、動かせる手順が確立できたのでメモとして残す。

これはあくまでベータ版を動かす手順。
正式リリースとして予定されている2025年12月で、手順はもっと単純化されると思う。

登場人物

  • rust
    • 2025-09-15時点で1.89.0が最新
  • wasm32-wasip1ターゲット
    • rustup target add wasm32-wasip1で追加できる
  • wit-deps
    • cargo install or cargo binstallでインストール可能
    • 2025-09-15時点で0.5.0が最新
  • wasm-tools
    • cargo install or cargo binstallでインストール可能
    • 2025-09-15時点で1.239.0が最新
  • wac-cli
    • cargo install or cargo binstallでインストール可能
    • 2025-09-15時点で0.8.0が最新
  • wit-bindgen-cli
    • cargo install or cargo binstallでインストール可能
    • 2025-09-15時点で0.42.1が最新
  • wasmtimeのソース
  • wasi_snapshot_preview1_reactor.wasm

いくつかのあまり見かけないツールをちょこっと解説。

wit-deps

WASI Preview2以降、Webassembry Component Modelベースとなっている。
このモデルの記述言語がWasm Interface Type (WIT)であり、deps.tomlを書いて(後述)コマンドを実行することで、いい感じに依存を解決してくれる。そんなツール。

wasm-tools

WASMの内容をWATフォーマットでダンプしたりなんかできるお役立ちツール。
今回は、Wasm moduleWasm componentに変換する目的で使用する。

wac-cli

Webassembry Component Modelは、Cannonical ABIとしてABIが厳密に仕様化されているため、言語の垣根を超えてコンポーネントを連携させることができるようになっている。
wacは複数のWASMをガッチャンコして一つのwasmとしてまとめ上げるツール。

手順

以下の内容を解説する。

  • wasmtimeのビルド
  • 非同期版wasi:cli/runの実装
  • stream型を使ったコンポーネントの実装
  • future型を使ったコンポーネントの実装

wasmtimeのビルド

プリビルドとして配布されているwasmtimeは非同期の実行機構が無効化されている。
そのため非同期に関するフィーチャーを有効化してビルドする必要がある。

1. Cargo.tomlを編集する

  • 以下の2箇所の依存に対して、フィーチャーを指定する。
# `wasmtime-wasi`の依存に`p3`の`feature`を追加
- wasmtime-wasi = { path = "crates/wasi", version = "38.0.0", default-features = false }
+ wasmtime-wasi = { path = "crates/wasi", version = "38.0.0", default-features = false, features = ["p3"] }
# `wasmtime-wit-bindgen` の依存に`component-model-async`の`feature`を追加
- wasmtime-wit-bindgen = { path = "crates/wit-bindgen", version = "=38.0.0", package = 'wasmtime-internal-wit-bindgen' }
+ wasmtime-wit-bindgen = { path = "crates/wit-bindgen", version = "=38.0.0", package = 'wasmtime-internal-wit-bindgen', features = ["component-model-async"] }

2. src/common.rsを編集

  • ソースレベルでwasip3の非同期実行が抑制されているため、解除する
- pub const P3_DEFAULT: bool = cfg!(feature = "component-model-async") && false;
+ pub const P3_DEFAULT: bool = cfg!(feature = "component-model-async");

3. ビルド

cargo build

非同期版wasi:cli/runの実装

1. コンポーネントクレート作成

  • ここではサブクレートとして用意する
cargo new --lib --vcs none crates/app

Cargo.tomlを編集する

+ [lib]
+ crate-type = ["cdylib"]

1. deps.tomlを用意する

クレートのルートから見てwit/deps.tomlを作成し、以下のように編集する。

[cli]
url = "https://github.com/WebAssembly/wasi-cli/archive/refs/tags/v0.3.0-rc-2025-08-15.tar.gz"
subdir = "wit-0.3.0-draft"

2025-09-15時点で、wasi/cliパッケージのバージョンは、v0.3.0-rc-2025-08-15
このバージョンのアーカイブにはwitwit-0.3.0-draftが含まれている。
witの方はv0.2.6でありwisip2用。
今回はwasip3として実装するため、subdirとしてそのパスを指定する必要がある。

クレートルートをカレントディレクトリにして、以下のコマンドを叩く

wit-deps update

wit/depsフォルダにwasi/cliパッケージに依存するwitが展開される。

CLIオプションとして依存の展開先、ロックファイル(deps.lock)のパス、マニフェストのパス(deps.toml)を指定することで、プロジェクトルート以外からでも実行できる。
詳細はwit-deps --helpを見てくだされ。

2. wit定義を用意する

クレートのルートから見てwit/world.witを作成し、以下のように編集する。

world.witという名称は慣用的に使われているもので、好きな名称を使用しても何ら問題ない。

package local:app@0.0.1;

world app-world {
    import wasi:cli/stdout@0.3.0-rc-2025-08-15;
    export wasi:cli/run@0.3.0-rc-2025-08-15;
}

クレートルートをカレントディレクトリにしてwit-bindgenを実行し、rustのバインディングコードを作成する。

wit-bindgen rust \  --out-dir ./src/bindings \
  --default-bindings-module "crate::bindings::app_world" \
  --async all \
  ./wit

src/bindings/app_world.rsが作成される。

3. ゲストインターフェースを実装する

src/lib.rsを編集する。

mod bindings {
    pub mod app_world;
}

use crate::bindings::app_world::{exports, wit_stream, wasi};

bindings::app_world::export!(App);

struct App;

impl exports::wasi::cli::run::Guest for App {
    async fn run() -> Result<(),()> {
    
        let (mut tx, rx) = wit_stream::new();
        wasi::cli::stdout::set_stdout(rx).await;

        let remaining = tx.write_all(b"Hello, WASI!!\n".to_vec()).await;
        assert!(remaining.is_empty());

        Ok(())
    }
}
  • バインディングコードとして生成されたwit_stream::new()関数で、送受信のチャンネルを作成
  • 受信側を標準出力に設定
  • 送信側にバイト列を書き出すと、標準出力として表示

4. ビルド

クレートルートをカレントディレクトリにして、まず以下のコマンドを叩いてwasip1wasmを作成する。

cargo build --target wasm32-wasip1

ついで、以下のコマンドを叩いてwasip3wasmを作成する。

# 出力先はどこでもいいけど、同じフォルダにした
wasm-tools component new \
    --adapt /path/to/wasi_snapshot_preview1.reactor.wasm \
    --output target/wasm32-wasip1/app.wasi.wasm \
    target/wasm32-wasip1/app.wasm

wasip3WASM Component Modelベースであるのに、wasm32-wasip2を指定しないのは不思議に思うかもしれない。
これは、wasip3でいくつかの組み込み関数が追加されているが、wasip2wasm_snapshot_preciew1.reactor.wasmには当然のごとく含まれていないため、componentizeのフェーズでwasm-component-ldがエラーを吐く。
そのため、wasip1 -> wasip3の変換を手作業で行う必要がある。

wasmtimeで実行する

# ソースビルドしたwasmtimeで実行する
/path/to/wasmtime -W component-model-async target/wasm32-wasip1/app.wasi.wasm

実行時にもフィーチャーで制限されているため、-W component-model-asyncを指定して有効化する必要がある。

うまくいけば

Hello, WASI!!

と表示される(はず)。

stream型を使ったコンポーネントの実装

手順はwasi:cli/run
の実装とさして違いはないため、ダイジェストで書き出す。

wit定義

package username:stream-value-set@0.0.1;

interface my-stream {
    enum kind { a, b, c, d }

    record entry {
        kind: kind,
        key: string,
        value: u32,
    }

    resource my-container {
        constructor();
    }
    
    iter-stream: func(c: my-container) -> stream<entry>;
}

world container-world {
    export my-stream;
}

バインディング

wit-bindgen rust \  --out-dir ./src/bindings \
  --default-bindings-module "crate::bindings::my_stream_world" \
  --async all \
  ./wit

--async allを指定するとmy-containerのコンストラクタも非同期関数として作られるけどめんどいからそのまま行く。

インターフェースの実装

// lib.rs
use std::collections::HashMap;

mod bindings {
    pub mod my_stream_world;
}

use crate::bindings::my_stream_world;
use crate::bindings::my_stream_world::exports::local::my_stream::my_stream;

#[derive(Debug, Clone)]
pub enum MyKind {
    A, B, C, D
}

impl From<MyKind> for my_stream::Kind {
    fn from(value: MyKind) -> Self {
        match value {
            MyKind::A => my_stream::Kind::A,
            MyKind::B => my_stream::Kind::B,
            MyKind::C => my_stream::Kind::C,
            MyKind::D => my_stream::Kind::D,
        }
    }
}

pub struct MyContainerImpl {
    kinds: Vec<MyKind>,
    map: std::sync::Arc<HashMap<String, u32>>,
}

impl MyContainerImpl {
    pub fn new_raw() -> Self {
        let kinds = vec![MyKind::A, MyKind::D, MyKind::B, MyKind::D, MyKind::C];
        let map = HashMap::from([
            ("A".to_string(), 2),
            ("B".to_string(), 3),
            ("C".to_string(), 4),
            ("D".to_string(), 8),
        ]);
        
        Self { kinds, map: std::sync::Arc::new(map) }
    }
}

impl my_stream::GuestMyContainer for MyContainerImpl {
    async fn new() -> Self {
        MyContainerImpl::new_raw()
    }
}

struct Component;
my_stream_world::export!(Component);

impl my_stream::Guest for Component {
    type MyContainer = MyContainerImpl;
    
    async fn iter_stream(c: my_stream::MyContainer,) -> wit_bindgen::rt::async_support::StreamReader<my_stream::Entry> {
        let (mut tx, rx) = container_world::wit_stream::new::<my_stream::Entry>();

        let container = c.get::<Self::MyContainer>();
        let kinds = container.kinds.clone(); 
        let map = container.map.clone(); 

        wit_bindgen::spawn(async move {
            for kind in kinds {
                let key = format!("{:?}", kind);
                let v = map.get(&key).unwrap().clone();

                tx.write_one(my_stream::Entry{
                    kind: kind.into(),
                    key,
                    value: v
                }).await;
            }
        });

        rx
    }
}

一つ注意点。
ストリームへの書き出しは、wit_bindgen::spawnを使って別コンテキストで行う必要がある。
これはおそらく公式のドキュメントに記載されている、

Streams and Futures

(snip)
As a temporary limitation, if a read and write for a single stream or
future occur from within the same component and the element type is non-empty,
there is a trap. In the future this limitation will be removed.

(https://github.com/WebAssembly/component-model/blob/main/design/mvp/Async.md#streams-and-futures)

によるものと思われる。

ビルド

cargo build --target wasm32-wasip1
wasm-tools component new \
    --adapt /path/to/wasi_snapshot_preview1.reactor.wasm \
    --output target/wasm32-wasip1/my_stream.wasi.wasm \
    target/wasm32-wasip1/my_stream.wasm

app.wasi.wasmに組み込む(wasip3なwasmを作るところまで)

  • deps.tomlに以下を追記
# 用意したwit定義へのパス
# 相対パスで指定する場合、witフォルダがルートとなることに注意が必要
my-stream = "/path/to/wit/"
  • wit-deps updateを叩いて依存を更新
  • app crateのwit定義を更新
world app-world {
    // (snip)
+    import local:my-stream/my-stream@0.0.1;
}
  • バインディングの再作成
wit-bindgen rust \  --out-dir ./src/bindings \
  --default-bindings-module "crate::bindings::app_world" \
  --async all \
  ./wit
  • wasi:cli/runの実装から呼び出す
// lib.rs
(snip)

use crate::bindings::app_world::username::my_stream::my_stream;

impl exports::wasi::cli::run::Guest for App {
    async fn run() -> Result<(),()> {
        let (mut tx, rx) = wit_stream::new();
        wasi::cli::stdout::set_stdout(rx).await;

        let remaining = tx.write_all(b"Hello, WASI!!\n".to_vec()).await;
        assert!(remaining.is_empty());
    
        let c = my_stream::MyContainer::new().await;
        let mut reader = my_stream::iter_stream(c).await;
    
        while let Some(v) = reader.next().await {
            tx.write_all(format!("{:?}\n", v).as_bytes().to_vec()).await;
        }

        Ok(())
    }
}
  • ビルド
cargo build --target wasm32-wasip1
wasm-tools component new \
    --adapt /path/to/wasi_snapshot_preview1.reactor.wasm \
    --output target/wasm32-wasip1/app.wasi.wasm \
    target/wasm32-wasip1/app.wasm

コンポーネントの合成

app.wasi.wasmは、my_stream.wasi.wasmの関数がまだインポートされていない。
そのまま実行するとインポート不足によりエラーとなるため、wac-cliを使用してこれらのwasmを合成したひとつのwasmを作る必要がある。

コマンドは以下

wac plug \
    --plug target/wasm32-wasip1/my_stream.wasi.wasm \
    --output target/wasm32-wasip1/app-plugged.wasi.wasm \
    target/wasm32-wasip1/app.wasi.wasm

母体(app.wasi.wasm)に対して、組み込むwasm(my_stream.wasi.wasm)を--plugオプションで指定する。

実行

# ソースビルドしたwasmtimeで実行する
/path/to/wasmtime -W component-model-async target/wasm32-wasip1/app-plugged.wasi.wasm

うまくいけば

Hello, WASI!!
Entry { kind: Kind::A, key: "A", value: 2 }
Entry { kind: Kind::D, key: "D", value: 8 }
Entry { kind: Kind::B, key: "B", value: 3 }
Entry { kind: Kind::D, key: "D", value: 8 }
Entry { kind: Kind::C, key: "C", value: 4 }

と表示される(はず)。

future型を使ったコンポーネントの実装

ここもダイジェストで

wit定義

package local:try-future@0.0.1;

interface my-task {
    ping: func(rx: future<string>, msg: string) -> future<string>;
    pong: func(rx: future<string>) -> string;
}

world my-future-world {
    export my-task;
}

バインディング

wit-bindgen rust \  --out-dir ./src/bindings \
  --default-bindings-module "crate::bindings::my_future_world" \
  --async all \
  ./wit

インターフェースの実装

// lib.rs

mod bindings {
    pub mod try_future_world;
}

use crate::bindings::try_future_world::{exports::username::try_future, wit_future};

struct MyFuture;
bindings::try_future_world::export!(MyFuture);

impl try_future::my_task::Guest for MyFuture {
    async fn ping(rx: wit_bindgen::FutureReader<String>, msg: String,) -> wit_bindgen::FutureReader<String> {
        let res = rx.await;

        let (next_tx, next_rx) = wit_future::new(|| unreachable!());

        wit_bindgen::spawn(async move {
            next_tx.write(format!("{}!{}", res, msg)).await.unwrap();
        });
        
        next_rx
    }

    async fn pong(rx: wit_bindgen::FutureReader<String>) -> String {
        rx.await
    }
}
  • ビルド
cargo build --target wasm32-wasip1
wasm-tools component new \
    --adapt /path/to/wasi_snapshot_preview1.reactor.wasm \
    --output target/wasm32-wasip1/my_future.wasi.wasm \
    target/wasm32-wasip1/my_future.wasm

app crateに組み込む

  • wit-depsの件は同様なので省略
  • app crateのworld.witを編集
world app-world {
    // (snip)
+    import local:my-future/my-task@0.0.1;
}
  • バインディングの再作成も同様なので省略
  • wasi:cli/runの実装から呼び出す
// lib.rs
(snip)

use crate::bindings::app_world::username::my_stream::my_stream;
use crate::bindings::app_world::username::try_future::my_task;
use crate::bindings::app_world::wit_future;

impl exports::wasi::cli::run::Guest for App {
    async fn run() -> Result<(),()> {
        let (mut tx, rx) = wit_stream::new();
        wasi::cli::stdout::set_stdout(rx).await;

        let remaining = tx.write_all(b"Hello, WASI!!\n".to_vec()).await;
        assert!(remaining.is_empty());
    
        let c = my_stream::MyContainer::new().await;
        let mut reader = my_stream::iter_stream(c).await;
    
        while let Some(v) = reader.next().await {
            tx.write_all(format!("{:?}\n", v).as_bytes().to_vec()).await;
        }

        let result = wit_bindgen::block_on(async move {
            eprintln!("start future");
            let (tx, rx) = wit_future::new(|| unreachable!());
            let rx2 = async {
                let rx = my_task::ping(rx, "Foo".into()).await;
                let rx = my_task::ping(rx, "Bar".into()).await;
                let rx = my_task::ping(rx, "Baz".into()).await;
                let rx = my_task::ping(rx, "Quax".into()).await;
                rx
            };
            let tx2 = async { tx.write("Start".into()).await.unwrap() };
            let (rx3, ()) = futures::join!(rx2, tx2);
            rx3.await
        });
        
        eprintln!("result: {}", result);
        Ok(())
    }
}

ここでも同一コンテキストでmy_task::pingを呼ぶとデッドロック扱いになる。
そのためwit_bindgen::block_onで同期的に非同期ブロックを実行している。

  • ビルド & 合成
cargo build --target wasm32-wasip1

wasm-tools component new \
    --adapt /path/to/wasi_snapshot_preview1.reactor.wasm \
    --output target/wasm32-wasip1/app.wasi.wasm \
    target/wasm32-wasip1/app.wasm

wac plug \
    --plug target/wasm32-wasip1/my_stream.wasi.wasm \
    --plug target/wasm32-wasip1/my_future.wasi.wasm \
    --output target/wasm32-wasip1/app-plugged.wasi.wasm \
    target/wasm32-wasip1/app.wasi.wasm

組み込むwasmが一つ増えただけ。

  • 実行
# ソースビルドしたwasmtimeで実行する
/path/to/wasmtime -W component-model-async target/wasm32-wasip1/app-plugged.wasi.wasm

うまくいけば

Hello, WASI!!
stream iterated
Entry { kind: Kind::A, key: "A", value: 2 }
Entry { kind: Kind::D, key: "D", value: 8 }
Entry { kind: Kind::B, key: "B", value: 3 }
Entry { kind: Kind::D, key: "D", value: 8 }
Entry { kind: Kind::C, key: "C", value: 4 }
start future
result: Start!Foo!Bar!Baz!Quax

となる(はず)。

まとめ

なかなかに手順が煩雑でメンドイところがあるけど、Wasm Component Modelを生かして、複数のwasmを簡単に合成できるので、今のうちから素振りしておくといいかも。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?