kanejaku.org

Emscripten Modularize

4 Jul 2018

Emscripten で出力した JS/wasm を Web アプリの一部として組み込む場合、これらのリソースの読み込みをできるだけ遅延させたいことがある。例えばユーザーがあるボタンをクリックしたときのみ wasm が提供する機能を使いたいとする。ボタンが押されるまでは wasm の読み込みは避けたい。

Emscripten はデフォルトでは Web アプリに組み込んだり遅延ロードさせるにはあまり適さない JS/wasm を出力する。Module オブジェクトはシングルトンとして生成されるし、wasm の場所もランタイムの JS と同じパスにあると仮定されてしまう。後者はランタイムの JS を読み込む前に適宜 locateFile とかを設定すればよいのだけど読み込み順序に依存するコードはアプリに組み込むには使いづらい。

Emscripten のフロントエンドである emcc に -s MODULARIZE=1 を渡すと Web アプリに組み込みやすい形で JS/wasm を出力してくれるようになる。このオプションを渡すと Module はシングルトンオブジェクトではなくコンストラクタ関数として生成される。これにより JS/wasm の読み込みタイミングと Module オブジェクトの生成タイミングを分離できて、例えばランタイムの JS は他の JS とバンドリングしておいて、wasm のロードは後で行う、といったことが可能になる。

以下は -s MODULARIZE=1 で生成した wasm をボタンが押されたタイミングでロード/実行する例:

function onWasmBinaryReady(wasmBinary) {
  return new Promise(resolve => {
    let mod = null;
    const args = {
      wasmBinary: wasmBinary,
      onRuntimeInitialized: () => {
        resolve(mod);
      }
    };
    mod = new Module(args);
  });
}

const wasmPath = "a.out.wasm";

const button = document.getElementById("button");
button.addEventListener("click", () => {
  fetch(wasmPath)
    .then(res => res.arrayBuffer())
    .then(buf => new Uint8Array(buf))
    .then(wasmBinary => onWasmBinaryReady(wasmBinary))
    .then(mod => {
      // Call mod.ccall(...)
    });
});