- カテゴリー
- WebAssembly・非同期処理
- 公開日
- 2026.09.09
Contents
はじめに
C/C++のread()のような処理は「結果が返るまで次へ進まない」書き方が一般的です。一方、ブラウザのfetch()はPromiseを返し、メインスレッドを止めません。この差を埋めるためのWebAssembly APIがJavaScript Promise Integration(JSPI)です。
JSPIは、Promiseが未完了の間だけWebAssemblyの計算を中断し、解決後に続きから再開します。JavaScript全体を同期化する機能ではなく、WebAssemblyとJavaScriptの境界に限定された仕組みです。
この記事は、C/C++からWebAssemblyを作ったことがあり、ブラウザの非同期APIを同期的なCコードから利用したい人を対象にしています。JSPIはWebAssembly提案プロセスのPhase 5ですが、利用可否は実行環境で異なるため、必ずAPIを機能検出します。
作るものと必要環境
300ms待ってから21を42にする最小プログラムを作ります。C側は通常の同期関数のように書き、待機中もブラウザのメインスレッドを止めないことを確認します。
- Emscripten 6.0.8(この記事での検証版)
- JSPI対応ブラウザ
- Python 3(ローカルHTTPサーバー用)
main.cを保存できる空の作業フォルダー
ブラウザの対応状況は変化するため、バージョン番号だけで判定せず、次節のWebAssembly.SuspendingとWebAssembly.promisingを確認してください。
2つのAPI
const suspendingImport = new WebAssembly.Suspending(asyncFunction);
const promisingExport = WebAssembly.promising(wasmExport);
Suspending:Promiseを返し得るJavaScriptのimportを印付けするpromising:中断され得るWasm exportを、Promiseを返す関数に包む
この2つは対です。中断可能なimportへ到達するexportをpromising()で包まないと、実行時エラーになる可能性があります。
機能検出
const supportsJSPI =
typeof WebAssembly.Suspending === "function" &&
typeof WebAssembly.promising === "function";
if (!supportsJSPI) {
document.querySelector("#status").textContent =
"この環境ではJSPIを利用できません。";
}
ブラウザ名やバージョン文字列だけで決めず、実際のAPIを確認します。
Emscriptenで最小例を作る
main.cを作ります。
#include <emscripten.h>
#include <stdio.h>
EM_ASYNC_JS(int, wait_for_value, (int value), {
await new Promise(resolve => setTimeout(resolve, 300));
return value * 2;
});
int main(void) {
puts("待機前");
int result = wait_for_value(21);
printf("結果: %d\n", result);
return 0;
}
この記事で検証したEmscripten 6.0.8でコンパイルします。
emcc main.c -O2 -sJSPI -o index.html
python -m http.server 8000
http://localhost:8000/を開くと、「待機前」の後、約300msで結果: 42が出ます。setTimeout中にブラウザのメインスレッドをブロックしません。
手動で組み立てる場合の形
独自ツールチェーンでWasmを作る場合、importとexportを次のように包みます。
const imports = {
host: {
readValue: new WebAssembly.Suspending(async (id) => {
const response = await fetch(`/api/value/${id}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return Number(await response.text());
}),
},
};
const { instance } = await WebAssembly.instantiateStreaming(
fetch("app.wasm"),
imports,
);
const run = WebAssembly.promising(instance.exports.run);
const result = await run(7);
instance.exports.runは本物のWebAssembly exportである必要があります。通常のJavaScript関数をpromising()へ渡す用途ではありません。
例外とキャンセル
import側のPromiseがrejectすると、包まれたexportが返すPromiseもrejectします。必ずtry...catchで扱います。
try {
const value = await run(7);
console.log(value);
} catch (error) {
console.error("Wasm処理を継続できません", error);
}
JSPIが自動的にfetchをキャンセルするわけではありません。キャンセルが必要ならAbortControllerをimport側の設計へ組み込み、Wasmの状態と整合させます。
向いている用途と向かない用途
向いているのは、同期I/O前提の既存C/C++コードをブラウザへ移植し、非同期Web APIへ接続する場面です。新規JavaScriptだけで完結するアプリなら、通常のasync/awaitで十分です。
中断中もWasmの状態を保持するため、大量の同時中断、深い再帰、メモリ制約は実測します。JSPIを使えばネットワークが速くなるわけでもありません。
動作確認
- APIがない環境で明確にフォールバックする
- 待機中もボタンやアニメーションが反応する
- Promise解決後にWasmの続きが実行される
- rejectが呼出側のPromiseへ伝わる
- 同時実行とキャンセルをアプリの要件に沿って確認する
トラブル対処
- APIがundefined:JSPIを必須にせず、対応案内または別の非同期実装へ切り替えます。
- SuspendError:中断可能なimportへ到達するWasm exportをpromisingで包んだか確認します。
- 待機しない:Emscriptenの版、-sJSPI、EM_ASYNC_JSの対象を確認します。
まとめ
JSPIは、同期的なWasmコードとPromise中心のWeb APIをつなぐ境界機能です。Suspendingで非同期importを、promisingで中断され得るexportを包みます。新しい非同期モデルを発明するのではなく、既存Wasm移植の負担を減らす選択肢として使います。

