Open3

Emscripten vs 同期的なコードの戦い

かめのこにょこにょこかめのこにょこにょこ

メインスレッド上で Asyncify

Asyncify を使うことで、ゲームループなどの無限ループを JavaScript 側にとってコルーチンみたいに取り扱えるようにしたり、JavaScript 側の非同期な処理を、C/C++ 側にとって同期的なコードに取り扱えるように、WebAssembly バイトコードを変換することができる。

#include <emscripten.h>
#include <stdio.h>

EM_ASYNC_JS(int, fetch_image, (), {
  await fetch("something.png");
  return 0;
});

int main() {
  // JavaScript 側の非同期処理の完了を待つことができる
  fetch_image();
  // fetch が完了してから呼ばれるコード
  return 0;
}

Asyncify の対象となる関数を直接的に呼び出す関数も、自動的に Asyncify 変換の対象となり、 Asyncify 変換処理が伝搬する。

// fetch_image は Asyncify 対象なので変換される。
int stub() {
  return fetch_image();
}

// stub も Asyncify 対象なので、これも変換される。
int main() {
  stub();
  return 0;
}

別の関数を間接的に呼び出す関数は、-sASYNCIFY_IGNORE_INDIRECT オプションが有効な時に限って、Asyncify 変換が行われる。しかし、このオプションを有効にした場合、呼び出される関数が Asyncify 変換の対象であるかどうかにかかわらず、変換処理が行われる。そのため、Asyncify 変換が必要でない関数も変換が行われ、コードサイズの肥大化が起こりやすい。

// fetch_image は Asyncify 対象なので変換される。
int stub() {
  return fetch_image();
}

void another() {
  // なんらかの Asyncify 変換のいらない処理
}

// -sASYNCIFY_IGNORE_INDIRECT が有効ならば変換される
int main() {
  int (*ptr)() = &stub;
  (*stub)();
  return 0;
}

// これも -sASYNCIFY_IGNORE_INDIRECT が有効ならば変換されてしまう
void some() {
    void (*ptr)() = &another;
    (*ptr)();
}

emscripten exception handling support と組み合わせると、try/catch のあるコードが間接呼び出しに変換されるため、爆発的にコードサイズが増えてしまう。
しかし、WebAssembly Exception Handing には未対応。

かめのこにょこにょこかめのこにょこにょこ

PROXY_TO_WORKER を使う

長所

  • -sPROXY_TO_WORKER を指定するだけでだいたい動く

短所

  • キーボード/マウスなどのイベント処理、WebGL/canvas をつかったグラフィカルな処理などのパフォーマンスが落ちる
    • ワーカースレッドから DOM まわりが全く触れず、メインスレッドにすべて移譲するため、メインスレッドとの通信コストが顕著に表れる
  • SharedArrayBuffer を使うため、Web サーバ側の設定が非常に面倒
かめのこにょこにょこかめのこにょこにょこ

PROXY_TO_WORKER + OffScreen Canvas を使う

長所

  • PROXY_TO_WORKER の便利良さを享受しつつ、WebGL/canvas をつかったグラフィカルな処理などのパフォーマンス低下も最小限に抑えられる

短所

  • ゲームループなどの無限ループと組み合わせることができない
    • JavaScript のイベントループに制御を戻して、初めて OffscreenCanvas の更新が行われるため
    • Asyncify と組み合わせると、 Asyncify のデメリットを引き継いでしまう
    • 画面更新以外の非同期処理はメインスレッドに移譲できるが、それでも面倒くささはある