動的リンク

このドキュメントはやや古く、現在更新中です。

Emscripten は、オブジェクトファイル(およびオブジェクトファイルを含む ar アーカイブ)の静的リンクをサポートしています。これにより、ほとんどのビルドシステムは、Emscripten をほとんど変更せずに使用できます(プロジェクトのビルドを参照)。

さらに、Emscripten は WebAssembly モジュールの動的リンク形式もサポートしています。これによりオーバーヘッドが増加する可能性があるため、最適なパフォーマンスを得るには静的リンクが推奨されます。ただし、このオーバーヘッドは特定のコマンドラインフラグを使用することで削減できます。詳細については以下を参照してください。

背景

動的リンクに入る前に、静的リンクについて説明しましょう。Emscripten のリンクモデルは、ほとんどのネイティブプラットフォームとは少し異なります。それを理解するために、ネイティブリンクモデルが以下の事実が当てはまる設定で動作することを考慮してください。

  1. アプリケーションはローカルシステム上で直接実行され、C および C++ 標準ライブラリなどのローカルシステムライブラリにアクセスできます。

  2. コードサイズは大きな問題ではありません。これは、システムライブラリがすでにシステムに存在するため、「hello world」を C++ で記述すると、C++ 標準ライブラリの iostream コードを大量に使用している場合でも小さくできるためです。また、コードサイズはコールドスタート時間に影響を与える可能性があるという点もあります。コードが多いほどディスクからのロードに時間がかかりますが、そのコストは一般的にそれほど大きくなく、最新の OS は、ロードが予想されるアプリのキャッシュなど、さまざまな方法でそれを軽減します。

Emscripten の場合、コードは通常 Web 上で実行されます。つまり、次のようになります。

  1. アプリケーションはサンドボックスで実行されています。動的にリンクするローカルシステムライブラリはありません。独自のシステムライブラリコードを出荷する必要があります。

  2. アプリケーションのコードはインターネット経由でダウンロードされるため、コードサイズは大きな懸念事項です。これは、ローカルマシンにインストールされたネイティブアプリよりも桁違いに遅くなります。

そのため、Emscripten はシステムライブラリを自動的に処理し、可能な限り小さくするためにデッドコード除去などを自動的に実行します。

ここでの追加要因は、Emscripten には JavaScript で記述された「js ライブラリ」(システムライブラリ)があることです。このようなシステムライブラリは、Web 上の API にアクセスする方法です。また、コンパイルされたコードと手書きのコードを同じページで接続するための便利な方法でもあります。これは、Emscripten がシステムライブラリを特別な方法で、特に、可能な限り多くの js ライブラリを削除し、実際に使用されているものだけを残すことができるように処理するもう1つの理由です。これも、外部依存関係のないスタンドアロンアプリを静的にリンクするコンテキストで最適に機能します。

動的リンクの概要

Emscripten の動的リンクは非常に簡単です。ソースコードからいくつかの個別のコード「モジュール」をビルドし、それらをランタイム時にリンクできます。リンクは基本的に、各モジュールの未定義シンボルを他のモジュールの定義済みシンボルに、最も簡単な方法で接続します。現在、いくつかの特殊なケースはサポートされていません。

システムライブラリは、そのような特殊なケースを含む、より高度なリンク機能をいくつか利用します。そのため、Emscripten は問題を次のように単純化しようとします。共有モジュールには 2 つのタイプがあります。

  1. システムライブラリがリンクされているメインモジュール

  2. システムライブラリがリンクされていないサイドモジュール

プロジェクトには正確に 1 つのメインモジュールが含まれている必要があります。その後、複数のサイドモジュールにランタイム時にリンクできます。このモデルは、他のことも単純化します。たとえば、シングルトンのメインモジュールのみに JavaScript 環境が含まれ、サイドモジュールは純粋な WebAssembly モジュールです。

この設計における 1 つの厄介な点は、サイドモジュールがメインモジュールが依存していなかったシステムライブラリに依存する可能性があることです。それに対処する方法については、以下のシステムライブラリに関するセクションを参照してください。

「メインモジュール」は、main() 関数を含める必要がないことに注意してください。サイドモジュールにある可能性もあります。「メイン」モジュールをメインモジュールにするのは、メインモジュールが 1 つしかなく、システムライブラリがリンクされているのはメインモジュールだけであるということです。

(システムライブラリはメインモジュールに静的にリンクされることに注意してください。そのようにすることで、デッドコードを削除したいほどではない場合でも、いくつかの最適化を行います。)

実用的な詳細

実行中のコードを確認するためにジャンプしたい場合は、テストスイートを参照できます。一般的に動的リンクをテストする test_dylink_* テストと、特に dlopen() をテストする test_dlfcn_* テストがあります。それ以外の場合は、今から手順を説明します。

ロード時動的リンク

ロード時動的リンクとは、サイドモジュールがメインモジュールと一緒に、起動時にロードされ、アプリケーションの実行開始前にリンクされるケースを指します。

  • コードの一部をメインモジュールとしてビルドし、-sMAIN_MODULE を使用してリンクします。

  • コードの他の部分をサイドモジュールとしてビルドし、-sSIDE_MODULE を使用してリンクします。

メインモジュールの出力サフィックスは .js である必要があります(WebAssemblyファイルは通常と同様に生成されます)。サイドモジュールの出力は WebAssembly モジュールのみになります。出力サフィックスは .wasm または .so (UNIXシステムで使用される共有ライブラリのサフィックス)をお勧めします。

サイドモジュールを起動時にロードするには、メインモジュールにその存在を知らせる必要があります。これは、メインモジュールをリンクする際にコマンドラインで指定することで実行できます。例:

emcc -sMAIN_MODULE main.c libsomething.wasm

実行時、JavaScriptのロードコードは、アプリケーションが実行を開始する前に、メインモジュールとともに libsomthing.wasm (およびその他のサイドモジュール) をロードします。実行中のアプリケーションは、リンクされたモジュールからコードにアクセスできるようになります。

dlopen() を使用した実行時動的リンク

実行時動的リンクは、プログラムがすでに実行されているときに dlopen() 関数を呼び出してサイドモジュールをロードすることで実行できます。手順は、メインモジュールとサイドモジュールをビルドするために使用する同じフラグを使用して、同じように開始します。違いは、メインモジュールをリンクするときにコマンドラインでサイドモジュールを指定しないことです。代わりに、dlopen (または fopen など) がアクセスできるように、サイドモジュールをファイルシステムにロードする必要があります (ファイルシステム統合なしで動作する現在の実行可能ファイルを開くことを意味する dlopen(NULL) を除く)。基本的にはそれだけです。その後、dlopen(), dlsym() などを通常どおり使用できます。

コードサイズ

デフォルトでは、メインモジュールはデッドコード削除を無効にします。つまり、コンパイルされたすべてのコードは、リンクされたすべてのシステムライブラリ、およびすべてのJSライブラリコードを含め、出力に残ります。

これは最も驚きが少ないためデフォルトの動作です。ただし、-sMAIN_MODULE=2 (1の代わりに) でビルドすることにより、通常のデッドコード削除を使用することもできます。このモードでは、メインモジュールは、コードを存続させるための特別な動作なしに、通常どおりにビルドされます。したがって、サイドモジュールが必要とするコードを存続させる責任はあなたにあります。これは、EXPORTED_FUNCTIONS に追加するか、ソースコードでシンボル EMSCRIPTEN_KEEPALIVE をタグ付けすることで実行できます。この動作の例については、other.test_minimal_dynamic を参照してください。

ロード時動的リンクを実行している場合、コマンドラインで指定されたサイドモジュールに必要なすべてのシンボルは自動的に存続されます。このため、ロード時動的リンクを行う場合は、MAIN_MODULE=2 を使用することを強くお勧めします。

サイドモジュールには、対応する -sSIDE_MODULE=2 もあります。

システムライブラリ

前述のように、システムライブラリはEmscriptenリンカーによって特別な方法で処理され、動的リンクでは、メインモジュールのみがシステムライブラリに対してリンクされます。メインモジュールをリンクする場合、コマンドラインでサイドモジュールを渡すことができ、その場合、システムライブラリの依存関係は自動的に処理されます。

ただし、サイドモジュールなしでメインモジュールをリンクする場合(通常は -sMAIN_MODULE=1 を使用)、必要なシステムライブラリが含まれていない可能性があります。このセクションでは、特定のライブラリに対してメインモジュールを強制的にリンクすることにより、それを修正する方法について説明します。

すべての標準ライブラリを強制的に含めるには、環境内で EMCC_FORCE_STDLIBS=1 を使用してメインモジュールをビルドできます。より洗練されたアプローチは、明示的に含めるシステムライブラリの名前を指定することです。たとえば、EMCC_FORCE_STDLIBS=libcxx,libcxxabi のようにします(これらの2つのライブラリが必要な場合)。

その他の注意事項

動的チェック

ネイティブリンカーは通常、すべてのシンボルが解決されたときにのみコードを実行します。Emscriptenの動的リンカーは、シンボルをそれらのシンボルへの未解決の参照に**動的に**接続します。その結果、未解決のシンボルが残っているかどうかを確認せずに、それらがある場合でもコードの実行を開始できます。実際にはそれらが呼び出されない場合、正常に実行されます。呼び出される場合は、実行時エラーが発生します。何が問題だったかは、スタックトレースから明らかになるはずです(圧縮されていないビルドの場合)。-sASSERTIONS を使用してビルドすると、さらに役立つ可能性があります。

制限事項

  • Chromiumはメインスレッドでの 4kB を超える WASM のコンパイルをサポートしておらず、サイドモジュールも同様です。--use-preload-plugins (emcc または file_packager.py) を使用して、Emscriptenで起動時にコンパイルさせることができます [doc] [discuss]

  • サイドモジュール内で定義された EM_ASM および EM_JS コードは、eval のサポートに依存しており、そのため -sDYNAMIC_EXECUTION=0 と互換性がありません。

Pthreads のサポート

動的リンク + pthreads はまだ実験段階です。そのため、MAIN_MODULE-pthread の両方でリンクすると、警告が生成されます。

ロード時の動的リンクは特に問題なく機能しますが、dlopen/dlsym を介した実行時の動的リンクには、いくつかの追加の考慮事項が必要になる場合があります。この理由としては、Emscriptenライブラリコードによって、スレッド間で間接関数ポインターテーブルの同期を維持する必要があるためです。dlopen を介して新しいライブラリがロードされたり、dlsym を介して新しいシンボルが要求されたりするたびに、テーブルスロットを追加でき、これらの変更はプロセスのすべてのスレッドに反映する必要があります。

テーブルへの変更は mutex によって保護されており、dlopen または dlsym から戻る前に、他のすべてのスレッドが同期するまで待機します。この同期を可能な限りシームレスにするために、emscripten_futex_wait および emscirpten_yield の低レベルプリミティブにフックします。

ほとんどのユースケースでは、これらすべてが舞台裏で行われ、特別なアクションは必要ありません。ただし、現在変更が必要になる可能性のあるアプリケーションのクラスが1つあります。アプリケーションがビジーウェイトする場合、または atomic.waitXX 命令(または clang __builtin_wasm_memory_atomic_waitXX 組み込み)を直接使用する場合は、デッドロックを回避するために、emscripten_futex_wait を使用するように切り替える必要がある場合があります。ブロック中に emscripten_futex_wait を使用しないと、dlopen および/または dlsym を呼び出している他のスレッドをブロックする可能性があります。