はじめに
当初のアナウンス通り、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
orcargo binstall
でインストール可能 - 2025-09-15時点で0.5.0が最新
-
- wasm-tools
-
cargo install
orcargo binstall
でインストール可能 - 2025-09-15時点で1.239.0が最新
-
- wac-cli
-
cargo install
orcargo binstall
でインストール可能 - 2025-09-15時点で0.8.0が最新
-
- wit-bindgen-cli
-
cargo install
orcargo binstall
でインストール可能 - 2025-09-15時点で0.42.1が最新
-
- wasmtimeのソース
- wasi_snapshot_preview1_reactor.wasm
- wasmtimeとバージョンを合わせる
- 基本最新版使っておけば大丈夫だと思う(希望的観測)
- https://github.com/bytecodealliance/wasmtime/releases/download/v36.0.2/wasi_snapshot_preview1.reactor.wasm
- wasmtimeとバージョンを合わせる
いくつかのあまり見かけないツールをちょこっと解説。
wit-deps
WASI Preview2
以降、Webassembry Component Model
ベースとなっている。
このモデルの記述言語がWasm Interface Type (WIT)
であり、deps.toml
を書いて(後述)コマンドを実行することで、いい感じに依存を解決してくれる。そんなツール。
wasm-tools
WASM
の内容をWAT
フォーマットでダンプしたりなんかできるお役立ちツール。
今回は、Wasm module
をWasm 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
。
このバージョンのアーカイブにはwit
とwit-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. ビルド
クレートルートをカレントディレクトリにして、まず以下のコマンドを叩いてwasip1
なwasm
を作成する。
cargo build --target wasm32-wasip1
ついで、以下のコマンドを叩いてwasip3
なwasm
を作成する。
# 出力先はどこでもいいけど、同じフォルダにした
wasm-tools component new \
--adapt /path/to/wasi_snapshot_preview1.reactor.wasm \
--output target/wasm32-wasip1/app.wasi.wasm \
target/wasm32-wasip1/app.wasm
wasip3
もWASM Component Model
ベースであるのに、wasm32-wasip2
を指定しないのは不思議に思うかもしれない。
これは、wasip3
でいくつかの組み込み関数が追加されているが、wasip2
のwasm_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 aread
andwrite
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
を簡単に合成できるので、今のうちから素振りしておくといいかも。