次の方法で共有


TN058: MFC モジュールの状態の実装

次のテクニカル ノートは、最初にオンライン ドキュメントに含まれてから更新されていません。 その結果、一部の手順やトピックが古くなっているか、正しくない可能性があります。 最新情報については、オンライン ドキュメント インデックスで関心のあるトピックを検索することをお勧めします。

このテクニカル ノートでは、MFC の "モジュール状態" コンストラクトの実装について説明します。 DLL (または OLE インプロセス サーバー) の MFC 共有 DLL を使用するには、モジュール状態の実装を理解することが重要です。

このメモを読む前に、「 新しいドキュメント、Windows、ビューの作成」の「MFC モジュールの状態データの管理」を参照してください。 この記事には、このテーマに関する重要な使用状況情報と概要情報が含まれています。

概要

MFC 状態情報には、モジュール状態、プロセス状態、スレッド状態の 3 種類があります。 これらの状態の種類を組み合わせることができる場合があります。 たとえば、MFC のハンドル マップは、モジュールローカルとスレッドローカルの両方です。 これにより、2 つの異なるモジュールが各スレッドで異なるマップを持つことができます。

プロセスの状態とスレッドの状態は似ています。 これらのデータ項目は、従来はグローバル変数でしたが、適切な Win32s のサポートまたは適切なマルチスレッド サポートのために、特定のプロセスまたはスレッドに固有である必要があります。 特定のデータ項目がどのカテゴリに適合するかは、その項目と、プロセスとスレッドの境界に関する目的のセマンティクスによって異なります。

モジュール状態は、真のグローバル状態またはプロセス ローカルまたはスレッド ローカルの状態を含むことができるという点で一意です。 さらに、それはすぐに切り替えることができる。

モジュール状態の切り替え

各スレッドには、"現在" または "アクティブ" のモジュール状態へのポインターが含まれています (当然のことながら、ポインターは MFC のスレッド のローカル状態の一部です)。 このポインターは、OLE コントロールまたは DLL を呼び出すアプリケーションや、アプリケーションを呼び出す OLE コントロールなど、実行スレッドがモジュール境界を通過すると変更されます。

現在のモジュールの状態は、 AfxSetModuleStateを呼び出すことによって切り替えられます。 ほとんどの場合、API を直接処理することはありません。 MFC では、多くの場合、それを呼び出します (WinMain、OLE エントリ ポイント、 AfxWndProcなど)。 これは、記述するすべてのコンポーネントで、特別な WndProcで静的にリンクし、現在のモジュール状態を認識する特別な WinMain (または DllMain) によって行われます。 このコードは、DLLMODUL を参照して確認できます。CPP または APPMODUL。MFC\SRC ディレクトリ内の CPP。

モジュールの状態を設定してから戻さないことはまれです。 ほとんどの場合、独自のモジュール状態を現在の状態として "プッシュ" し、完了後に元のコンテキストを "ポップ" します。 これは、マクロ AFX_MANAGE_STATE と特殊なクラス AFX_MAINTAIN_STATEによって行われます。

CCmdTarget には、モジュール状態の切り替えをサポートするための特別な機能があります。 特に、 CCmdTarget は、OLE オートメーションおよび OLE COM エントリ ポイントに使用されるルート クラスです。 システムに公開されている他のエントリ ポイントと同様に、これらのエントリ ポイントは正しいモジュールの状態を設定する必要があります。 特定の CCmdTarget が "正しい" モジュールの状態を認識する方法。答えは、モジュールの構築時に "現在の" モジュールの状態が何であるかを "記憶" することです。これにより、現在のモジュールの状態を後で呼び出されたときにその "記憶された" 値に設定できます。 その結果、特定の CCmdTarget オブジェクトが関連付けられているモジュールの状態は、オブジェクトの作成時に現在のモジュール状態になります。 INPROC サーバーの読み込み、オブジェクトの作成、メソッドの呼び出しの簡単な例を見てみましょう。

  1. DLL は、 LoadLibraryを使用して OLE によって読み込まれます。

  2. RawDllMain が最初に呼び出されます。 モジュールの状態を DLL の既知の静的モジュールの状態に設定します。 このため、 RawDllMain は DLL に静的にリンクされます。

  3. オブジェクトに関連付けられているクラス ファクトリのコンストラクターが呼び出されます。 COleObjectFactoryCCmdTarget から派生し、その結果、インスタンス化されたモジュールの状態が記憶されます。 これは重要です。クラス ファクトリがオブジェクトの作成を求められたら、現在のモジュール状態を認識するようになりました。

  4. DllGetClassObject クラス ファクトリを取得するために呼び出されます。 MFC は、このモジュールに関連付けられているクラス ファクトリ リストを検索して返します。

  5. COleObjectFactory::XClassFactory2::CreateInstance が呼び出されます オブジェクトを作成して返す前に、この関数はモジュールの状態を、手順 3 で現在のモジュール状態 ( COleObjectFactory がインスタンス化されたときに現在の状態) に設定します。 これは、 METHOD_PROLOGUE内で行われます。

  6. オブジェクトが作成されるときも CCmdTarget 派生であり、同じ方法で、どのモジュール状態がアクティブであったかを記憶 COleObjectFactory 、この新しいオブジェクトも同様です。 これで、オブジェクトは、呼び出されるたびに切り替えるモジュールの状態を認識します。

  7. クライアントは、 CoCreateInstance 呼び出しから受信した OLE COM オブジェクトに対して関数を呼び出します。 オブジェクトが呼び出されると、 METHOD_PROLOGUE を使用して、 COleObjectFactory と同様にモジュールの状態を切り替えます。

ご覧のように、モジュールの状態は、作成されるとオブジェクトからオブジェクトに伝達されます。 モジュールの状態を適切に設定することが重要です。 設定されていない場合、DLL または COM オブジェクトは、呼び出している MFC アプリケーションと十分に対話できないか、独自のリソースを見つけることができないか、他の悲惨な方法で失敗する可能性があります。

特定の種類の DLL、特に "MFC 拡張機能" DLL では、モジュールの状態が RawDllMain に切り替えられません (実際には、通常は RawDllMainもありません)。 これは、それらを使用するアプリケーションに実際に存在していた "かのように" 動作することを目的としているためです。 これらは、実行されているアプリケーションの非常に一部であり、そのアプリケーションのグローバル状態を変更する意図です。

OLE コントロールとその他の DLL は大きく異なります。 呼び出し元アプリケーションの状態を変更する必要はありません。それらを呼び出しているアプリケーションは MFC アプリケーションでもない可能性があるため、変更する状態がない可能性があります。 これがモジュールの状態切り替えが発明された理由です。

DLL でダイアログ ボックスを起動する関数など、DLL からエクスポートされた関数の場合は、関数の先頭に次のコードを追加する必要があります。

AFX_MANAGE_STATE(AfxGetStaticModuleState())

これにより、現在のモジュールの状態が、現在のスコープの最後まで AfxGetStaticModuleState から返された状態にスワップされます。

dll 内のリソースに関する問題は、AFX_MODULE_STATE マクロが使用されていない場合に発生します。 既定では、MFC はメイン アプリケーションのリソース ハンドルを使用してリソース テンプレートを読み込みます。 このテンプレートは、実際には DLL に格納されます。 根本原因は、MFC のモジュール状態情報が AFX_MODULE_STATE マクロによって切り替えされていないことです。 リソース ハンドルは MFC のモジュール状態から復旧されます。 モジュールの状態を切り替えないと、間違ったリソース ハンドルが使用されます。

AFX_MODULE_STATE DLL 内のすべての関数に配置する必要はありません。 たとえば、 InitInstance は、AFX_MODULE_STATEせずにアプリケーションの MFC コードによって呼び出すことができます。MFC は、 InitInstance する前にモジュールの状態を自動的にシフトし、 InitInstance 戻った後に戻すからです。 すべてのメッセージ マップ ハンドラーについても同じことが当てはまります。 通常の MFC DLL には、実際には、メッセージをルーティングする前にモジュールの状態を自動的に切り替える特別なマスター ウィンドウ プロシージャがあります。

ローカル データの処理

Win32s DLL モデルの難しさがない場合、ローカル データの処理には大きな懸念はありません。 Win32s では、複数のアプリケーションによって読み込まれた場合でも、すべての DLL がグローバル データを共有します。 これは、"実際の" Win32 DLL データ モデルとは大きく異なります。各 DLL は、DLL にアタッチする各プロセス内のデータ領域の個別のコピーを取得します。 複雑さを増すために、Win32s DLL のヒープに割り当てられたデータは、実際にはプロセス固有です (少なくとも所有権に関する限り)。 次のデータとコードについて考えてみましょう。

static CString strGlobal; // at file scope

__declspec(dllexport)
void SetGlobalString(LPCTSTR lpsz)
{
    strGlobal = lpsz;
}

__declspec(dllexport)
void GetGlobalString(LPCTSTR lpsz, size_t cb)
{
    StringCbCopy(lpsz, cb, strGlobal);
}

上記のコードが DLL 内にあり、その DLL が 2 つのプロセス A と B によって読み込まれる場合 (実際には、同じアプリケーションの 2 つのインスタンスである可能性があります) はどうなるかを検討してください。 呼び出し SetGlobalString("Hello from A")。 その結果、プロセス A のコンテキストで CString データにメモリが割り当てられます。 CString 自体はグローバルであり、A と B の両方から参照可能であることに注意してください。B は GetGlobalString(sz, sizeof(sz))を呼び出します。 B は、A セットのデータを確認できます。 これは、Win32s が Win32 のようなプロセス間の保護を提供しないためです。 これが最初の問題です。多くの場合、1 つのアプリケーションが別のアプリケーションによって所有されていると見なされるグローバル データに影響を与えるのは望ましくありません。

他にも問題があります。 A が終了したとします。 A が終了すると、"strGlobal" 文字列によって使用されるメモリがシステムで使用できるようになります。つまり、プロセス A によって割り当てられたすべてのメモリは、オペレーティング システムによって自動的に解放されます。 CStringデストラクターが呼び出されているため、解放されません。まだ呼び出されていません。 割り当てたアプリケーションがシーンを離れたからといって解放されます。 B が GetGlobalString(sz, sizeof(sz))を呼び出した場合、有効なデータが取得されない可能性があります。 他の一部のアプリケーションでは、そのメモリを別のメモリに使用している可能性があります。

明らかに問題が存在します。 MFC 3.x では、スレッド ローカル ストレージ (TLS) と呼ばれる手法を使用しました。 MFC 3.x は、Win32s の下で実際にプロセス ローカル ストレージ インデックスとして機能する TLS インデックスを割り当てます。ただし、このインデックスは呼び出されず、その TLS インデックスに基づいてすべてのデータを参照します。 これは、Win32 にスレッド ローカル データを格納するために使用された TLS インデックスに似ています (その主題の詳細については、以下を参照してください)。 これにより、すべての MFC DLL でプロセスごとに少なくとも 2 つの TLS インデックスが使用されました。 多くの OLE コントロール DLL (OCX) の読み込みを考慮すると、TLS インデックスがすぐに使い果たされます (使用可能なのは 64 個のみです)。 さらに、MFC では、このデータをすべて 1 つの場所に 1 つの構造に配置する必要がありました。 それは非常に拡張可能ではなく、TLSインデックスの使用に関しては理想的ではなかった。

MFC 4.x は、ローカルで処理する必要があるデータを "ラップ" できる一連のクラス テンプレートでこれに対処します。 たとえば、上記の問題は、次のように記述することで修正できます。

struct CMyGlobalData : public CNoTrackObject
{
    CString strGlobal;
};
CProcessLocal<CMyGlobalData> globalData;

__declspec(dllexport)
void SetGlobalString(LPCTSTR lpsz)
{
    globalData->strGlobal = lpsz;
}

__declspec(dllexport)
void GetGlobalString(LPCTSTR lpsz, size_t cb)
{
    StringCbCopy(lpsz, cb, globalData->strGlobal);
}

MFC は、2 つの手順でこれを実装します。 最初に、Win32 Tls* API (TlsAllocTlsSetValueTlsGetValue など) の上にレイヤーがあり、DLL の数に関係なく、プロセスごとに 2 つの TLS インデックスのみを使用します。 次に、このデータにアクセスするための CProcessLocal テンプレートが提供されます。 これは、演算子> をオーバーライドします。これは、上記の直感的な構文を可能にします。 CProcessLocalによってラップされるすべてのオブジェクトは、CNoTrackObjectから派生する必要があります。 CNoTrackObject では、下位レベルのアロケーター (LocalAlloc/LocalFree) と仮想デストラクターが提供されるため、MFC はプロセスの終了時にプロセス ローカル オブジェクトを自動的に破棄できます。 このようなオブジェクトは、追加のクリーンアップが必要な場合にカスタム デストラクターを持つことができます。 上記の例では、埋め込まれた CString オブジェクトを破棄する既定のデストラクターがコンパイラによって生成されるため、必要ありません。

このアプローチには他にも興味深い利点があります。 すべての CProcessLocal オブジェクトは自動的に破棄されるだけでなく、必要になるまで構築されません。 CProcessLocal::operator-> では、関連付けられたオブジェクトが初めて呼び出されるときにインスタンス化されます。それ以下のタイミングでインスタンス化されます。 上の例では、最初にSetGlobalStringまたはGetGlobalStringが呼び出されるまで、'strGlobal' 文字列が構築されないことを意味します。 場合によっては、DLL の起動時間を短縮するのに役立ちます。

スレッド ローカル データ

ローカル データの処理と同様に、スレッド ローカル データは、データが特定のスレッドに対してローカルである必要がある場合に使用されます。 つまり、そのデータにアクセスするスレッドごとに、データの個別のインスタンスが必要です。 これは、広範な同期メカニズムの代わりに何度も使用できます。 データを複数のスレッドで共有する必要がない場合、このようなメカニズムはコストがかかり、不要になる可能性があります。 CString オブジェクトがあるとします (上記のサンプルとよく似ています)。 CThreadLocal テンプレートでラップすることで、スレッドをローカルにすることができます。

struct CMyThreadData : public CNoTrackObject
{
    CString strThread;
};
CThreadLocal<CMyThreadData> threadData;

void MakeRandomString()
{
    // a kind of card shuffle (not a great one)
    CString& str = threadData->strThread;
    str.Empty();
    while (str.GetLength() != 52)
    {
        unsigned int randomNumber;
        errno_t randErr;
        randErr = rand_s(&randomNumber);

        if (randErr == 0)
        {
            TCHAR ch = randomNumber % 52 + 1;
            if (str.Find(ch) <0)
            str += ch; // not found, add it
        }
    }
}

MakeRandomStringが 2 つの異なるスレッドから呼び出された場合、それぞれが別の方法で文字列を "シャッフル" し、他のスレッドに干渉しません。 これは、実際には 1 つのグローバル インスタンスではなく、スレッドごとに strThread インスタンスがあるためです。

参照を使用して、ループの反復ごとに 1 回ではなく、 CString アドレスを 1 回キャプチャする方法に注意してください。 ループ コードは、'str' が使用されているすべての場所でthreadData->strThreadで記述できましたが、コードの実行がはるかに遅くなります。 このような参照がループで発生した場合は、データへの参照をキャッシュすることをお勧めします。

CThreadLocal クラス テンプレートでは、CProcessLocalと同じメカニズムと同じ実装手法が使用されます。

こちらも参照ください

番号別テクニカル ノート
カテゴリ別テクニカル ノート