Emscripten ランタイム環境

Emscripten ランタイム環境は、ほとんどの C/C++ アプリケーションが想定する環境とは異なります。Emscripten は、これらの違いを抽象化し、軽減するために努力しており、一般的にコードはほとんど変更なしでコンパイルできます。

この記事では、いくつかの違いと、その結果生じる API の制限 について詳しく説明し、C/C++ コードに必要な変更点を概説します。

入出力

Emscripten は、ブラウザ環境用に Simple DirectMedia Layer API (SDL) を実装しており、オーディオ、キーボード、マウス、ジョイスティック、グラフィックハードウェアへの低レベルアクセスを提供します。SDL を使用するアプリケーションは、通常、ブラウザで実行するために入出力の変更は必要ありません。

さらに、glutglfwglewxlib のサポートもより限定的に提供しています。

SDL やその他の API を使用しないアプリケーションは、入出力に Emscripten 固有の API を使用できます。

  • html5.h は、キー、マウス、ホイール、デバイスの向き、バッテリーレベル、振動など、ネイティブコードから HTML5 イベントと対話するための Emscripten の低レベルグルーバインディングを定義します。

  • マルチメディアとグラフィックス API ( OpenGLEGL など )。

ファイルシステム

多くの C/C++ コードは、ローカルファイルシステムのコードにアクセスするために、libc および libcxx の同期ファイルシステム API を使用しています。これは、ブラウザがホストシステムのファイルへの直接アクセスをコードに許可せず、JavaScript が Web ワーカーの外部で非同期ファイルアクセスのみをサポートしているため、問題があります。

Emscripten は、libclibcxx の実装と仮想ファイルシステムを提供しているため、通常の C/C++ コードを変更せずにコンパイルして実行できます。ほとんどの開発者は、実行時に仮想ファイルシステムにプリロードするために パッケージ化 するファイルセットを指定するだけで済みます。

仮想ファイルシステムを使用すると、上記の制限が回避されます。ファイルデータはコンパイル時にパッケージ化され、コンパイルされたコードの実行が許可される前に、非同期 JavaScript API を使用してファイルシステムにダウンロードされます。次に、コンパイルされたコードは、実際にはプログラムメモリへの呼び出しである「ファイル」呼び出しを行います。

デフォルトのファイルシステム (MEMFS) は、ファイルをメモリに保存するため、ページをリロードすると変更は失われます。ファイルの変更をより永続的に保存する必要がある場合は、開発者は IDBFS ファイルシステムをマウントできます。これにより、データをブラウザに永続化できます。node.js でコードを実行する場合、開発者は NODEFS をマウントして、コードにローカルファイルシステムへの直接アクセスを提供できます。

Emscripten には、非同期ファイルアクセス をサポートするための API もあります。

詳細と例については、ファイルとファイルシステム を参照してください。

ブラウザのメインループ

ブラウザのイベントモデルでは、協調型マルチタスクを使用します。各イベントには実行する「ターン」があり、その後、他のイベントを処理できるようにブラウザに制御を返す必要があります。HTML ページがハングする一般的な原因は、完了せずにブラウザに制御を返さない JavaScript です。

グラフィカル C++ アプリは、通常、無限ループで実行されます。ループの各反復内で、アプリはイベント処理、処理、レンダリングを実行し、フレームレートを一定に保つための遅延 ("wait") が続きます。この無限ループは、他のコードを実行できるように制御をブラウザに戻す方法がないため、ブラウザ環境では問題になります。しばらくすると、ブラウザはページがスタックしていることをユーザーに通知し、停止または閉じることができるようにします。

同様に、WebGL のような JavaScript API は、現在の「ターン」が終了したときにのみ実行でき、その時点で自動的にレンダリングとバッファのスワップを行います。これは、バッファを手動でスワップする必要がある OpenGL C++ アプリとは対照的です。

C/C++ での非同期メインループの実装

この問題に対する標準的な解決策は、メインループの 1 回の反復 (「遅延」を含まない) を実行する C 関数を定義することです。ネイティブビルドの場合、この関数は無限ループで呼び出すことができ、動作は事実上変わりません。

Emscripten コンパイル済みのコード内では、emscripten_request_animation_frame_loop() を使用して、フレームをレンダリングする適切な頻度でこの同じ関数を呼び出すように環境を取得します (つまり、ブラウザが 60fps でレンダリングする場合、1 秒あたり 60 回呼び出します)。反復はまだ「無限に」実行されますが、反復間では他のコードを実行できるようになり、ブラウザはハングしなくなります。

通常、2 つのケースに対して #ifdef __EMSCRIPTEN__ を含む小さなセクションがあります。例えば

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

// Our "main loop" function. This callback receives the current time as
// reported by the browser, and the user data we provide in the call to
// emscripten_request_animation_frame_loop().
bool one_iter(double time, void* userData) {
  // Can render to the screen here, etc.
  puts("one iteration");
  // Return true to keep the loop running.
  return true;
}

int main() {
#ifdef __EMSCRIPTEN__
  // Receives a function to call and some user data to provide it.
  emscripten_request_animation_frame_loop(one_iter, 0);
#else
  while (1) {
    one_iter();
    // Delay to keep frame rate constant (using SDL).
    SDL_Delay(time_to_next_frame());
  }
#endif
}

より機能豊富な API は emscripten_set_main_loop() で提供されています。これにより、関数を呼び出す頻度などを指定できます。

SDL を使用する場合、多くの場合、メインループを設定する必要があります。ただし、

単一のフレームをレンダリングして停止する場合を除きます。また、注意してください

  • 現在の Emscripten 実装の SDL_QUIT は、emscripten_set_main_loop() を使用すると機能します。ページがシャットダウンされると、メインループへの最後の直接呼び出しが強制的に行われ、SDL_QUIT イベントを認識する機会が与えられます。メインループを使用しない場合、アプリはこのイベントに気づく機会を得る前に閉じます。

  • ページがシャットダウンされるとき (onunload) にできることには制限があります。アラートの表示などの一部のアクションは、この時点ではブラウザによって禁止されています。

Asyncify を使用してブラウザに処理を譲る

別のオプションとして、Asyncify を使用する方法があります。これは、emscripten_sleep() を呼び出すだけでブラウザのメインイベントループに戻れるようにプログラムを書き換えます。ただし、この書き換えはサイズと速度のオーバーヘッドを引き起こすことに注意してください。一方、前述の emscripten_request_animation_frame_loop / emscripten_set_main_loop はそうではありません。

実行ライフサイクル

Emscripten でコンパイルされたアプリケーションがロードされると、最初に preloading フェーズでデータの準備を開始します。プリロード 用にマークしたファイル ( emcc --preload-file を使用するか、JavaScript から FS.createPreloadedFile() を使用して手動で) は、この段階で設定されます。

addRunDependency() を使用して追加の操作を追加できます。これは、コンパイルされたコードが実行される前に実行されるべきすべての依存関係のカウンターです。これらの完了に伴い、removeRunDependency() を呼び出して、完了した依存関係を削除できます。

通常、追加の操作を追加する必要はありません。プリロードはほぼすべてのユースケースに適しています。

すべての依存関係が満たされると、Emscripten は run() を呼び出し、それが main() 関数を呼び出します。main() 関数は初期化タスクを実行するために使用する必要があり、多くの場合、emscripten_set_main_loop() ( 上記で説明したように ) を呼び出します。その後、メインループ関数が要求された頻度で呼び出されます。

メインループの動作にはいくつかの方法で影響を与えることができます。

  • emscripten_push_main_loop_blocker() は、ブロッカーが完了するまでメインループを **ブロック** する関数を追加します。

    これは、例えば、新しいゲームレベルのロードを管理するのに役立ちます。レベルが完了した後、関連する各アクション(ファイルの解凍、データ構造の生成など)に対してブロッカーをプッシュできます。すべてのブロッカーが完了すると、メインループが再開され、ゲームは新しいレベルを実行する必要があります。この関数を emscripten_set_main_loop_expected_blockers() と組み合わせて使用して、ユーザーに進捗状況を知らせることもできます。

  • emscripten_pause_main_loop() はメインループを一時停止し、emscripten_resume_main_loop() はメインループを再開します。これらはブロッカー関数の低レベル(あまり推奨されない)代替手段です。

  • emscripten_async_call() を使用すると、特定の時間間隔の後に関数を呼び出すことができます。これは、(デフォルトで)requestAnimationFrame を使用し、特定の時間間隔が要求された場合は setTimeout を使用します。

ブラウザ実行環境リファレンス (emscripten.h) には、実行を制御するための他の多くのメソッドが記載されています。

Emscripten メモリ表現

asm.js と WebAssembly の両方で、Emscripten はネイティブアーキテクチャと同様の方法でメモリを表現します。ポインタはメモリへのオフセットを表し、構造体は通常と同じ量のアドレス空間を使用します。

WebAssembly では、これはその目的のために設計された WebAssembly.Memory を使用して行われます。asm.js では、Emscripten は単一の 型付き配列を使用し、異なるビューが異なる型へのアクセスを提供します(32ビット符号なし整数の場合は HEAPU32など)。

Emscripten は過去に他のメモリ表現を試みましたが、最終的には上記のように JS および asm.js に「型付き配列モード 2」アプローチを採用し、その後 WebAssembly が同様のものを実装しました。