ダイナミック リンク ライブラリのベスト プラクティス
**更新日時:**
- 2006 年 5 月 17 日
重要な API
開発者にとって、DLL の作成には多くの課題が存在します。 DLL には、システムによって適用されるバージョン管理がありません。 1 つのシステムに複数のバージョンの DLL が存在する場合、上書きが簡単にできてしまうことに加え、バージョン管理スキーマが欠如しているために、依存関係と API の競合が発生します。 開発環境、ローダーの実装、DLL の依存関係が複雑であるため、読み込み順序とアプリケーションの動作が不安定になります。 最後に、多くのアプリケーションは DLL に依存しています。また、複雑ないくつもの依存関係を含んでいるため、アプリケーションが正常に機能するには、それに対応することが求められます。 このドキュメントでは、DLL 開発者がより堅牢で移植可能、かつ拡張可能な DLL を構築できるよう支援するガイドラインを提供します。
DllMain 内で不適切な同期が行われると、アプリケーションでデッドロックが起こったり、初期化されていない DLL 内のデータやコードへのアクセスが行われたりするおそれがあります。 DllMain 内から特定の関数を呼び出すと、このような問題が発生します。
一般的なベスト プラクティス
DllMain は、ローダーロックが保持されている間に呼び出されます。 したがって、DllMain 内で呼び出すことができる関数には大きな制限が課されます。 そのため、DllMain は、Microsoft® Windows® API の小さなサブセットを使用して、最小限の初期化タスクを実行するように設計されています。 直接的または間接的にローダー ロックを取得しようとする、DllMain 内の関数を呼び出すことはできません。 そうでないと、アプリケーションがデッドロック状態になったりクラッシュしたりする可能性があります。 DllMain の実装でエラーが発生すると、プロセス全体とそのすべてのスレッドが損なわれるおそれがあります。
理想的な DllMain は、単なる空のスタブであることです。 ただし、多くのアプリケーションは複雑であるため、通常は、これでは限定的過ぎます。 DllMain に関する適切な経験則は、できるだけ多くの初期化を延期することです。 遅延初期化を行うと、ローダー ロックが保持されている間はこの初期化が実行されないため、アプリケーションの堅牢性が向上します。 また、遅延初期化を使用すると、より多くの Windows API を安全に使用できます。
一部の初期化タスクは延期できません。 たとえば、構成ファイルに依存する DLL は、ファイルの形式が正しくない場合やガベージを含む場合は、読み込みに失敗します。 この種の初期化の場合、DLL では、他の処理を完了してリソースを無駄にするのではなく、アクションを試みて、すぐに失敗します。
DllMain 内からは、次のタスクを実行しないでください。
- LoadLibrary または LoadLibraryEx を (直接的または間接的に) 呼び出す。 これを行うと、デッドロックやクラッシュが発生するおそれがあります。
- GetStringTypeA、GetStringTypeEx、または GetStringTypeW を (直接的または間接的に) 呼び出す。 これを行うと、デッドロックやクラッシュが発生するおそれがあります。
- 他のスレッドと同期する。 これを行うと、デッドロックが発生するおそれがあります。
- ローダー ロックの取得を待機しているコードによって所有されている、同期オブジェクトを取得する。 これを行うと、デッドロックが発生するおそれがあります。
- CoInitializeEx を使用して COM スレッドを初期化する。 特定の条件下では、この関数で LoadLibraryEx を呼び出すことができます。
- レジストリ関数を呼び出す。
- CreateProcess を呼び出す。 プロセスを作成すると、別の DLL を読み込むことができます。
- ExitThread を呼び出す。 DLL のデタッチ中にスレッドを終了すると、ローダー ロックがもう一度取得され、デッドロックまたはクラッシュが発生するおそれがあります。
- CreateThread を呼び出す。 他のスレッドと同期しないのであればスレッドを作成できますが、リスクがあります。
- ShGetFolderPathW を呼び出す。 シェル/既知のフォルダーの API を呼び出すとスレッドが同期されるため、デッドロックが発生するおそれがあります。
- 名前付きパイプまたはその他の名前付きオブジェクトを作成する (Windows 2000 のみ)。 Windows 2000 では、名前付きオブジェクトはターミナル サービス DLL によって提供されます。 この DLL が初期化されていない場合、その DLL を呼び出すと、プロセスがクラッシュするおそれがあります。
- 動的 C ランタイム (CRT) のメモリ管理関数を使用する。 CRT DLL が初期化されていない場合、これらの関数を呼び出すと、プロセスがクラッシュするおそれがあります。
- User32.dll または Gdi32.dll で関数を呼び出す。 一部の関数では、初期化されていない可能性がある別の DLL を読み込みます。
- マネージド コードを使用する。
次のタスクは、DllMain 内で安全に実行できます。
- コンパイル時に静的データ構造とメンバーを初期化する。
- 同期オブジェクトを作成して初期化する。
- メモリを割り当て、動的データ構造を初期化する (上記に示した関数を回避します)。
- スレッド ローカル ストレージ (TLS) を設定する。
- ファイルを開き、そこから読み取り、そこに書き込む。
- Kernel32.dll で関数を呼び出す (上記に示した関数を除きます)。
- グローバル ポインターを NULL に設定して、動的メンバーの初期化を延期する。 Microsoft Windows Vista™ では、1 回限りの初期化関数を使用して、マルチスレッド環境でコード ブロックが 1 回だけ実行されるようにすることができます。
ロック順序の反転によって発生するデッドロック
ロックなどの同期オブジェクトを複数使用するコードを実装する場合は、ロック順序を守ることが重要です。 一度に複数のロックを取得する必要がある場合は、ロック階層またはロック順序と呼ばれる明示的な優先順位を定義する必要があります。 たとえば、コード内のどこかでロック B の前にロック A が取得され、コード内の別の場所でロック C の前にロック B が取得された場合、ロック順序は A、B、C となり、コード全体でこの順序に従う必要があります。 ロック順序の反転が起こるのは、このロック順序に従わない (たとえば、ロック A の前にロック B が取得された) 場合です。ロック順序の反転が起こると、デバッグするのが難しいデッドロックが発生するおそれがあります。 このような問題を回避するには、すべてのスレッドでロックを同じ順序で取得する必要があります。
ローダーでは、既にローダー ロックが取得された状態で DllMain を呼び出すため、ローダー ロックの優先順位がロック階層内で最も高くなる必要があるので注意してください。 また、コードでは、適切な同期に必要なロックのみを取得する必要がある点にも注意してください。階層内で定義されているすべてのロックを取得する必要はありません。 たとえば、あるコード セクションで適切な同期のためにロック A と C のみが必要な場合、コードではロック C を取得する前にロック A を取得する必要があります。コードでロック B も取得する必要はありません。さらに、DLL コードではローダー ロックを明示的に取得できません。 コードでローダー ロックを間接的に取得できる GetModuleFileName などの API を呼び出す必要があり、コードでプライベート ロックも取得する必要がある場合、コードではロック P を取得する前に GetModuleFileName を呼び出す必要があります。こうして読み込み順序が守られます。
図 2 は、ロック順序の反転を示す例です。 メイン スレッドに DllMain が含まれている DLL について考えます。 ライブラリ ローダーでは、ローダー ロック L を取得してから、DllMain を呼び出します。 メイン スレッドでは、同期オブジェクト A、B、G を作成して、そのデータ構造へのアクセスをシリアル化した後、ロック G の取得を試みます。既に ロック G を正常に取得したワーカー スレッドでは、ローダー ロック L の取得を試みる GetModuleHandle などの関数を呼び出します。こうして、ワーカー スレッドは L でブロックされ、メイン スレッドは G でブロックされ、デッドロックが発生します。
ロック順序の反転によって発生するデッドロックを防ぐには、すべてのスレッドで常に、定義された読み込み順序で同期オブジェクトを取得する必要があります。
同期に関するベスト プラクティス
初期化の一環としてワーカー スレッドを作成する DLL について考えます。 DLL のクリーンアップ時には、すべてのワーカー スレッドと同期して、データ構造が一貫性のある状態であることを確認してから、ワーカー スレッドを終了する必要があります。 現在、マルチスレッド環境での DLL のクリーンな同期とシャットダウンの問題を完全に解決する簡単な方法はありません。 このセクションでは、DLL のシャットダウン中におけるスレッドの同期に関する現在のベスト プラクティスについて説明します。
プロセス終了時の DllMain でのスレッド同期
- プロセス終了時に DllMain が呼び出されるまでにすべてのプロセスのスレッドが強制的にクリーンアップされていて、アドレス空間に不整合が生じる可能性があります。 この場合、同期は必要ありません。 つまり、理想的な DLL_PROCESS_DETACH ハンドラーは空であるということです。
- Windows Vista では、コア データ構造 (環境変数、現在のディレクトリ、プロセス ヒープなど) が一貫性のある状態であることを確認します。 ただし、他のデータ構造が破損している可能性があるため、メモリをクリーンすることは安全ではありません。
- 保存する必要がある永続的な状態は、永続記憶域にフラッシュする必要があります。
DLL アンロード時における DLL_THREAD_DETACH の DllMain でのスレッド同期
- DLL がアンロードされるときに、アドレス空間は破棄されません。 したがって、DLL ではクリーン シャットダウンを実行する必要があります。 これには、スレッドの同期、開いているハンドル、永続的な状態、割り当て済みのリソースが含まれます。
- スレッドの同期には注意が必要です。DllMain でスレッドが終了するのを待機すると、デッドロックが発生するおそれがあるためです。 たとえば、DLL A はローダー ロックを保持しています。 これは、スレッド T に終了するように通知し、スレッドが終了するのを待機します。 スレッド T が終了し、ローダーは、DLL_THREAD_DETACH を使用して DLL A の DllMain を呼び出すためのローダー ロックを取得しようとします。 これにより、"デッドロックが発生します"。 デッドロックのリスクを最小限に抑えるには:
すべてのスレッドが作成された後、それらの実行が開始される前に DLL がアンロードされると、スレッドがクラッシュするおそれがあります。 DLL で初期化の一環として DllMain 内にスレッドが作成された場合、一部のスレッドが初期化を完了していない可能性があり、DLL_THREAD_ATTACH メッセージは DLL に配信されるのを待機したままになります。 この状況で DLL がアンロードされると、スレッドの終了が開始されます。 ただし、一部のスレッドはローダー ロックの背後でブロックされる可能性があります。 それらのDLL_THREAD_ATTACH メッセージは、DLL がマップ解除された後に処理され、プロセスがクラッシュする原因となります。
推奨事項
推奨されているガイドラインを次に示します。
- アプリケーション検証ツールを使用して、DllMain の最も一般的なエラーをキャッチします。
- DllMain 内でプライベート ロックを使用する場合は、ロック階層を定義し、それを常に使用します。 ローダー ロックは、この階層の一番下になる必要があります。
- まだ完全に読み込まれていない可能性がある別の DLL に依存する呼び出しがないことを確認します。
- DllMain 内ではなく、コンパイル時に単純な初期化を静的に実行します。
- 遅らせることができる、DllMain 内のすべての呼び出しは延期します。
- 遅らせることができる初期化タスクは延期します。 アプリケーションでエラーを適切に処理できるように、特定のエラー状態を早期に検出する必要があります。 ただし、この早期検出と、それによって起こる可能性がある堅牢性の損失との間には、トレードオフが生じます。 多くの場合は、初期化を遅延するのが最適です。