注释
自联机文档中首次包含此说明以来,尚未更新以下技术说明。 因此,某些过程和主题可能过期或不正确。 有关最新信息,建议在在线文档索引中搜索您感兴趣的主题。
此技术说明介绍了 MFC“模块状态”构造的实现。 了解模块状态实现对于使用 DLL 中的 MFC 共享 DLL(或 OLE 进程内服务器)至关重要。
在阅读此说明之前,请参阅“ 创建新文档、Windows 和视图”中的“管理 MFC 模块的状态数据”。 本文包含有关此主题的重要使用情况信息和概述信息。
概述
有三种类型的 MFC 状态信息:模块状态、进程状态和线程状态。 有时,可以组合这些状态类型。 例如,MFC 的句柄映射既是模块本地映射,也是线程本地映射。 这允许两个不同的模块在每个线程中具有不同的映射。
进程状态和线程状态类似。 这些数据项通常是全局变量,但必须特定于给定进程或线程,才能获得适当的 Win32 支持或适当的多线程支持。 给定数据项适合的类别取决于该项及其关于进程和线程边界的所需语义。
模块状态是唯一的,因为它可以包含真正的全局状态或正在处理本地或线程本地的状态。 此外,可以快速切换它。
模块状态切换
每个线程都包含指向“current”或“active”模块状态的指针(毫不奇怪,指针是 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 服务器、创建对象和调用其方法的简单示例为例。
DLL 由 OLE 使用
LoadLibrary
.RawDllMain
首先调用 。 它将模块状态设置为 DLL 的已知静态模块状态。 因此RawDllMain
,静态链接到 DLL。调用与对象关联的类工厂的构造函数。
COleObjectFactory
派生自CCmdTarget
它,因此,它会记住实例化的模块状态。 这一点很重要 - 当类工厂被要求创建对象时,它现在知道要使当前模块状态是什么。DllGetClassObject
调用 以获取类工厂。 MFC 搜索与此模块关联的类工厂列表并返回它。调用
COleObjectFactory::XClassFactory2::CreateInstance
。 在创建对象并返回对象之前,此函数会将模块状态设置为步骤 3 中当前状态(实例化时COleObjectFactory
为当前状态)。 这是 在METHOD_PROLOGUE内完成的。创建对象时,它也是一种
CCmdTarget
派生体,并且以相同的方式COleObjectFactory
记住哪个模块状态处于活动状态,因此此新对象也是如此。 现在,对象知道每当调用它时要切换到哪个模块状态。客户端在从其调用收到的 OLE COM 对象上调用函数
CoCreateInstance
。 调用对象时,它使用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
可以在应用程序中由 MFC 代码调用,而无需AFX_MODULE_STATE,因为 MFC 会在返回之前 InitInstance
自动移动模块状态,然后在返回后切换它 InitInstance
。 所有消息映射处理程序也是如此。 常规 MFC DLL 实际上具有特殊的主窗口过程,用于在路由任何消息之前自动切换模块状态。
处理本地数据
如果 Win32s DLL 模型没有遇到困难,则处理本地数据不会引起极大的关注。 在 Win32 中,所有 DLL 共享其全局数据,即使由多个应用程序加载也是如此。 这与“real”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 由两个进程 A 和 B 加载(实际上,它可以是同一应用程序的两个实例),会发生什么情况。 调用 SetGlobalString("Hello from A")
。 因此,为进程 A 上下文中的数据分配 CString
内存。请记住, CString
自身是全局的,对 A 和 B 都是可见的。现在 B 调用 GetGlobalString(sz, sizeof(sz))
。 B 将能够查看 A 集的数据。 这是因为 Win32s 在 Win32 等进程之间不提供保护。 这是第一个问题:在许多情况下,不希望有一个应用程序影响被视为由其他应用程序拥有的全局数据。
还有其他问题。 假设 A 现在退出。 A 退出时,“”strGlobal
字符串使用的内存可供系统使用,即进程 A 分配的所有内存由作系统自动释放。 它没有释放, CString
因为正在调用析构函数;它尚未调用。 它只是因为分配它的应用程序已离开场景而释放。 现在,如果调用 GetGlobalString(sz, sizeof(sz))
B,它可能无法获取有效的数据。 其他一些应用程序可能已将该内存用于其他应用程序。
显然存在问题。 MFC 3.x 使用了一种称为线程本地存储(TLS)的技术。 MFC 3.x 将分配一个 TLS 索引,在 Win32s 下,该索引实际上充当进程本地存储索引,即使它未调用该索引,然后会基于该 TLS 索引引用所有数据。 这类似于用于在 Win32 上存储线程本地数据的 TLS 索引(有关该主题的详细信息,请参阅下文)。 这导致每个 MFC DLL 每个进程至少使用两个 TLS 索引。 当你考虑加载许多 OLE 控制 DLL (OCX)时,会很快耗尽 TLS 索引(只有 64 个可用)。 此外,MFC 必须将所有这些数据放在一个结构中的一个位置。 它不是非常可扩展的,对于它使用 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 在两个步骤中实现此情况。 首先,Win32 Tls* API(TlsAlloc、 TlsSetValue、 TlsGetValue 等)顶部有一个层,每个进程只使用两个 TLS 索引,不管你拥有多少 DLL。 其次, 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
从两个不同的线程调用,则每个线程都会以不同的方式“洗牌”字符串,而不会干扰另一个线程。 这是因为实际上每个线程有一个 strThread
实例,而不是一个全局实例。
请注意引用如何用于捕获 CString
地址一次,而不是每个循环迭代一次。 循环代码可能随处使用“”str
一起threadData->strThread
编写,但执行速度会慢得多。 最好在循环中出现此类引用时缓存对数据的引用。
类 CThreadLocal
模板使用相同的机制 CProcessLocal
和相同的实现技术。