概要
Asyncify.cpp は、WebAssembly モジュールに「一時停止・再開」機能を付与するパス(変換処理)を実装している。
これにより、C/C++ などの同期的なコードを、JS などの非同期イベント駆動環境でも「途中で止めて、後から再開」できるようにする。
1. 目的
- WebAssembly の関数呼び出しやローカル変数の状態を保存・復元し、途中で「一時停止」や「再開」ができるようにする。
- 例: ファイル読み込みやネットワーク待ちなど、非同期イベントで一時停止し、完了後に元の関数の続きから再開できる。
2. 主な仕組み・流れ
2.1. Asyncifyの全体像
Asyncifyは、WebAssemblyバイナリを変換し、コールスタックやローカル変数の状態を保存・復元できるようにするパスである。これにより、もともと同期的なコード(C/C++など)を、非同期イベント駆動環境(JSなど)でも「一時停止・再開」できるようにする。
AsyncifyはBinaryenのパスとして実装されており、wasm-opt --asyncify などで利用できる。変換後のWasmには、スタックの巻き戻し(unwind)・再構築(rewind)を制御するためのAPIが自動的に追加される。
2.2. 状態管理とAPI
__asyncify_stateというグローバル変数で「今の状態(通常/アンワインド中/リワインド中)」を管理する。- 0: 通常実行
- 1: アンワインド(unwinding, スタックを巻き戻して一時停止)
- 2: リワインド(rewinding, スタックを再構築して再開)
__asyncify_dataというグローバル変数で「保存用データ構造体」のポインタを管理する。
Asyncifyが自動生成・エクスポートする主なAPIは以下の通り:
asyncify_start_unwind(ptr):巻き戻しを開始する。ptrは保存用データ構造体の先頭アドレス。asyncify_stop_unwind():巻き戻しを終了する。asyncify_start_rewind(ptr):再構築を開始する。ptrは保存用データ構造体の先頭アドレス。asyncify_stop_rewind():再構築を終了する。asyncify_get_state():現在の状態を返す。
データ構造とメモリレイアウト
asyncify_start_unwindやasyncify_start_rewindに渡すポインタは、Wasmのリニアメモリ上の構造体を指す必要がある。構造体のレイアウトは以下の通り:
- オフセット0: i32(asyncify stackの開始アドレス)
- オフセット4: i32(asyncify stackの終了アドレス)
この領域の直後に、実際のasyncify stack(状態保存用のメモリ領域)を確保する。スタックサイズが小さすぎるとunreachable例外が発生するため、十分なサイズを確保する必要がある。
2.3. コールスタックの巻き戻し・再構築
- 一時停止したい箇所で
asyncify_start_unwindを呼び出すと、Wasmランタイムはコールスタックとローカル変数の状態を保存しながら「巻き戻し」を開始する。 - その後、
asyncify_stop_unwindで巻き戻しを終了し、Wasmの実行が一時停止する。 - 再開時は
asyncify_start_rewindを呼び、保存された状態をもとに「再構築(リワインド)」を行う。 asyncify_stop_rewindで再構築を終了し、元の関数の続きから実行が再開される。
なぜ2回呼ぶ必要があるのか
巻き戻し・再構築の過程では、同じ関数が2回呼ばれることになる。1回目は通常実行、2回目はリワインドのため。関数内で「今が巻き戻し中か再構築中か」を判定し、適切な分岐を行う必要がある。
2.4. 例:純粋なWebAssemblyでの利用
(module (memory 1 1) (import "spectest" "print" (func $print (param i32))) (import "asyncify" "start_unwind" (func $asyncify_start_unwind (param i32))) (import "asyncify" "stop_unwind" (func $asyncify_stop_unwind)) (import "asyncify" "start_rewind" (func $asyncify_start_rewind (param i32))) (import "asyncify" "stop_rewind" (func $asyncify_stop_rewind)) (global $sleeping (mut i32) (i32.const 0)) (start $runtime) (func $main (call $print (i32.const 1)) (call $sleep) (call $print (i32.const 3)) ) (func $sleep (if (i32.eqz (global.get $sleeping)) (block (global.set $sleeping (i32.const 1)) (i32.store (i32.const 16) (i32.const 24)) (i32.store (i32.const 20) (i32.const 1024)) (call $asyncify_start_unwind (i32.const 16)) ) (block (call $asyncify_stop_rewind) (global.set $sleeping (i32.const 0)) ) ) ) (func $runtime (call $main) (call $asyncify_stop_unwind) (call $print (i32.const 2)) (call $asyncify_start_rewind (i32.const 16)) (call $main) ) )
この例では、mainを2回呼ぶことで、途中で一時停止し、外部から再開できるようになっている。
2.5. 最適化とオーバーヘッド
- Asyncifyを有効にすると、Wasmバイナリのサイズは1.5~2倍程度になることが多い。
- 実行速度も最大で2倍程度遅くなるが、実際に一時停止・再開を多用しない限り、オーバーヘッドは限定的。
- importリスト(どのimportが一時停止を引き起こすか)を明示することで、不要な関数の変換を避け、サイズ・速度のオーバーヘッドを大幅に削減できる。
- indirect call(間接呼び出し)が多い場合、静的解析が難しくなり、オーバーヘッドが増える。
- 最適化時は
wasm-opt -O1 --asyncifyのように、Asyncifyと同時に最適化パスを有効にすることが推奨される。
2.6. 注意点
- asyncify stackのサイズが不足すると
unreachable例外が発生するため、十分な領域を確保すること。 - 最適化レベルによっては、関数のインライン展開などでAsyncifyの動作が乱れる場合がある。
-O1程度が安全。 - importリストやindirect callの扱いを適切に設定することで、オーバーヘッドを最小化できる。
(参考: Pause and Resume WebAssembly with Binaryen's Asyncify)
3. 主なクラス・構造
| クラス名 | 役割 |
|---|---|
ModuleAnalyzer |
どの関数が「状態を変える(アンワインド/リワインドを開始する)」可能性があるかを解析する |
AsyncifyFlow |
関数本体の制御フローを書き換え、「一時停止・再開」に対応できるようにする |
AsyncifyLocals |
ローカル変数の保存・復元処理を挿入する |
FakeGlobalHelper |
一時的な値の保存用に「偽グローバル変数」を使う仕組みを提供する |
4. 変換の例
例えば、
x = x + 1; bar(x); x = x / 2;
のようなコードがあった場合、
- 「bar(x)」の呼び出しで一時停止する可能性があるなら、
- その前後で「状態チェック」や「保存・復元処理」が自動的に挿入される。
5. オプション・カスタマイズ
- どの関数・インポートが一時停止を引き起こすかをリストで指定できる。
- 最適化レベルやアサート挿入、詳細なログ出力などもオプションで制御可能だ。
6. まとめ
Asyncify.cpp は、WebAssembly の関数を「途中で一時停止・再開できる」ように自動変換するパスである。
などを行い、C/C++ などの「同期的なコード」を JS などの「非同期イベント駆動環境」でも自然に動かせるようにする。