先看一个例子:
1、创建一个动态链接到MFC DLL的规则DLL,其内部包含一个对话框资源。指定该对话框ID如下:
#define IDD_DLL_DIALOG 2000
2、创建一个基于对话框的mfc应用程序,它包含两个对话框资源,IDD_UI_DIALOG和IDD_EXE_DIALOG。并将后者的ID指定如下:
#define IDD_EXE_DIALOG 2000
其中前者是这个应用程序的用户界面,单击上面的按钮,将弹出一个对话框。部分代码如下:
// in DLL void CDLL::ShowDlg(void) { CDialog dlg(IDD_DLL_DIALOG); //打开ID为2000的对话框 dlg.DoModal(); }
// in EXE void CEXE::OnButtonClick() { ShowDlg(); }
3、单击按钮,弹出的不是期望的DLL中的对话框IDD_DLL_DIALOG,而是应用程序中的对话框IDD_EXE_DIALOG。
解释:
1、应用程序进程本身及其调用的每个DLL模块都具有一个全局唯一的HINSTANCE句柄,它们代表了EXE或DLL模块在进程虚拟空间中的起始地址。(进程本身的模块句柄一般为0x400000,而DLL模块的缺省句柄为0x10000000。如果程序同时加载了多个DLL,则每个DLL模块都会有不同的HINSTANCE。应用程序在加载DLL时对其进行了重定位)。
2、共享MFC DLL(或MFC扩展DLL)的规则DLL涉及到HINSTANCE句柄问题,HINSTANCE句柄对于加载资源特别重要。EXE和DLL都有其自己的资源,而且这些资源ID可能重复,如果应用程序与规则DLL共享MFC DLL(或MFC扩展DLL),那么将总是默认使用EXE的资源。
3、因此应用程序需要通过资源模块的切换来找到正确的资源。如果应用程序需要来自于DLL的资源,就应将资源模块句柄指定为DLL的模块句柄;如果需要EXE文件中包含的资源,就应将资源模块句柄指定为EXE的模块句柄。
解决办法:
1、在DLL中改进:
- 方法1
// in DLL void CDLL::ShowDlg(void) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); CDialog dlg(IDD_DLL_DIALOG); //打开ID为2000的对话框 dlg.DoModal(); }
注:AFX_MANAGE_STATE(AfxGetStaticModuleState());一定是作为接口函数的第一条语句。
其功能是在栈上(这意味着其作用域是局部的)创建一个AFX_MODULE_STATE类的实例,并将其指针pModuleState返回。
AFX_MODULE_STATE类利用其构造函数和析构函数进行存储模块状态现场及恢复现场的工作。
该宏用于将pModuleState设置为当前的有效模块状态。当离开该宏的作用域时(也就离开了pModuleState所指栈上对象的作用域),先前的模块状态将由类AFX_MODULE_STATE的析构函数恢复。(即自动恢复)
- 方法2
// in DLL void CDLL::ShowDlg(void) { HINSTANCE save_hInstance = AfxGetResourceHandle(); AfxSetResourceHandle(theApp.m_hInstance); CDialog dlg(IDD_DLL_DIALOG); //打开ID为2000的对话框 dlg.DoModal(); AfxSetResourceHandle(save_hInstance); }
注:AfxGetResourceHandle:获取当前资源模块句柄;AfxSetResourceHandle:设置程序目前要使用的资源模块句柄。
同方法1比较,方法2能够灵活地设置程序的资源模块句柄,而方法1则只能在DLL接口函数退出的时候才会恢复模块句柄。
2、在应用程序中改进:
// in EXE void CEXE::OnButtonClick() { HINSTANCE exe_hInstance = GetModuleHandle(NULL); HINSTANCE dll_hInstance = GetModuleHandle("SharedDll.dll"); AfxSetResourceHandle(dll_hInstance); //切换状态 ShowDlg(); AfxSetResourceHandle(exe_hInstance); //恢复状态 }
注:使用状态切换的情况:当DLL导出函数包含MFC资源、类或者需要创建窗口时。
附加信息1
AFX_MANAGE_STATE(AfxGetStaticModuleState());//用于模块切换时的状态保护,
- AfxGetStaticModuleState()指向当前模块状态;
- 当前函数调用结束后原模块的状态自动被恢复;
- 用于DLL中所调用MFC函数、类、资源时的模块状态切换
摘自MSDN:
By default, MFC uses the resource handle of the main application to load the resource template. If you have an exported function in a DLL, such as one that launches a dialog box in the DLL, this template is actually stored in the DLL module. You need to switch the module state for the correct handle to be used. You can do this by adding the following code to the beginning of the function: AFX_MANAGE_STATE(AfxGetStaticModuleState( )); This swaps the current module state with the state returned from AfxGetStaticModuleState until the end of the current scope.
也就是說,並不是每一个dll的输出函数前都要调用它,只有在要輸出對話框等用到資源時要調用!
dll中资源是共享的用了这个函数的防止不同的进程修改资源产生错误!
缺省情况下MFC使用主应用程序的资源句柄来载入资源模板,而DLL中的资源模板是存在于DLL模板中,因此要使用这一语句切换到由AfxGetStaticModuleState返回的正确的模块状态,得到正确的句柄。
----------------------------------------------------------------------------------------------------------------------------------
动态链接到MFC的规则DLL所有输出的函数应该以如下语句开始:
AFX_MANAGE_STATE(AfxGetStaticModuleState( )) //此语句用来正确地切换MFC模块状态。作用在MSDN的解释:
By default, MFC uses the resource handle of the main application to load the resource template. If you have an exported function in a DLL, such as one that launches a dialog box in the DLL, this template is actually stored in the DLL module. You need to switch the module state for the correct handle to be used. You can do this by adding the following code to the beginning of the function:
AFX_MANAGE_STATE(AfxGetStaticModuleState( ));
在Dll中创建对话框并调用(简单的示例程序)
1、第一步创建一“MFC AppWizard (dll)”工程,接下来选择“Regular Dll using shared MFC DLL”,点击“Finish”。
2、添加一对话框资源到工程中,从菜单中选择Insert->Resource,添加一“Dialog”
选择“New”,至此对话框已添加到DLL工程中。
3、为对话框添加一新类,如:CTest,基类为CDialog。
4、在MFCDLL.cpp中(因创建的工程为MFCDLL)添加接口函数:
extern "C" __declspec(dllexport) void Show() { AFX_MANAGE_STATE(AfxGetStaticModuleState()); CTest test; test.DoModal (); }
别忘了在文件中添加: #include "Test.h":),大功告成,编译吧!
5、用VC新建一对话框工程,在一按钮点击事件中添加如下代码:
typedef void (WINAPI * TESTDLL)(); HINSTANCE hmod; hmod = ::LoadLibrary ("mfcdll.dll"); if(hmod==NULL) { AfxMessageBox("Fail"); } TESTDLL lpproc; lpproc = (TESTDLL)GetProcAddress (hmod,"Show"); if(lpproc!=(TESTDLL)NULL) (*lpproc)(); FreeLibrary(hmod);
/**********************************************************
/*
/* 2018/9.14补充
/*
**************************************************************
本技术备忘录介绍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,避免循环调用->操作符,这样可以提高代码的效率。