Skip to content
Published on

WASI 0.3のネイティブasync — wasi:ioが消えた理由と、まだ終わっていないこと

シェア
Authors

はじめに — 6月11日に実際に起きたこと

2026年6月11日、WASIサブグループが投票でWASI 0.3.0を批准しました。見出しは「WebAssemblyコンポーネントにasyncがネイティブで入った」です。

この文章はハイプに聞こえやすく、実際WASM陣営はここ数年「もうすぐ来る」を何度も繰り返してきました。そこで本稿は2つを切り分けて見ようと思います — 実際にスペックとランタイムに入ったもの、そしてまだ終わっていないのに終わったように読めてしまうもの

まず結論から。今回のリリースは本物です。ただし「本物」の意味は、たいていの人が期待するものとは違います。これは性能リリースではなく、合成可能性(composability)リリースです。そして性能だけで見ると、今この瞬間はむしろ後退している区間があります。その話も下でします。

WASI 0.2の本当の問題 — サンドイッチ問題

WASI 0.2でもasyncはできました。wasi:ioパッケージがpollableリソースとinput-stream / output-stream、そしてpoll関数を提供しており、これでノンブロッキングI/Oを表現できました。

問題はコンポーネントを合成するときに噴き出しました。wasi.devの0.3概要ドキュメントが挙げる例は明快です。コンポーネントAがBを呼び、Bがホストを呼ぶ状況を考えてみましょう。

A  →  B  →  Host

ここでpollable単一のコンポーネントインスタンスに結びついたリソースです。だからBは、ホストから受け取ったpollableのウェイクアップをそのままAに渡す方法がありません。Bにできるのは、準備シグナルを中継しようと自分でポーリングし続けることだけで、ドキュメントの表現では、実務ではそのウェイクアップの連鎖がそのまま切れてしまいます。これをサンドイッチ問題(sandwich problem)と呼びます — WASI 0.2はasyncを表現はできても、コンポーネント境界を越えて合成することはできませんでした。

Bytecode Allianceの発表記事はこれをもっと直截に整理しています。WASI 0.2ではコンポーネントごとに自前のイベントループないしasyncランタイムが必要で、そのため個々のコンポーネントをホスト上で動かすことはできても、そのイベントループ同士が互いに調整し合う方法がなかった、というのです。結果として、コンポーネントがストリーミングやasync APIを使った瞬間、そのコンポーネントは他のコンポーネントと合成できなくなりました。

これがなぜ痛いかは、コンポーネントモデルの存在理由を考えれば明らかです。コンポーネントモデルの売りは「言語を問わず組み立てられるバイナリ」なのに、実務コードはほぼすべてI/Oをし、I/Oをすればasyncを使い、asyncを使えば組み立てられませんでした。つまり、もっとも重要な機能が、もっともよくあるケースで動かなかったのです。

WASI 0.3の解法は、asyncをコンポーネントモデルのカノニカルABIへと下ろすことです。これでイベントループはホストが一つだけ管理し、すべてのコンポーネントがそれを共有します。スケジューリングとウェイクアップの伝播をランタイムが所有するので、生産者と消費者の間にコンポーネントがいくつ挟まっていても、asyncはきちんと動作します。

3つの新しいプリミティブ

カノニカルABIに一級市民として追加されたのは3つです — async func、そしてストリームとフューチャー型です。

// 関数自体をasyncとして宣言する
handle: async func(request: request) -> result<response, error-code>;

// stream<T>: 型付きの非同期データチャネル
// future<T>: 単一の値の非同期な完了
read-via-stream: func() -> tuple<stream<u8>, future<result<_, error-code>>>;

いくつかの設計上の判断は目を留める価値があります。

所有権。 ストリームとフューチャーはリソース型のように振る舞います — それぞれ所有されたハンドルで、コンポーネント境界を越えて渡すと所有権が呼び出し元から呼び出し先へ移ります。ただしリソースと違い、借用はできません(borrow不可)。0.2のinput-streamがインスタンスに結びついたリソースだったため渡せなかったのとは正反対に、今やストリームは渡せるです。サンドイッチ問題が解けるのは、まさにこの点です。

完了ベース(completion-based)モデル。 これは準備ベース(readiness-based)ではありません。発表記事はこれをLinuxのio_uring、WindowsのIOCP/IoRingに例えています。epollやkqueueスタイルの準備ベースAPIが必要なら、この上にエミュレートできると明記しています。この選択は好みの問題ではなく合成に直結します — 準備シグナルは中継しなければなりませんが、完了はランタイムが直接起こしてやればそれで済むからです。

ストリームの状態問題の解決。 0.2ではストリームの終了エラーが、readの呼び出しのたびにインラインで乗って出てきました。そのため呼び出し側は最後まで読んで初めて結果がわかり、途中で読むのをやめると正常終了とエラーを区別できませんでした。0.3はストリームにフューチャーをもう一つ添えて送ることで、ストリームをどれだけ消費したかに関係なく結果が別途確定するようにしました。上のコードでストリームとフューチャーをタプルでまとめて返している形が、まさにその答えです。

wasi:ioはどこへ行ったのか

wasi:ioパッケージはまるごと削除されました。WASI 0.3版のwasi:ioは存在しません。担っていた仕事はすべて、コンポーネントモデルのネイティブプリミティブに吸収されました。

リリースノートが示すマッピング表が、このリリースの半分を要約しています。

WASI 0.2 (wasi:io)              →  WASI 0.3 (Component Model)
--------------------------------------------------------------
resource pollable               →  future<T>
resource input-stream           →  stream<u8>
resource output-stream          →  stream<u8>  (書き込み方向)
poll(list<pollable>)            →  futureをawait (ランタイムが処理)
subscribe() on resource         →  呼び出しがfuture<...>を返す
start-foo / finish-foo          →  foo: async func(...)

最後の行が実務でいちばん体感が大きいです。0.2のstart-bind / finish-bindstart-connect / finish-connectのような2段階のダンスと、それを回していたsubscribe()が全部消えて、async funcひとつに畳み込まれます。

インターフェースごとに実際に変わったもの

リリースノートは、変更の大半は機械的だと言います。0.2がasyncを装うために繰り出していた曲芸が要らなくなったので、同じことをずっと簡潔に書けるようになった、ということです。それでも何か所かは機械的ではありません。

wasi:http — 変更の幅がいちばん大きいです。 ここだけはポーリングインターフェースをasyncへ機械変換したのではなく、ワールドと中核の抽象を再編成しました。incoming-requestoutgoing-requestincoming-responseoutgoing-responseincoming-bodyoutgoing-bodyfuture-trailersfuture-incoming-responseresponse-outparam — これらのリソースが全部消え、ボディがストリームでトレイラーがフューチャーである統合されたrequest / responseリソースに整理されました。そしてワールドが二つに分かれます。

// serviceワールド: HTTP呼び出しを行い、入ってきたリクエストを処理する
world service {
  import client;
  export handler;
}

// middlewareワールド: serviceの上位集合
world middleware {
  include service;  // serviceがすることは全部やる
  import handler;   // 入ってきたリクエストを別のhandlerへ渡すこともできる
}

0.2時代のproxyワールドはserviceに置き換わり、middlewareワールドはリクエスト経路のミドルウェアを一級市民として表現します。

wasi:sockets — インターフェースが7個から2個に。 networkinstance-networktcptcp-create-socketudpudp-create-socketが、tcp-socketudp-socketリソースを収めた統合typesインターフェースにまとまり、DNSは別途ip-name-lookupが担います。そしてnetworkリソースが消えました — 0.2はネットワークアクセスを、すべてのbind/connect/lookup呼び出しに載せて運ぶcapabilityリソースとしてモデル化していましたが、0.3はこれを廃止し、ワールドのインポートレベルで権限を与えます。

wasi:clocks — ほとんどが名前変更。 wall-clocksystem-clockに、datetimeinstantに変わりました。理由は一貫性です — 「wall clock」と「datetime」は、POSIXにも、Rustのstd::timeにもない、WASI固有の用語でした。instantレコードの秒はu64からs64に変わり、エポック以前のタイムスタンプを表現できるようになりました。ダウンストリームへの影響はおおむね検索置換レベルですが、リリースノートがあえてこれをフラグしたのは、このchurnがテストスイート全体に現れるからです。

wasi:random — 名前の裏に意味の変化が隠れています。 get-random-bytesのパラメータがlenからmax-lenに変わりましたが、これは単純なリネームではありません。実装は要求されたよりも少ないバイト数を返してもよいことになりました。呼び出し側は、欲しい分だけ集まるまでループを回さなければならない、ということです。機械的変換だと思い込んでスルーすると、痛い目を見やすい箇所です。

ランタイムは準備できているか — Wasmtime 46が実際にやったこと

ここが「約束」と「出荷」を区別すべきところです。

発表記事(6月11日)は、Wasmtime 45が当時の最新リリース候補を回しており、Wasmtime 46がコンポーネントモデルasyncをデフォルトで有効にしてWASI 0.3.0を出荷する予定だと予告していました。未来形です。

実際に確認してみると — Wasmtime 46.0.0が2026年6月22日にリリースされ、そのリリースノートにこの一行があります。Wasmtimeは今やWASI 0.3.0をデフォルトでサポートし、component-model-async wasm機能がデフォルトで有効になった(#13612)。予告から11日で実際に届きました。ランタイム側は本物です。

一つ、文書の不一致を指摘しておく価値があります。wasi.devロードマップは「WASI 0.3サポートはWasmtime 43+で利用可能」と書いていますが、発表記事は45/46について語っています。概要ドキュメントを見ると、この差異が説明されます — Wasmtime v44から、wasmtime serve-Sp3 -W component-model-async=yフラグを与えれば0.3コンポーネントをサーブできていました(そして0.3のserviceワールドをエクスポートしないコンポーネントは自動的に0.2のwasi:http/proxyワールドにフォールバックします)。v44はwasi:tls@0.3.0-draftの初期サポートも入れました。つまりフラグの裏でのサポートはもっと早く入っており、デフォルトで有効になったのが46なのです。文書ごとに違う数字を語っているのは、この二つを区別していないからです。

適合性は共通のwasi-testsuiteで検証されており、WASI 0.3のカバレッジはWasmtimeとjcoで、Linux・macOS・Windowsにわたって回っています。

正直に言うと — まだ終わっていないこと

ここからが、この記事を書いた理由です。発表記事だけを読むとすべて終わったように見えますが、Bytecode Alliance自身が出した別の記事と一緒に読むと、絵はかなり違って見えてきます。

同期呼び出しは今、約3.5倍遅いです。 The Road to Component Model 1.0にこんな一節があります。現在の実装は、コンポーネント境界を越えるasyncタスクのインフラをホストコールで管理していますが、これが純粋に同期的な呼び出し経路にもおよそ3.5倍のオーバーヘッドを乗せます(Nick Fitzgeraldが Plumbers Summit で計測した数値として引用されています)。スペックレベルでは一部すでに対処されており(P3開発中にrecursion checkが除去されました)、残りはWASI P3出荷後に計画されています — 同期アダプタがスタックに軽量なタスクだけを割り当てるようタスク状態をリファクタリングし、Craneliftがほとんどを最適化で消し去れるようにする方向で、目標はasyncサポートが入るの性能プロファイルを取り戻すことです。

この一文は正確に読むことが重要です。目標は「もっと速くなる」ではなく、「以前と同じくらいに戻る」です。つまり、asyncをネイティブに入れた代償を同期経路が今払っており、その請求書はまだ払い終わっていません。

ゲストツールチェーンはまだ追いついている最中です。 スペックが批准されたことと、あなたの言語でそれが使えることは別の話です。発表記事は、Python、JavaScript、C#、Cを含む複数の言語で、ゲストバインディングジェネレータのasyncサポートが進行中だと書いています。jcoはWASI 0.3全体をサポートしていますが、それがデフォルトで有効なリリースは「まもなく」出る予定で、Rust側ではwit-bindgenasync fnが自然にマッピングされます。Goは毛色が違い、componentize-goはゴルーチンをABI境界でパーキングしておき、ストリームの準備ができたら再開する形でスタックフルコルーチンを走らせます — コンポーネントモデルのasync ABIが、スタックレスとスタックフルの両方のコルーチンを受け入れるよう設計されているからこそ可能なことです。発表記事の表現どおり「これから数週間から数か月かけて」個々のプロジェクトが0.3サポートを表明していくでしょう。それは、今日の時点ではほとんどがまだだということを意味します。

スレッドとストリームスプライシングは0.3.0には入りませんでした。 どちらも0.3.x以降のリリースに先送りされました。協調的スレッドはコンポーネントモデルのレベルで実装され(WASIより下の層です)、Rustコンポーネントがstd::thread::spawnを使えば、WITで何もしなくてもサポートを受けられます。進捗としては、wasi-libcのpthreadsサポートがほぼ終わっており、LLVMパッチがちょうどランドしたところで、Wasmtimeにはフィーチャーフラグの裏で動く実装があります。協調的が先、プリエンプティブが後という順です。ストリームスプライシング(途中でコピーせずにストリームを別のストリームへつなげる)はストリーミング性能にとって重要ですが、文書の表現では単に締め切りに近すぎてP3自体には載せられませんでした。

LLVMがもっとも長いリードタイムになりえます。 1.0へ向かうABI作業には、C ABIレベルのmultivalue返り値が絡んでいますが、LLVMはまだC ABIレベルでmultivalueをサポートしていません。正確なABI提案は出ていますが、それをアップストリームに乗せることがこの作業の中でもっとも時間がかかりうると、記事自身が認めています。参考までに、LLVMは6か月ごとにリリースされ、Rustに何かを入れてstableに至るまでおよそ9週間かかります。このパイプライン全体をBytecode Allianceがコントロールしているわけではありません。

0.2からの移行ドキュメントはまだありません。 実例付きの詳細な0.2→0.3移行ガイドは、コンポーネントモデルのドキュメントで「準備中」です。

「6桁速くなる」という主張について

発表記事にはこんな一文があります。middlewareワールドが可能にするサービスチェイニングのおかげで、互いに頻繁に通信するマイクロサービスコンポーネントが、ネットワークを経由せず同じプロセス内で直接合成できるようになり、そのため「ほとんどのマイクロサービスで、他のマイクロサービスを呼び出す時間がミリ秒からナノ秒へ、6桁(six orders of magnitude)縮む」という主張です。

この文はこう読むべきです。

  • これは測定値ではなく見通しです。原文は未来形("will reduce")で、ベンチマークも測定条件も提示されていません。
  • 比較対象がフェアではありません。「ネットワークホップ」と「同じプロセス内の関数呼び出し」を比べれば、当然桁が違ってきます。これはWASI 0.3が速いからではなく、ネットワークを経由しないから出てくる数字です。同じ論理なら、二つのマイクロサービスを一つのバイナリに合体させることでも6桁縮まります。
  • そして直前の節で見たとおり、同じ組織の別の記事は、今のところ同期コンポーネント間の呼び出しに3.5倍のオーバーヘッドがあると書いています。この二つの文は矛盾ではありません(一方はネットワーク除去の利得、もう一方は呼び出し規約のオーバーヘッド)— ただし、並べて見て初めてバランスが取れます。

ブラウザ側の数字も同じ基準で読むのがよいでしょう。Mozillaのライアン・ハントによる実験では、DOM変更の多いWASM VDOMの再調整ループが、JavaScriptのグルーレイヤーを迂回してブラウザAPIを直接呼び出すことで、2倍近い速度向上を得られたとのことです。記事自体が「実際の利得はワークロードによって異なる」と但し書きをつけています。特定のワークロードにおける上限であって、一般的な期待値ではありません。

WASM陣営が長く悩まされてきたのは、まさにこの点です。技術は本当に良いのに、主張がいつも半拍先を行ってしまう。今回のリリースの本当の成果はナノ秒ではなく、asyncを使いながらもコンポーネントを合成できるようになったことであり、それだけで十分に大きなニュースです。

Component Model 1.0とWASI 1.0 — 日付はない

よく混同される部分なので整理すると、Component Model 1.0とWASI 1.0は別のマイルストーンです。そして順序があります — WASI 1.0は、コンポーネントモデルが先に1.0へ到達して、そこから続くものです。

Luke WagnerがPlumbers Summitで語った比喩が、この関係をよく説明しています。コンポーネントモデルは常に存在するマイクロカーネルです — どんなホストでも動く基礎プリミティブを提供します。WASIはその上に乗るOSサービス(ネットワーキング、ストレージ、グラフィックス)で、マイクロカーネルの上のプロセスのように動き、機器によってあったりなかったりします。ブラウザを考えると理解しやすいです — ブラウザはどんなI/O APIが存在すべきかについて非常に強い意見を持っており、そのためWASIインターフェースはブラウザ上ではポリフィルとして動きます。一方コンポーネントモデル自体は計算プリミティブしか提供しないため、コアWASMの隣にネイティブで実装されえます。

しかしここには手ごわい関門があります。コンポーネントモデルは、少なくとも二つのブラウザエンジンにネイティブ実装されない限り、公式には1.0に到達できません。 現在の状況はこうです — Mozillaチームが最近WebAssembly CGの対面会議で性能結果を発表し、Chrome V8チームはコンポーネントモデル実装を評価するためのイシューを開きました。記事自身の表現が正確です — これは約束ではなく意味のあるシグナルです。

そこで1.0を勝ち取ろうとする戦略が興味深いのです。asm.js時代のプレイブックをそのまま使います — asm.jsは、ブラウザエンジンが機能使用のテレメトリで検知できるno-op文字列"use asm"を埋め込んでおき、そのデータで最適化の根拠を作り、結局WebAssembly自体へとつながりました。jcoは今、同じことをしています。jcoが生成するJavaScriptグルーが"use components"識別子を吐きます(jco 1.16.8で"use jco"から名前が変わりました)。jcoでトランスパイルされたコンポーネントの実使用が積み上がれば、それがネイティブ実装の根拠になる、という計算です。

まとめると、WASI 1.0の日付を語る一次資料はありません。 ロードマップページにもなく、1.0への道を扱った記事にもありません。ロードマップ文書自身が最初の行でくぎを刺しています — これは生きた文書であり、目標と見通しは暫定的で修正され得ると。(ネット上で「2026年末/2027年初め目標」のような数字を何か所かで見かけましたが、一次資料で確認できないため、ここには持ち込みません。)

確かなのはケイデンスだけです。WASIのポイントリリースは2か月ごと、その月の第一木曜日に、リリーストレインモデルで出ます — 「電車に乗る準備ができた」ものが何であれ、決まった周期で出るという意味です。0.3.0以降は、後方互換の0.3.xがこの電車に乗ります。予定されている貨物は、言語イディオムに統合されたキャンセル(cancellation)、ストリームの特殊化と最適化、ゼロコピーを広げる呼び出し元提供バッファ、そしてスレッドです。

では、今移行すべきか

もっとも重要な事実から。マイグレーションは必須ではありません。 0.3対応ランタイムを使うために0.3へ移る必要はありません。wasmtime serveは同じバイナリで0.3コンポーネントと0.2コンポーネントをコンポーネントごとにディスパッチして両方動かします。ロードマップも、実装が0.2と0.3を並行してサポートするか、0.2を0.3の上でポリフィル(仮想化)できると明記しています。P1モジュールもまだ動きますし、P2コンポーネントもまだ動きます — このチームはP1以来、セマンティックバージョニング、並行実装、WASM-to-WASMアダプタで安定性を保ってきており、その話は1.0以降も続きます。

今移行して価値がある場合

  • コンポーネントを実際に合成しており、その連鎖にasyncやストリーミングが絡んでいる。サンドイッチ問題ですでに痛い目を見ているなら、これがまさにその薬です。
  • HTTPミドルウェアをコンポーネントとして表現したい — 0.2のproxyワールドでは表現できなかったことが、middlewareワールドで一級になりました。
  • ストリームを途中で打ち切る消費者がいて、正常終了とエラーを区別する必要がある。
  • ランタイムをWasmtime 46+に固定でき、ゲスト言語がRustである。

まだ待つほうがよい場合

  • 同期呼び出し経路がホットパスである。3.5倍オーバーヘッドの修正はまだ計画段階です。
  • ゲスト言語がPython・C#・Cのような、バインディングのasyncサポートが進行中の言語である。
  • スレッドやストリームスプライシングが必要である — 0.3.xを待つ必要があります。
  • 単一のコンポーネントをホスト上だけで動かしている。合成をしないなら0.2で十分で、0.3が解く問題はあなたの問題ではありません。

移行すると決めたなら、実務上の落とし穴が一つあります。ツールチェーンのバージョンを一貫して固定してください。 コンポーネントモデルはカノニカルなインターフェース名を定義しており、それにより互換バージョン間のリンクは可能なのですが、すべてのツールがまだこのバージョン認識リンキングをサポートしているわけではありません。それまでは、Wasmtimeとバインディングジェネレータ(Rustならwit-bindgen、JavaScriptならjco)が同じWITバージョン0.3.0をターゲットにする必要があります。ずれると、インスタンス化の時点でわかりにくいwrong typeエラーとして飛び出します。この種のエラーは原因究明に半日消えるタイプなので、あらかじめ知っておく価値は大きいです。

残りのマイグレーション手順はおおむね機械的です — wasi:io型を上のマッピング表どおりに変え、ワールドを適切に選び(CLIならwasi:cli/command、HTTPサーバならwasi:http/service、ミドルウェアならwasi:http/middleware)、start-foo / finish-fooの呼び出し箇所をasync funcに変えればよいのです。先に述べたwasi:randommax-lenのような意味変化だけ、機械的変換から取りこぼさないようにすればよいのです。

おわりに

まとめるとこうです。WASI 0.3.0は2026年6月11日に批准され、asyncをwasi:ioパッケージからコンポーネントモデルのカノニカルABIへと下ろしました。wasi:ioは削除され、pollable / input-stream / output-stream / pollasync funcとストリーム・フューチャーのプリミティブに置き換わりました。Wasmtime 46が6月22日にこれをデフォルトで有効にしました。

このリリースの価値は速度ではなく、合成です。0.2ではコンポーネントがasyncを使った瞬間に組み合わせ不可能になり — コンポーネントモデルの存在理由を真正面から蝕む欠陥でした — 0.3はイベントループの所有権をホストに移すことでそれを直しました。それだけで十分に大きなニュースです。ナノ秒のマーケティングは無くてもよかったのです。

同時に正直に言えば、同期呼び出し経路は今およそ3.5倍のオーバーヘッドを負っており、その修正はまだ計画であり、ゲストツールチェーンは言語ごとに進行中であり、スレッドとスプライシングは次の電車であり、Component Model 1.0はブラウザエンジン二つのネイティブ実装を待っており、誰もWASI 1.0の日付を約束していません。

だから実務上の判断は単純です。コンポーネントを合成していて、その連鎖にasyncがあるなら、今見てください。 そうでなければ0.2は動き続けますし、ポリフィルもありますし、次の電車は2か月後に来ます。このプロジェクトでもっとも信頼できるのは、ロードマップの日付ではなく、リリーストレインのケイデンスです — そして今回は、予告したものが実際に11日で届いたという事実です。

参考資料