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

MFC 模块状态的实现的更多相关文章

  1. MFC模块状态(一)

    先看一个例子: 1.创建一个动态链接到MFC DLL的规则DLL,其内部包含一个对话框资源.指定该对话框ID如下:              #define IDD_DLL_DIALOG  2000 ...

  2. MFC模块状态(二)AFX_MANAGE_STATE(AfxGetStaticModuleState())

    以前写MFC的DLL的时候,总会在自动生成的代码框架里看到提示,需要在每一个输出的函数开始添加上AFX_MANAGE_STATE(AfxGetStaticModuleState()).一直不明白这样做 ...

  3. Vuex 单状态库 与 多模块状态库

    之前对 Vuex 进行了简单的了解.近期在做 Vue 项目的同时重新学习了 Vuex .本篇博文主要总结一下 Vuex 单状态库和多模块 modules 的两类使用场景. 本篇所有代码是基于 Vue- ...

  4. OpenCV MFC 模块间通信

    1. 新建MFC项目 点击完成. 2. 添加按钮 在"工具箱"中找到"Button"控件,添加至界面:  2. 配置opencv, 添加colordetecto ...

  5. saltStack 状态模块(状态间的关系)

    unless onlyif:状态间的条件判断,主要用于cmd状态模块 常用方法:    onlyif:检查的命令,仅当'onlyif'  选项指向的命令返回true时才执行name 定义的命 unle ...

  6. Win32 DLL和MFC DLL 中封装对话框

    现在最常看见的关于DLL的问题就是如何在DLL中使用对话框,这是一个很普遍的关于如何在DLL中使用资源的问题.这里我们从Win32   DLL和MFC   DLL两个方面来分析并解决这个问题.     ...

  7. c#调用c++ dll(一)

    首先来说说c++中的dll 核心的一些知识 比较大的应用程序都由很多模块组成,这些模块分别完成相对独立的功能,它们彼此协作来完成整个软件系统的工作.可能存在一些模块的功能较为通用,在构造其它软件系统时 ...

  8. 基于Visual C++6.0的DLL编程实现

    整理自基于Visual C++6.0的DLL编程实现 本文通过通俗易懂的方式,全面介绍了动态链接库的概念.动态链接库的创建和动态链接库的链接,并给出个简单明了的例子,相信读者看了本文后,能够创建自己的 ...

  9. C++ Dll 编写入门

    一.前言 自从微软推出16位的Windows操作系统起,此后每种版本的Windows操作系统都非常依赖于动态链接库(DLL)中的函数和数据,实际上 Windows操作系统中几乎所有的内容都由DLL以一 ...

随机推荐

  1. latex的资料ftp

    ftp://ftp.tex.ac.uk/ctan/tex-archive/ ftp ftp.tex.ac.uk anonymous ls

  2. RQNOJ PID331 家族

    题目描述 若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系. 规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚.如果x,y是 ...

  3. C#@字符的使用

    一,在字符串中的使用 //当在字符串前面加上一个@字符的时候,我们就可以把一个字符串定义在多行 // 编译器不会再去识别字符串中的转义字符 // 如果需要在字符串中表示一个双引号的话,需要使用两个双引 ...

  4. Hadoop伪分布式环境安装

    一.环境准备 阿里云ECS(Centos7).已预装JDK8 Hadoop安装包 hadoop-2.7.7.tar.gz 二. 安装步骤 1.确认JDK环境的安装位置 命令 echo $JAVA_HO ...

  5. 简易的富文本编辑器WangEditor

    网址http://www.wangeditor.com/ var E = window.wangEditor; var editor = new E('#editor') // 或者 var edit ...

  6. Codeforces 1101D 点分治

    题意:有一颗树,每个点有一个点权,边权都是1,问路径上的所有点的gcd不是1的最长路径是多少? 思路:之前补这道题的时候,用分解质因数 + 树形DP做的,其实用点分治可以更暴力一点.对于这类统计树上路 ...

  7. Codeforces 353E 贪心

    题意:给你一张有向图,第i条边连接i号点和(i + 1) % n号点,问最多可以选择多少个点,使得这些点互相不可达. 思路:容易发现,如果某个边的集合点的数目大于等于2,那么就可以选出一个点,当然也可 ...

  8. 九、结构模式之装饰(Decorator)模式

    装饰模式又叫包装模式,装饰模式以客户端透明的方式扩展对象的功能,是继承关系的一个替代方案.装饰模式可以在不使用创造更多的子类的情况下,将对象的功能加以扩展. 装饰模式结构图如下: 其包含的角色就分为: ...

  9. HashMap源码浅析

    HashMap源码主要一些属性 //默认的初始化容量(2的n次方) static final int default_inital_capacity = 16; //最大指定容量为2的30次方 sta ...

  10. @HttpEntity参数(怪异)

    1).在Controller中写 与@RequestBody请求体对应 @HttpEntity更强大,不光有请求体,还能获取请求头 @RequestMapping("/test02" ...