Dynamic-Link ライブラリのベスト プラクティス
**更新: **
- 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 を呼び出します。 スレッドの作成は、他のスレッドと同期しない場合は機能しますが、危険です。
- 名前付きパイプまたはその他の名前付きオブジェクトを作成します (Windows 2000 のみ)。 Windows 2000 では、ターミナル サービス DLL によって名前付きオブジェクトが提供されます。 この DLL が初期化されていない場合、DLL を呼び出すと、プロセスがクラッシュする可能性があります。
- 動的 C Run-Time (CRT) のメモリ管理機能を使用します。 CRT DLL が初期化されていない場合、これらの関数を呼び出すと、プロセスがクラッシュする可能性があります。
- User32.dllまたはGdi32.dllの関数を呼び出します。 一部の関数は別の DLL を読み込みますが、初期化できない可能性があります。
- マネージド コードを使用します。
次のタスクは、 DllMain 内で安全に実行できます。
- コンパイル時に静的データ構造とメンバーを初期化します。
- 同期オブジェクトを作成して初期化します。
- メモリを割り当て、動的データ構造を初期化します (上記の関数は避けてください)。
- スレッド ローカル ストレージ (TLS) を設定します。
- ファイルを開き、読み取り、書き込みを行います。
- Kernel32.dll内の関数を呼び出します (上記の関数を除きます)。
- グローバル ポインターを NULL に設定し、動的メンバーの初期化を中止します。 Microsoft Windows Vista™ では、1 回限りの初期化関数を使用して、コード ブロックがマルチスレッド環境で 1 回だけ実行されるようにすることができます。
ロック順序の反転によって発生するデッドロック
ロックなどの複数の同期オブジェクトを使用するコードを実装する場合は、ロックの順序を考慮することが重要です。 一度に複数のロックを取得する必要がある場合は、ロック階層またはロック順序と呼ばれる明示的な優先順位を定義する必要があります。 たとえば、ロック A がコード内のどこかでロック B の前に取得され、ロック B がコード内の別の場所でロック C の前に取得された場合、ロック順序は 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 内のすべての呼び出しを延期します。
- 後で待機できる初期化タスクを延期します。 アプリケーションがエラーを適切に処理できるように、特定のエラー状態を早期に検出する必要があります。 ただし、この早期検出と、その結果として生じる可能性のある堅牢性の喪失との間にはトレードオフがあります。 多くの場合、初期化の遅延が最適です。