テクニカル ノート 58: MFC のモジュール状態の実装
更新 : 2007 年 11 月
メモ : |
---|
次のテクニカル ノートは、最初にオンライン ドキュメントの一部とされてから更新されていません。結果として、一部のプロシージャおよびトピックが最新でないか、不正になります。最新の情報について、オンライン ドキュメントのキーワードで関係のあるトピックを検索することをお勧めします。 |
ここでは、MFC (Microsoft Foundation Class) の "モジュール状態" の実装について説明します。DLL や OLE インプロセス サーバーから MFC の共有 DLL を使用するには、モジュール状態の実装を理解することが重要です。
このテクニカル ノートを読む前に、「MFC の一般的なトピック」の「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 は、生成されたときの現在のモジュール状態を覚えておき、後で呼び出されたときに現在のモジュール状態を覚えておいた値に設定します。つまり、CCmdTarget オブジェクトに関連付けられているモジュール状態は、そのオブジェクトが生成されたときに現在の状態だったモジュール状態になります。INPROC サーバーを読み込んでオブジェクトを作成し、そのメソッドを呼び出す簡単な例を次に示します。
OLE が LoadLibrary を使って DLL を読み込みます。
まず RawDllMain が呼び出されます。この関数は DLL で識別できる静的なモジュール状態を現在のモジュール状態として設定します。これが RawDllMain を DLL と静的にリンクする理由です。
使用するオブジェクトに対応するクラス ファクトリのコンストラクタが呼び出されます。COleObjectFactory は CCmdTarget から派生するため、インスタンス化されたときのモジュール状態を記憶しています。このことは重要です。オブジェクトの作成を依頼されたときに、クラス ファクトリが、現在の状態にすべきモジュール状態を把握できることを意味しているためです。
DllGetClassObject が呼び出されてクラス ファクトリを取得します。MFC ではこのモジュールに関連付けられたクラス ファクトリのリストを検索し、このリストを返します。
COleObjectFactory::XClassFactory2::CreateInstance が呼び出されます。オブジェクトを作成して返す前に、この関数は、モジュール状態を手順 3 で現在の状態になっていたモジュール状態 (COleObjectFactory のインスタンスが生成されたときに現在の状態だったモジュール状態) に設定します。これは、METHOD_PROLOGUE の内部で実行されます。
このオブジェクトも CCmdTarget から派生するため、COleObjectFactory と同じように、アクティブなモジュール状態を記憶します。これで、このオブジェクトは、呼び出されたときにどのモジュール状態に切り替えるべきかを判断できるようになります。
クライアントが CoCreateInstance から受け取った OLE COM オブジェクトに対して関数を呼び出します。呼び出されたオブジェクトでは、METHOD_PROLOGUE を使用して、COleObjectFactory と同様の方法でモジュール状態を切り替えます。
以上のように、モジュール状態は、オブジェクトが作成されるときにオブジェクトからオブジェクトへと伝播します。モジュール状態を適切に設定することは重要です。適切に設定されていないと、DLL や COM オブジェクトと呼び出し側の MFC アプリケーションとの対話が不十分になったり、それらのオブジェクトがリソースを検出できなくなったり、その他の重大なエラーが生じたりする可能性があります。
ある種の DLL、特に "MFC 拡張" DLL は、RawDllMain 内でモジュール状態を切り替えません。実際、これらの DLL には RawDllMain がありません。これは、これらの DLL があたかも実際にアプリケーション内にあるかのように動作することを想定されているためです。これらの DLL は実行中のアプリケーションの一部であり、そのアプリケーションのグローバルな状態を変更するために使用されます。
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 の実行が終わると自動的に前のモジュール状態に戻ります。すべてのメッセージ マップ ハンドラについても同様です。実際には、標準 DLL には特殊なマスター ウィンドウ プロシージャがあり、このプロシージャによって、メッセージのルーティングの前にモジュール状態が自動的に切り替わります。
プロセス ローカルなデータ
Win32s の DLL モデルに関する難点さえなければ、プロセス ローカルなデータが問題になることはありません。Win32s では、すべての DLL は複数のアプリケーションに読み込まれてもグローバルなデータを共有します。これは、DLL にアタッチする各プロセスでデータ空間のコピーを取得する "本当" の Win32 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 がプロセス A と B (実際には、同じアプリケーションの 2 つのインスタンスの可能性もあります) に読み込まれた場合に、何が起こるかを考えてみます。まず、プロセス A が SetGlobalString("Hello from A") を呼び出します。その結果、プロセス A のコンテキスト内に CString データ用のメモリが割り当てられます。CString 自体はグローバルであり、A と B の両方から使用できます。次に、B が GetGlobalString(sz, sizeof(sz)) を呼び出します。これで、A で設定したデータを B から使用できるようになります。これは、Win32 とは異なり、Win32s ではプロセス間の保護が提供されていないためです。これが第 1 の問題です。多くの場合、あるアプリケーションが所有するグローバル データに対して、他のアプリケーションが影響を与えることは望ましくありません。
問題は他にもあります。ここで、A が終了した場合を考えます。A が終了すると、'strGlobal' 文字列で使用されていたメモリをシステムで使用できるようになります。つまり、プロセス A によって割り当てられたメモリは、すべてオペレーティング システムによって自動的に解放されます。この解放は、CString デストラクタの呼び出しによるものではありません。このデストラクタはまだ呼び出されていません。メモリは、単にメモリを割り当てたアプリケーションが終了したために解放されています。ここで B が GetGlobalString(sz, sizeof(sz)) を呼び出しても、有効なデータは得られません。既に他のアプリケーションでそのメモリを他の用途に使用している可能性もあります。
このような事態は明らかに問題です。MFC 3.x では、スレッド ローカル ストレージ (TLS: Thread-Local Storage) と呼ばれる方法を使用していました。MFC 3.x で割り当てる TLS のインデックスは、名前が違っていることと、TLS インデックスに基づいてすべてのデータを参照することを除いて、Win32s のプロセス ローカル ストレージのインデックスと同じように機能します。これは、Win32 のスレッド ローカルなデータの格納に使用されていた TLS のインデックスによく似ています。詳細については後で説明します。この結果、すべての MFC DLL では 1 プロセスあたり少なくとも 2 つの TLS のインデックスを利用します。多数の OLE コントロール DLL (OCX) を読み込むと、すぐに TLS インデックスが不足します (最大で 64 個まで使用可能)。さらに、MFC ではこのデータをすべて 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 がいくつあっても、1 プロセスあたりで使用する TLS インデックスは 2 つだけです。次に、このデータのアクセス用に 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
}
}
}
2 つの異なるスレッドから MakeRandomString が呼び出されると、それぞれのスレッドでは相互に影響を与えることなく異なる方法で文字列をシャッフルします。これは、実際には 1 つのグローバルなインスタンスではなく、スレッドごとに 1 つの strThread のインスタンスがあるためです。
CString のアドレスをループの繰り返しのたびにではなく 1 回だけ取得するために、参照がどのように使用されているかに注意してください。ループ内で 'str' が使用される箇所すべてに threadData->strThread を記述することもできますが、コードの実行速度ははるかに遅くなります。このような参照がループ内で発生する場合は、参照をキャッシュしておくのが得策です。
CThreadLocal クラス テンプレートは CProcessLocal と同じ機構および実装方法を使っています。