zoukankan      html  css  js  c++  java
  • 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,避免循环调用->操作符,这样可以提高代码的效率。

  • 相关阅读:
    树莓派控制Arduino
    树莓派的基本配置
    OneNet的产品创建和支持协议
    云平台基本认知(OneNet)
    初识Arduino
    NEFU-大二大三训练赛17C-最大值
    NEFU-大二大三训练赛17D-泡泡堂
    pb数据导出
    PB窗口根据分辨率的大小调整窗口大小
    PowerBuilder常用字符串函数
  • 原文地址:https://www.cnblogs.com/oneway1990/p/9048415.html
Copyright © 2011-2022 走看看