テクニカル ノート 58: MFC のモジュール状態の実装

Note

次のテクニカル ノートは、最初にオンライン ドキュメントの一部とされてから更新されていません。 結果として、一部のプロシージャおよびトピックが最新でないか、不正になります。 最新の情報について、オンライン ドキュメントのキーワードで関係のあるトピックを検索することをお勧めします。

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

このメモを読む前に、「新しいドキュメント、ウィンドウ、ビューの作成」の「MFC モジュールの状態データの管理」をご覧ください。 この記事には、この論題の重要な使用情報と概要情報が含まれています。

概要

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

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

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

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

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

現在のモジュールの状態は、AfxSetModuleState を呼び出すことによって切り替えられます。 ほとんどの場合、API を直接処理することはできません。 MFC では、多くの場合、(WinMain、OLE エントリ ポイント、AfxWndProc などで) これを呼び出すことになります。 これは、特殊な WndProc に静的にリンクすることによって作成される任意のコンポーネント、およびどのモジュールの状態が最新であるべきかを把握する特別な WinMain (または DllMain) で実行されます。 このコードについては、MFC\ SRC ディレクトリ内の DLLMODUL.CPP または APPMODUL.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 から返された状態と交換されます。

AFX_MODULE_STATE マクロを使用しない場合、DLL 内のリソースに関する問題が発生します。 既定では、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 つのインスタンスである可能性があります)、どのようになるかを考えてみましょう。 A が 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 で 1 プロセスあたり少なくとも 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 (TlsAlloc、TlsSetValue、TlsGetValue など) の上にレイヤーがあり、DLL の数に関係なく、プロセスごとに 2 つの TLS インデックスのみを使用します。 次に、この CProcessLocal データにアクセスするためにテンプレートが提供されます。 これは上記の直感的な構文を使用できるようにする演算子 -> をオーバーライドします。 CProcessLocal でラップされるオブジェクトはすべて、CNoTrackObject から派生させる必要があります。 CNoTrackObject によって、下位レベルのアロケーター (LocalAlloc/LocalFree) と、プロセスの終了時に MFC がプロセス ローカル オブジェクトを自動的に破棄できるようにする仮想デストラクターが提供されます。 追加のクリーンアップが必要な場合は、このようなオブジェクトにカスタム デストラクターを設定できます。 上の例でそれを必要としないのは、埋め込み CString オブジェクトを破棄する既定のデストラクターがコンパイラによって生成されるからです。

このアプローチには、他にも興味深い利点があります。 すべての CProcessLocal オブジェクトが自動的に破棄されるだけでなく、必要になるまで構築されません。 CProcessLocal::operator-> は、関連付けられたオブジェクトが初めて呼び出されるとインスタンス化され、それより前にインスタンス化されることはありません。 つまり上の例では、'strGlobal' 文字列は初めて SetGlobalString または GetGlobalString が呼び出されるまで構築されません。 場合によっては、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 と同じメカニズムと実装手法が使用されます。

関連項目

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