MFC 模块状态的实现

本技术备忘录介绍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,避免循环调用->操作符,这样可以提高代码的效率。

posted @ 2018-05-16 21:54  onewayheaven  阅读(668)  评论(0编辑  收藏  举报