本技术备忘录介绍MFC “模块状态”结构的实现。充分理解模块状态这个概念对于在DLL中使用MFC的共享动态库是十分重要的。
MFC的状态信息分为三种:全局模块状态数据、进程局部状态数据和线程局部状态数据。有时这些数据类型之间没有严格界限,例如MFC的句柄表既是全局模块状态数据也属于线程局部状态数据。
进程局部状态数据和线程局部状态数据差不多。早先这些数据是全局的,但是为了更好的支持Win32和多线程,现在设计成进程或者线程相关的。模块状态数据既可以包含真正的全局状态数据,也可以指向进程或者线程相关的数据。
一、什么是模块状态?
模块状态实际上是指可执行模块运行所需的一个数据结构。首先要说明,这里的"模块"指的是一个MFC可执行程序,或者使用共享版本MFC动态库的DLL或者ActiveX控件。没有使用MFC的程序或者DLL等不在讨论范围之内。
正如下图"单个模块的状态数据"所描述的,使用MFC的每个模块都有一套状态数据。这些数据包括包括:窗口进程句柄(用于加载资源),指向当前程序的CWinApp和CWinThread对象的指针,OLE模块引用次数,以及很多关于Windows对象和其对应句柄的映射表等等。
单个模块(程序)的状态数据
+-------------MFC程序
|
//
+--------------------------------------------+
| |
| +--------------------------------+ |
| | | |
| | 线程对象 | |
| | | |
| +--------------------------------+ |
| | m_pModuleState +---+ |
| +--------------------------------+ | |
| // |
+--------------------------------------------+
| 状态数据 |
+--------------------------------------------+
(注意,因为采用的字符画图,如果图形显示有问题,请复制到记事本中看)
一个模块的所有状态数据包含在一个结构中,这个结构在MFC中被打包成一个类 AFX_MODULE_STATE, 它派生自 CNoTrackObject。关于这个类后面会谈到。AFX_MODULE_STATE类的定义位于AfxStat_.H中。内容如下所示:
// AFX_MODULE_STATE (模块的全局数据)
class AFX_MODULE_STATE : public CNoTrackObject
{
public: //构造函数
#ifdef _AFXDLL
AFX_MODULE_STATE(BOOL bDLL, WNDPROC pfnAfxWndProc, DWORD dwVersion);
AFX_MODULE_STATE(BOOL bDLL, WNDPROC pfnAfxWndProc, DWORD dwVersion,
BOOL bSystem);
#else
AFX_MODULE_STATE(BOOL bDLL);
#endif
~AFX_MODULE_STATE(); //析构函数
CWinApp* m_pCurrentWinApp; //指向CWinApp对象的指针
HINSTANCE m_hCurrentInstanceHandle; //当前进程句柄
HINSTANCE m_hCurrentResourceHandle; //当前资源句柄
LPCTSTR m_lpszCurrentAppName; //当前程序的文件名
BYTE m_bDLL; //TRUE表示模块是 DLL,否则是EXE
BYTE m_bSystem; //TRUE表示模块是系统模块。
BYTE m_bReserved[2]; //字节对齐
DWORD m_fRegisteredClasses; //窗口类注册标记
。。。//很多其它运行态数据
};
二、为什么需要切换模块状态
模块状态数据是十分重要的。因为很多MFC函数都要使用这些状态数据。如果一个MFC程序使用多模块,比如一个MFC程序需要调用多个DLL或者OLE控件的情况,则每个模块都拥有自己的一套MFC状态数据。
MFC程序运行过程中,每个线程都包含一个指向“当前”或者“有效”模块状态的指针(自然,这个指针是MFC的线程局部状态数据的一部分)。当线程执行代码流跨越模块边界,转入一个特定的模块的时候,就要改变这个指针的值,如下图所示,m_pModuleState必须设置成指向有效的模块状态数据。这一点是非常重要的,否则将导致无法预知的程序错误。
多模块下的状态数据
MFC程序
/
/ +--------------+
+--------------------------------------+ | DLL模块1 |
| | | |
| +----------------+ 转向模块1 | +--------------+
| | 线程对象 | +-----------+-------->| 状态数据 |
| | | | | +--------------+
| +----------------+ | |
| | m_pModuleState +-----+ | +--------------+
| | | 转向模块2 | | DLL模块2 |
| | +-----------------+----+ | |
| +----------------+ | | +--------------+
| | +--->| 状态数据 |
+--------------------------------------+ +--------------+
| 状态数据 |
+--------------------------------------+
(注意,因为采用的字符画图,如果图形显示有问题,请复制到记事本中看)
比如说,如果你在DLL中导出了一个函数,该函数要创建一个对话框,而这个对话框的模板资源位于DLL中。缺省情况下,MFC是使用主程序中的资源句柄来加载资源的,但现在这个对话框的资源位于DLL中,所以,必须设置m_pModuleState指向DLL模块的状态数据,否则,就会导致加载资源失败。
因此,每个模块要负责在它的所有入口点进行状态数据的切换。所谓"入口点" 就是任何执行代码流可以进入模块的地方,包括:
1、DLL中导出的函数;
2、COM接口函数
3、窗口过程
首先谈dll中的导出函数。一般来说,如果从一个DLL中导出了一个函数,应该使用AFX_MANAGE_STATE 宏维护正确的全局状态。
调用这个宏的时候,它设置pModuleState指向有效的模块状态数据,从而该函数后面的代码就可以通过该指针得到有效的状态数据。当函数执行完毕,即将返回时,该宏将自动恢复指针原来的值。
这个自动切换是这样完成的,在栈空间上创建一个AFX_MODULE_STATE类的实例,并把当前的模块状态指针保存在一个成员变量里面,然后把pModuleState设置成有效的模块状态,在这个实例对象的析构函数中,对象恢复以前保存的指针。
所以,对于上面所说的DLL导出函数,可以在该函数的开始加入如下预句:
AFX_MANAGE_STATE(AfxGetStaticModuleState( ))
这个代码将当前的模块状态设置成AfxGetStaticModuleState返回的值。离开当前作用域之后恢复原来的模块状态。
但是,不是任何DLL中导出的函数都需要使用AFX_MANAGE_STATE。例如InitInstance函数,MFC在调用这个函数的时候是自动切换模块状态的。对于MFC常规动态库中的所有消息处理函数来说也不需要使用这个宏。因为常规DLL会链接一个特殊的主窗口过程,里面会自动切换模块状态。对于其它导出函数,如果没有用到模块状态中的数据,也可以不使用这个宏。
对于COM接口的成员函数来说,一般使用METHOD_PROLOGUE宏来维护正确的模块状态数据。这个宏实际上也使用了AFX_MANAGE_STATE。详细信息可以参考技术备忘录38:"MFC/OLE IUnknown的实现"。
对于窗口过程,如果模块使用了MFC,则该模块会静态链接一个特殊的窗口过程实现函数,首先用AFX_MANAGE_STATE宏设置有效的模块状态,然后调用AfxWndProc,这个函数接着调用某窗口具体的WindowProc函数。具体可以参考WINCORE.CPP。
三、模块状态是如何切换的
一般来说,设置当前的模块状态数据可以通过函数AfxSetModuleState。但是大多数情况下,无需直接使用这个API函数,MFC知道应该如何正确设置模块状态数据,它会替你调用它,比如在WinMain函数、OLE入口、AfxWndProc中等等。这是通过静态链接一个特殊的WndProc和WinMain (或者DllMain)实现的。可以参考 DLLMODUL.CPP或者APPMODUL.CPP,找到这些实现代码。
设置当前的模块状态,而又不把它设置回去的情况是十分少见的,一般来讲,在改变了模块状态后,都要进行恢复。可以通过AFX_MANAGE_STATE宏和AFX_MAINTAIN_STATE类来实现。我们看看这个宏的定义:
#ifdef _AFXDLL //定义了这个符号表示动态链接MFC
struct AFX_MAINTAIN_STATE
{
AFX_MAINTAIN_STATE(AFX_MODULE_STATE* pModuleState);//参数是AFX_MODULE_STATE类对象指针
~AFX_MAINTAIN_STATE();
protected:
AFX_MODULE_STATE* m_pPrevModuleState; //保存在这个私有变量中
};
class _AFX_THREAD_STATE; //线程局部状态数据,这个类也是派生自CNoTrackObject
struct AFX_MAINTAIN_STATE2 //多线程版本
{
AFX_MAINTAIN_STATE2(AFX_MODULE_STATE* pModuleState);
~AFX_MAINTAIN_STATE2();
protected:
AFX_MODULE_STATE* m_pPrevModuleState; //用来保存模块状态数据的指针
_AFX_THREAD_STATE* m_pThreadState; //指向线程局部状态数据的指针
};
#define AFX_MANAGE_STATE(p) AFX_MAINTAIN_STATE2 _ctlState(p); //定义AFX_MANAGE_STATE宏
#else // _AFXDLL
#define AFX_MANAGE_STATE(p) //否则,这个宏没有意义。
#endif //!_AFXDLL
我们再来看看AFX_MAINTAIN_STATE2的构造函数,很简单的代码:
AFX_MAINTAIN_STATE2::AFX_MAINTAIN_STATE2(AFX_MODULE_STATE* pNewState)
{
m_pThreadState = _afxThreadState; //首先保存线程局部状态数据指针
m_pPrevModuleState = m_pThreadState->m_pModuleState; //保存全局模块状态数据指针
m_pThreadState->m_pModuleState = pNewState; //设置全局模块状态数据指针,指向pNewState。
}
由此可见,线程局部状态数据里面包含一个指向全局模块状态数据的指针。
四、进程局部数据
对于Win32 DLL,在每个关联它的进程中都有一份独立的数据拷贝。考虑如下代码:
static CString strGlobal; // at file scope
__declspec(dllexport)
void SetGlobalString(LPCTSTR lpsz)
{
strGlobal = lpsz;
}
__declspec(dllexport)
void GetGlobalString(LPCTSTR lpsz, int cb)
{
lstrcpyn(lpsz, strGlobal, cb);
}
如果上述代码位于一个DLL中,并且该DLL被两个进程A和B加载(或者同一个程序的两个实例),那么将会发生什么事情呢? A调用SetGlobalString("Hello from A"),结果,在进程A的上下文中为该CString对象分配内存空间,现在B 调用GetGlobalString(sz, sizeof(sz))。那么B是否可以访问到A 设置的数据呢?
在WIN3.1中是可以的,因为Win32s没有提供象Win32那样的进程间的保护措施。显然这是有问题的,为了解决这个问题。MFC 3.x 是采用线程局部存储(TLS)技术解决这个问题,和Win32下保存线程局部数据的方法类似。但是每个MFC DLL都要在每个进程中使用两个TLS索引,如果加载过多DLL,会很快消耗完TLS索引(只有64个)。除此以外,还有其它问题。所以在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, int cb)
{
lstrcpyn(lpsz, globalData->strGlobal, cb);
}
MFC采用两个步骤实现该方法。首先,在Win32 Tls* API (包括TlsAlloc, TlsSetValue, TlsGetValue等)之上实现一个接口层,无论进程加载多少DLL,每个进程仅需使用两个TLS索引。其次,通过CProcessLocal模板访问数据,它重载了->操作符。所有打包进CProcessLocal的对象必须派生自CNoTrackObject。而 CNoTrackObject提供一个底层的内存分配函数(LocalAlloc/LocalFree)以及一个虚析构函数,保证进程终止的时候,MFC可以自动销毁该进程局部数据。这些CNoTrackObject派生类对象可以有自己的析构函数,用于其它必要的清除操作。上面的例子里面没有,因为编译器会自动产生一个,并销毁内嵌的 CString 对象。CNoTrackObject类的定义位于Afxtls_.h中,主要是重载new 和 delete操作符,它的实现位于Afxtls.cpp中。
五、线程局部数据
和进程局部数据类似,线程局部数据是指必须和指定线程相关的局部数据,也就是说,不同线程访问同一个数据的时候,要为每个线程准备一份数据的实例。假设有一个CString对象,可以通过把它嵌入 CThreadLocal模板,使它成为线程局部数据:
struct CMyThreadData : public CNoTrackObject
{
CString strThread;
};
CThreadLocal<CMyThreadData> threadData;
void MakeRandomString()
{
// 一种洗牌方式,52张牌,效率很低,不实用
CString& str = threadData->strThread;
str.Empty();
while (str.GetLength() != 52)
{
TCHAR ch = rand() % 52 + 1;
if (str.Find(ch) < 0)
str += ch;
}
}
如果从两个不同的线程调用 MakeRandomString ,则每个线程都会打乱字符串的顺序,而且相互之间没有影响。这是因为每个线程都有一个strThread实例对象,而不是只有一个全局对象。
上述代码中使用了一个引用,而不是在循环中使用 threadData->strThread,避免循环调用->操作符,这样可以提高代码的效率。
转自:http://blog.csdn.net/playstudy/article/details/6426148