約2年ぶりにEmscriptenを使ってハマった点など

# はじめに
この記事は、以前作った[z-music.js](https://github.com/toyoshim/z-music.js)を最新のEmscriptenを使ってコンパイルし直した際に出くわした問題をまとめています。「以前」というのは2017末くらいの事。WebAssembly対応が始まった頃に実験的にコンパイルしたのが最後で、公式にはasm.js版がリリース版といった感じでした。「最新」としているのは、2020年7月時点での最新版です。

もともとの動機はwasm対応ではなく、Web Audioで自動再生ができなくなった事に対する回避策の追加で、久々に環境を整えるところからスタートしたら「おや、動かんぞ」という事で四苦八苦するところから始まります。

# ハマった点
## Pointer_stringifyの廃止
C/C++側からJavaScriptの関数を呼び出す際、JavaScript側で受け取ったポインタからC文字列を復元するための便利関数として`Module.Pointer_stringify`という関数が存在していたのですが、これが廃止になっていました。もともと文字列を雑に扱っていた関数だったので、これが文字コードを考慮した関数に置き換わったのは歓迎すべき点かと。

新しい関数は[preamble.js](https://emscripten.org/docs/api_reference/preamble.js.html)により提供される、`AsciiToString`、`UTF8ToString`あたり。C/C++側でUTF8を使ってるなら良いのですが、今回のケースのようにレガシーなSJIS環境から持ってくる場合には、真面目にSJISからUnicodeに変換してあげるか、強引に`AsciiToString`に渡して日本語は雑に処理するか、ですね。Unicode変換自体、地雷が多いので、今回は雑に処理しました。日本語ファイル名の扱いでバグるのは諦めるとして、メッセージ表示に関しては機種依存文字、エスケープシーケンス、ユーザー定義文字などが使われていたので、適当に正規表現で置換して対処してます。

## -s WASM=1がデフォルト
ハマってはいないですが、気になった変更点として、デフォルトのリンクモードがasm.jsではなくwasmになってました。今まではwasm build時に`-s WASM=1`を指定していたのですが、今回はasm.js build時に`-s WASM=0`の指定が必要でした。

## Module.argumentsが変更できない
以前は、`Module.preInit`で`addRunDependency`を呼んでEmscripten側のコードを待機させ、その間に`Module.arguments`を設定・変更。`removeRunDependency`で待機解除する事で、指定した引数でEmscripten側のコードを実行する事ができました。

今回のバージョンではこのタイミングで更新しても間に合わない！というか、なぜか`preInit`よりも前のタイミングで`Module.arguments`のコピーを作り、`removeRunDependency`後の実行開始時には`Module.arguments`を確認せず、事前コピーした内容を参照する、といった動作に変更になっていました。他の実行環境（nodeとか）との兼ね合いで修正されたコードがたまたまそういう挙動になっているのかも。Emscriptenで作るバイナリは大きい事も多く、実行パラメータが決まるまで読み込みを遅延させるのは得策ではないため、読み込みつつ`Module.arguments`相当の事ができないか試してみました。

```js
var Module = {
  // Default arguments.
  arguments: [],
  preInit: function () {
    Module.addRunDependency("initialize");
  },
  // Dirty hacks to replace previously evaluated Module.arguments during
  // Module.run().
  onRuntimeInitialized: function () {
    // Resets |calledRun| as this was already set in doRun().
    calledRun = false;
    // Resets this function so that this should not be called recursively during
    // the following run().
    Module.onRuntimeInitialized = null;
    // Calls run() again, but with the revised arguments.
    run(Module.arguments);
    // Restores the original |shouldRunNow| so that doRun() does not call main()
    // again with the original arguments after this function finishes.
    // |calledRun| should be already restored during the run() call above.
    shouldRunNow = false;
  }
};

Module.arguments = [...];
Module.removeRunDependency("initialize");
```

あまり行儀のよう方法ではなく、内部のコードを触ってるので、今後のバージョンでまた動かなくなる可能性は高いですが。

`addRunDependency`するまでは同じで、その他に`onRuntimeInitialized`にもフックをかけます。これは`removeRunDependency`後にC側のmainを呼ぶ直前に呼ばれます。

ここでまず`calledRun`を解除。このフック自体が`run`から呼ばれるのですが、その`run`を内部からもう一度呼ぶためのトリックです。この解除をしないと`run`が何もせずに終了します。

続けてフック自体を削除。これは自身の呼び出す`run`が再帰的にフックを呼び出すのを避けるためです。

続けて`run`を新たな引数で呼び出し。これにより少し前に呼ばれていたはずの`run`から新しい引数を用いて安全にやり直しができます。

最後に`shouldRunNow`を解除して復帰。これにより、もともとこのフックを呼んでいた最初の`run`が、このフックから戻るとmainを呼び出さずにすぐに終了します。

## Web XRとrequestAnimationFrame
もう1つ今時な修正をしようとしてハマったのがWeb XR対応。Emscriptenでメインループを回そうと思った時は、[emscripten_set_main_loop()](https://emscripten.org/docs/api_reference/emscripten.h.html#c.emscripten_set_main_loop)をfps=0で呼び出すことで登録した関数が`window.requestAnimationFrame`から定期的に呼び出されるようになります。

ところがWeb XRでXRSessionを開始した場合、`window.requestAnimationFrame`は実行を停止するため、代わりに`XRSession.requestAnimationFrame`を使いメインループを実行する必要があります。polyfillで実行しているぶんにはwindow側のループが停止しないため気づかないのですが、実機に持っていったら突然うずまきアイコンでハングアップ……何が起きているんだろう……と悩みました。

実機だけを考えた場合の対処はやるべき事をやるだけ。
`XRSession.requestsAnimationFrame`を使ってXR用のメインループを書き、そこから`Browser.mainLoop.runner`を呼び出せば登録されたメインループ関数が動きます。ただ、デスクトップでWeb XRのpolyfillを経由して実行している際などは、window側のループも停止しないため、毎フレーム2回ずつメインループが呼び出されるようになってしまいます。また、`XRSession.requestAnimationFrame`自体が`window.requestAnimationFrame`を使ってpolyfillされているため、

```js
window.requestAnimationFrame = session.requestAnimationFrame.bind(session);
```

みたいな事をやると再帰呼び出しで無限ループ。このあたりは状況に合わせてうまく対処する必要がありそうです。自分の場合は`window.requestAnimationFrame`を予め乗っ取って最後のRequest IDを保存するようにし、XR開始時に`window.cancelAnimationFrame`で明示的にwindow側のループを止めることにしました。

# まとめ
以上、ざっくりと。Module.argumentsなんかは小細工してないで公式に問い合わせて安定した方法を提供してもらえよって感じですけど、ひとまずはメモ書きで。落ち着いたらまた考えます。