WebAssembly JSPIとは?Promiseを待つWasmの仕組みと最小構成

積み重なる処理層と接続記号でWebAssembly JSPIを表した図 WebAssembly・非同期処理
カテゴリー
WebAssembly・非同期処理
公開日
2026.09.09

はじめに

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待ってから2142にする最小プログラムを作ります。C側は通常の同期関数のように書き、待機中もブラウザのメインスレッドを止めないことを確認します。

  • Emscripten 6.0.8(この記事での検証版)
  • JSPI対応ブラウザ
  • Python 3(ローカルHTTPサーバー用)
  • main.cを保存できる空の作業フォルダー

ブラウザの対応状況は変化するため、バージョン番号だけで判定せず、次節のWebAssembly.SuspendingWebAssembly.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移植の負担を減らす選択肢として使います。

参考リンク

タイトルとURLをコピーしました