动态链接库开发说明
目录
第1章基本概念
1.1 一个简单的例子
下面将使用VC++创建一个动态链接库文件。这个文件将导出两个函数StringReverseA、StringReverseW,前者将一个ANSI字符串逆序,后者将一个Unicode字符串逆序。
1.1.1 新建一个VC++项目
对于VC++6.0而言,项目类型请选择Win32 Dynamic-Link Library。输入项目名称后,单击"OK"按钮。
在接下来的界面里,选择"An empty DLL project",然后单击"Finish"按钮。
在接下来的界面里单击"OK"按钮完成项目创建。
对于VC++9.0(即VC++2008)而言,项目类型请选择Win32。输入项目名称后,单击"确定"按钮。
在接下来的界面里,请选择"应用程序设置"下的"DLL"和"空项目"。单击"完成"按钮完成项目创建。
1.1.2 添加源文件
对于VC++6.0而言,在Workspace 窗口的 FileView 选项卡内,右键单击"Test files",在右键菜单里单击【Add Files to Project...】菜单项
输入源文件名后,单击"OK"按钮
弹出对话框里询问是否在项目里增加Test.c这个文件的引用。请单击"是"按钮。
此时鼠标双击Test.c。因为这个文件还不存在,VC++6.0会提示是否创建,请单击"是"按钮。
对于VC++9.0而言,在解决方案资源管理器里,右键单击"Test",在右键菜单里单击【添加】【新建项】菜单项。
接下来的界面内,请选择"C++文件(.cpp)",并输入源文件名Test.c,然后单击"添加"按钮。完成Test.c文件的添加和创建。
1.1.3 输入源代码
在Test.c里输入如下源代码:
#include <windows.h>
/***************************************************************************\ 将一个 Unicode 字符串逆序 \***************************************************************************/ __declspec(dllexport) wchar_t* WINAPI StringReverseW(wchar_t*wzStr) { if(wzStr) { int p1 = 0; int p2 = wcslen(wzStr) - 1; wchar_t t;
while(p1 < p2) { t = wzStr[p1]; wzStr[p1++] = wzStr[p2]; wzStr[p2--] = t; } } return wzStr; }
/***************************************************************************\ 将一个 ANSI 字符串逆序 \***************************************************************************/ __declspec(dllexport) char* WINAPI StringReverseA(char*szStr) { if(szStr) { int nLenA = strlen(szStr) + 1; int nLenW = MultiByteToWideChar(CP_ACP,0,szStr,nLenA,NULL,0); wchar_t*pStrW = (wchar_t*)malloc(nLenW * sizeof(wchar_t));
MultiByteToWideChar(CP_ACP,0,szStr,nLenA,pStrW,nLenW); StringReverseW(pStrW); WideCharToMultiByte(CP_ACP,0,pStrW,nLenW,szStr,nLenA,NULL,NULL); free(pStrW); } return szStr; } |
1.1.4 __declspec(dllexport)
__declspec(dllexport)修饰符用来导出函数StringReverseA和StringReverseW。它还可以导出变量和类,这个后面介绍。
1.1.5 WINAPI
WINAPI 其实就是__stdcall。以StringReverseA为例,调用它时,参数szStr将被压入栈中,从StringReverseA返回时,参数szStr需要出栈。__stdcall表示由StringReverseA自己执行出栈操作。假如将__stdcall去掉或换为__cdecl,则由调用StringReverseA的函数负责执行出栈操作。说了这么多,最重要的是:某些语言,如VB6.0只支持__stdcall,所以为了让这个dll被尽可能多的编程语言支持,请使用WINAPI。
1.1.6 导出符号
现在可以编译程序,生成Test.dll了。使用eXeScope6.30打开Test.dll,可以看到Test.dll确实导出了两个函数。请注意:每个导出函数都有一个序号,它是一个正整数。
不过有意思的是:导出函数的名称并不是StringReverseA和StringReverseW,而是_StringReverseA@4和_StringReverseW@4。
如果把Test.c改名为Test.cpp,则导出的名称更为复杂。请参考下图:
这是什么原因呢?因为Test.c的扩展名为c,VC++使用C编译器进行编译。Test.cpp的扩展名为cpp,VC++使用C++编译器进行编译。C++为了实现函数重载,编译时会根据参数类型和个数对函数名进行再次命名。
1.1.7 DEF文件
如何防止VC++编译器生成dll时将导出函数名更改掉?答案就是使用模块定义文件。请在VC++项目里增加模块定义文件Test.def。这个文件名可以是1.def、A.def……只要扩展名是def即可。编辑Test.def,使其内容如下:
EXPORTS StringReverseA StringReverseW |
上述内容表示:导出函数StringReverseA和StringReverseW。此时,这两个函数前面的__declspec(dllexport)修饰符将不再需要。
DEF文件的功能还有很多,具体请参考MSDN。
1.2 调用动态库
生成的动态库文件可以被多种编程语言使用。限于篇幅下面仅介绍VC++如何调用动态库。
1.2.1 隐式链接
编译动态库文件时,同时会生成Lib文件。使用动态库的VC++程序可以链接这个Lib文件,这就是隐式链接。可参考的代码如下:
#include <windows.h> #include <stdio.h>
__declspec(dllimport) char* WINAPI StringReverseA(char*szStr); #pragma comment(lib,"D:/VC6/Test/Debug/Test.lib")
void main() { char szStr[] = "隐式链接动态库"; puts(StringReverseA(szStr)); } |
__declspec(dllimport) char* WINAPI StringReverseA(char*szStr); 是函数声明。修饰符__declspec(dllimport)表示这是一个导入函数。去除这个修饰符不影响程序的编译、运行,但有了__declspec(dllimport)之后,生成的代码更小,运行更快。
注意:对于C++程序而言,可能需要这样声明函数:
extern "C"
{
__declspec(dllimport) char* WINAPI StringReverseA(char*szStr);
}
extern "C" 的作用是:告诉C++编译器连接时不要以C++语法修改StringReverseA的名称。为什么说是"可能"需要extern "C"呢?这与dll的编译有关系。如果使用C编译器编译dll,则需要extern "C";如果使用C++编译器编译dll,则不需要extern "C"。
#pragma comment(lib,"D:/VC6/Test/Debug/Test.lib")表示链接的时候使用D:\VC6\Test\Debug\Test.Lib文件。就是编译动态库时产生的那个Lib文件。
采用隐式链接,运行程序的时候动态库文件首先被加载至内存。系统如何定位dll文件呢?其搜索顺序为:exe所在目录、当前目录(GetCurrentDirectory)、System32目录(GetSystemDirectory)、Windows目录(GetWindowsDirectory)、环境变量PATH指定的目录。不用记这么多,最保险的做法就是将dll和exe放在同一文件夹下。如果为了多个exe程序共享一个dll,请将这个dll文件复制到System32目录下。
1.2.2 显式链接
显式链接可以灵活控制动态库文件的加载、卸载。其使用步骤如下:
1、使用LoadLibrary函数载入动态库文件至内存;
2、使用GetProcAddress函数获得导出函数的地址;
3、调用导出函数;
4、使用FreeLibrary卸载动态库文件。
可参考如下代码:
#include <windows.h> #include <stdio.h>
void main() { HINSTANCE hDll = LoadLibrary("Test.dll"); //载入动态库文件 if(hDll) {//载入成功 char* (WINAPI*pfn)(char*szStr) = NULL; //声明一个函数指针 //获得函数StringReverseA的指针 #ifdef __cplusplus (FARPROC&)pfn = GetProcAddress(hDll,"StringReverseA"); #else (FARPROC)pfn = GetProcAddress(hDll,"StringReverseA"); #endif if(pfn) {//成功获得函数指针 char szStr[] = "显式链接动态库"; pfn(szStr); //调用函数,等价于(*pfn)(szStr) puts(szStr); } FreeLibrary(hDll); //卸载动态库文件 } } |
需要说明的是
1、LoadLibrary("Test.dll")在载入Test.dll时是有搜索顺序的:首先在exe所在目录查找,然后在当前目录(GetCurrentDirectory)下查找,然后在System32目录下查找……具体请参考MSDN帮助;
2、注意获得函数指针的C代码和C++代码是不同的,它们通过#ifdef __cplusplus这个条件编译语句来区分。或者使用C/C++通用的代码:
typedef char* (WINAPI*STRINGREVERSEA)(char*szStr);
STRINGREVERSEA pfn = (STRINGREVERSEA)GetProcAddress(hDll,"StringReverseA");
3、GetProcAddress的第二个参数可以指定函数名,如:"StringReverseA"。还可以指定为序号,如:GetProcAddress(hDll,(LPCSTR)1)或GetProcAddress(hDll,MAKEINTRESOURCEA(1))。其中1表示导出函数的序号为1。GetProcAddress如何区分第2个参数是名称还是序号?对于字符串而言,首地址是一定大于0xFFFF的,而序号必须小于等于0xFFFF。这就是判断的依据。使用序号定位导出函数,效率上会高一些,但是这样的代码不利于阅读和维护;
4、关于FreeLibrary,需要说明的是:不能自己释放自己。如下面的函数在Test.dll内,其意图是自己释放自己。实际上它是行不通的:
void FreeMyself(HINSTANCE hDll)
{
FreeLibrary(hDll);
}
1.3 导出数据
dll导出数据很简单,下面是一个示例:
__declspec(dllexport) int nDataInDll;
或者
extern "C"
{//C++编译时,防止重命名导出符号nDataInDll
__declspec(dllexport) int nDataInDll;
}
或者使用DEF文件,其内容如下
EXPORTS nDataInDll DATA |
1.3.1 隐式链接
客户端程序隐式链接dll时,使用导出数据很简单。首先是按下列语法声明变量,然后就可以使用了。
extern "C" //是否使用extern "C"需要根据实际情况而定
{
__declspec(dllimport) int nDataInDll;
}
1.3.2 显式链接
客户端程序显式链接dll时,使用导出数据稍显麻烦,其代码如下:
HINSTANCE hDll = LoadLibrary("Test.dll"); //载入动态库文件 if(hDll) {//载入成功 int* nDataDll = NULL; #ifdef __cplusplus (FARPROC&)nDataDll = GetProcAddress(hDll,"nDataInDll"); #else (FARPROC)nDataDll = GetProcAddress(hDll,"nDataInDll"); #endif //使用数据 ... ... ... FreeLibrary(hDll); //卸载动态库文件 } |
注意:GetProcAddress获得的是数据的地址。
1.4 导出类
导出类的语法有两种。方法1是定义类的时候同时定义导出,方法2是先声明类导出,再定义类。方法2比方法1灵活,但有时会有限制。
方法1: class __declspec(dllexport) CTest { public: int m_nValue; CObj m_obj; }; |
方法2: //类声明,说明是一个导出类 class __declspec(dllexport) CTest; class CTest { public: int m_nValue; CObj m_obj; }; |
导出类的实质其实就是把类的成员函数给导出了。
1.4.1 成员类
以上面的代码为例,实例化CTest时需要构造m_obj。因此CObj也必须被导出,否则编译的时候会产生警告,客户程序可能无法正常构造CTest类(Debug版正常,Release版分配内存但不调用构造函数)。
1.4.2 导出模板类
首先看下面的代码
template <class TYPE> class CTemplate { public: CTemplate() { a = 0; } public: TYPE a; };
template class __declspec(dllexport) CTemplate<int>; template class __declspec(dllexport) CTemplate<double>;
class __declspec(dllexport) CUseTemplate { public: CTemplate<int> i; CTemplate<double> d; }; |
导出CUseTemplate时,CTemplate<int>和CTemplate<double>也应该被导出。
注意:类模板是无法导出的,如下面的代码无法导出CTemplate。
template <class TYPE> class __declspec(dllexport) CTemplate { public: CTemplate() { a = 0; } public: TYPE a; }; |
如果不想导出模板类,请修改成员变量为指针类型。这样的话,成员变量的构造、析构将在DLL内完成,而不是在客户程序里完成。
class __declspec(dllexport) CUseTemplate { public: CTemplate<int>* pi; CTemplate<double>* pd; }; |
1.4.3 内联成员函数
内联函数相当于宏,编译的时候用来替换源代码,用以提高效率。一般它是不会被编译成目标代码的,但是一旦使用了__declspec(dllexport),编译程序将会为其生成一份目标代码,并导出。
1.4.4 友元函数
友元函数的实质上还是一个函数,只不过它们是类的朋友,可以访问类的私有成员变量。友元函数的导出,需要专门声明。具体方法如下:
方法1: class __declspec(dllexport) CTest { public: int m_nValue; public: //导出友元函数要专门声明 friend __declspec(dllexport) CTest operator+(const CTest&a,const CTest&b); }; |
方法2: //类声明 class __declspec(dllexport) CTest; //友元函数声明 __declspec(dllexport) CTest operator+(const CTest&a,const CTest&b); class CTest { public: int m_nValue; public: friend CTest operator+(const CTest&a,const CTest&b); }; |
1.4.5 嵌套类
下面是导出嵌套类的示例代码:
class __declspec(dllexport) CTest { public: //导出嵌套类,前面也要使用__declspec(dllexport) class __declspec(dllexport) CNest { } }; |
1.4.6 静态成员变量
一个类被导出,则该类的所有静态成员变量被当作变量导出。如:下面的代码:
class __declspec(dllexport) CTest { public: static int s_nValue; //s_nValue 被当作变量导出 }; |
1.4.7 查看导出
按下图设置VC++6.0,使得编译时生成map文件。
编译如下代码
class __declspec(dllexport) CTest { public: void SetValue(int v) {m_Value = v;} int GetValue(); private: int m_Value; public: static int s_nValue; }; int CTest::s_nValue = 1; int CTest::GetValue() {return m_Value;} |
编译后查看 map 文件,提取包含 CTest 的函数或变量:
Publics by Value |
Rva+Base |
说明 |
?SetValue@CTest@@QAEXH@Z |
10001030 f i |
SetValue函数 |
??4CTest@@QAEAAV0@ABV0@@Z |
10001070 f i |
构造函数 |
?GetValue@CTest@@QAEHXZ |
100010b0 f |
GetValue函数 |
?s_nValue@CTest@@2HA |
1002ba30 |
s_nValue |
注意上表的第2列,f表示函数,i表示内联。如果将 class __declspec(dllexport) CTest 中的 __declspec(dllexport) 去掉,重新编译,则上表第2列包含 f i 的在map文件中不会再出现。
去掉CTest定义中的__declspec(dllexport)后,可以使用 DEF文件导出一些CTest成员函数,如:下面的DEF文件导出了函数CTest::GetValue和变量CTest::s_nValue。
EXPORTS ?GetValue@CTest@@QAEHXZ ?s_nValue@CTest@@2HA DATA |
1.5 导入类
客户端程序可以使用dll导出的类,定义类的时候需要__declspec(dllimport)
方法1: class __declspec(dllimport) CTest { public: int m_nValue; public: //友元函数 friend __declspec(dllimport) CTest operator+(const CTest&a,const CTest&b); }; |
方法2: class __declspec(dllimport) CTest; //类声明 //下面这句话不再需要 //friend __declspec(dllimport) CTest operator+(const CTest&a,const CTest&b); class CTest { public: int m_nValue; public: //友元函数 friendCTest operator+(const CTest&a,const CTest&b); }; |
1.5.1 内联成员函数
考虑如下代码:
class __declspec(dllimport) CTest { public: int m_nValue; public: int GetValue() { return m_nValue; } }; |
现在的问题是:对于内联函数GetValue,到底使用这里的定义,还是使用dll导出的内联函数?经测试发现:在__declspec(dllimport)存在的情况下,将使用dll导出的内联函数(此时就不是内联了);在删除上面的__declspec(dllimport)之后,将使用上面定义的内联函数。
第2章 MFC Regular DLL
2.1 三种DLL
使用VC++生成dll,共有三种类型。分别为:non-MFC Win32 DLL、MFC Regular DLL和MFC Extension DLL。
2.1.1 non-MFC Win32 DLL
上一章创建的 Win32 Dynamic-Link Library 即为non-MFC Win32 DLL。它的特点是可以使用MFC,也可以不使用MFC。导出的函数、类涉及界面、Windows消息处理较少。它特别适合封装数值计算、硬件控制等功能,此外它还可用于制作纯资源dll(连接时指定 /NOENTRY 即可)。
它可以被多种编程语言所支持。
2.1.2 MFC Regular DLL
对于过多的界面处理,如果不借助MFC,则难度是比较大的。此时,可以使用MFC Regular DLL。它必须使用MFC共享库,可以静态连接也可以动态链接MFC共享库,建议动态链接。
它也可以被多种编程语言所支持。
2.1.3 MFC Extension DLL
扩展的含义是对MFC类的扩展,它主要用于实现MFC类的派生类。如:如果觉得CButton类不太好,可以派生一个CButtonEx类,并在MFC Extension DLL里实现、导出CButtonEx。任何MFC程序都可以直接使用这个CButtonEx。
它必须使用MFC共享库,而且必须动态连接MFC共享库。只有MFC程序才能使用它。
2.2 模块状态
使用MFC Regular DLL要特别注意模块状态。在导出函数的第一行请增加如下代码:
AFX_MANAGE_STATE(AfxGetStaticModuleState());
它在栈上创建了一个对象。该对象记录下当前的模块状态,然后设置当前模块状态为本dll模块状态。导出函数返回的时候,对象被析构。析构时,会恢复当前模块状态。
如果不注意模块状态的切换,则本dll内的资源可能无法被调用,甚至程序会崩溃。
2.3 InitInstance
CWinApp派生类的InitInstance函数必须返回 TRUE,否则dll将加载失败。
如果dll内部需要 ActiveX 控件或 OLE 拖放,请在InitInstance里调用OleInitialize(NULL),ExitInstance里调用OleUninitialize。因为每个MFC模块都有自己的模块状态,所以exe里调用了AfxOleInit对MFC Regular DLL是没有什么影响的,后者需要再次初始化COM库。但是AfxOleInit这个函数在MFC Regular DLL里没有发挥应有的作用(可能是MFC的BUG吧)。
请参考如下代码:
class CDllApp : public CWinApp { public: BOOL InitInstance() { AfxEnableControlContainer(); OleInitialize(NULL); return TRUE; } int ExitInstance() { OleUninitialize(); return CWinApp::ExitInstance(); } }theApp; |
顺便说一句OleInitialize与CoInitialize、CoInitializeEx的区别:CoInitializeEx是CoInitialize的扩展;OleInitialize将调用CoInitialize初始化COM库,并且还做了其它一些工作,以支持OLE拖放等操作。
2.4 AfxGetApp
AfxGetApp() 等价于 AfxGetModuleState()->m_pCurrentWinApp,而AfxGetModuleState()则是当前模块状态。
所以AFX_MANAGE_STATE(AfxGetStaticModuleState());之后,AfxGetModuleState()就是AfxGetStaticModuleState(),即当前模块状态是本dll模块状态。AfxGetApp()将返回本dll内CWinApp全局对象的地址(即theApp的地址)。
如果从exe调用dll的导出函数,而导出函数没有调用AFX_MANAGE_STATE(AfxGetStaticModuleState()),则AfxGetModuleState()将是AfxGetAppModuleState(),即当前模块状态是exe模块状态。AfxGetApp()将返回exe内CWinApp全局对象的地址。如果exe不是MFC程序,而是VB6.0、VC#程序,则AfxGetAppModuleState的返回值是不能使用的。
2.5 PreTranslateMessage
虽然MFC Regular DLL派生了CWinApp类,并有一个theApp全局对象。但它不包含CWinApp::Run机制,主消息由exe负责接收、分发。如果DLL 生成了无模式对话框或有自己的主框架窗口,则它应该导出函数来调用 PreTranslateMessage。exe程序需要调用这个导出函数。示例代码如下:
DLL端需要导出函数,调用AfxGetApp()->PreTranslateMessage __declspec(dllexport) BOOL DllPreTranslateMessage(MSG* pMsg) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); //切换模块状态 return AfxGetApp()->PreTranslateMessage(pMsg); } |
exe端需要调用DLL的导出函数 class CTestApp : public CWinApp { public: BOOL PreTranslateMessage(MSG* pMsg) { if(DllPreTranslateMessage(pMsg)) { return TRUE; } return CWinApp::PreTranslateMessage(pMsg); } ... ... ... }theApp; |
如果不这么做,则dll内部的非模态对话框PreTranslateMessage函数不会被执行,对话框内按Tab键也无法切换焦点。
如果exe不是MFC程序,而是VB6.0、VC#程序,该如何处理PreTranslateMessage?这个问题需要再进行深入研究。此时,非模态对话框内按Tab键无法切换焦点,该如何处理?估计得使用键盘钩子了……
2.6 OnIdle
一个标准的MFC程序里,主窗口菜单项、工具栏的显示更新,以及临时对象的销毁是在应用程序类的OnIdle里进行处理的。如果MFC Regular DLL里有框架窗口(CFrameWnd),并有菜单、工具栏,则它也需要处理OnIdle。方法与PreTranslateMessage的处理相同,其代码如下:
DLL端需要导出函数,调用AfxGetApp()->OnIdle __declspec(dllexport) void DllOnIdle(LONG lCount) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); //切换模块状态 AfxGetApp()->OnIdle(lCount); } |
exe端需要调用DLL的导出函数 class CTestApp : public CWinApp { public: BOOL OnIdle(LONG lCount) { DllOnIdle(lCount); return CWinApp::OnIdle(lCount); } ... ... ... }theApp; |
如果exe端不是MFC程序,则在dll内部需要定时调用如下代码:
AFX_MANAGE_STATE(AfxGetStaticModuleState());
AfxGetApp()->OnIdle(0); //更新菜单、工具栏
AfxGetApp()->OnIdle(1); //删除临时对象
可以把这几行代码放在导出函数的最前面。
第3章 MFC Extension DLL
如前所言,MFC Extension DLL主要用于实现MFC类的派生类。如:如果觉得CButton类不太好,可以派生一个CButtonEx类,并在MFC Extension DLL里实现、导出CButtonEx。任何MFC程序都可以直接使用这个CButtonEx。当然是可以在MFC Extension DLL里创建一个非模态对话框的,但它的PreTranslateMessage同样不会被主动执行。也就是说MFC Extension DLL的主要功能不是处理界面、Windows消息,而是扩展MFC已有的类。
MFC Extension DLL必须使用MFC共享库,而且必须动态连接MFC共享库。只有MFC程序才能使用它。
顺便说一句:MFC Extension DLL不再像MFC Reguler DLL那样需要切换模块状态。
3.1 显式链接
大多数情况下,MFC Extension DLL都是隐式连接的,因为它导出的基本上都是扩展MFC的类。但显式链接也是可行的,不过使用的不是LoadLibrary,而是AfxLoadLibrary。卸载的函数也由FreeLibrary改为AfxFreeLibrary。
3.2 查找资源
代码CDialog(_T("DlgRes")).DoModal();用于显示一个对话框。为了显示这个对话框,需要查找对话框资源"DlgRes"。查找资源的顺序如下图所示:
即:首先在本模块(MFC Regular DLL或EXE)里查找,然后在各个MFC Extension DLL里查找,最后在MFC42.DLL里查找。
注意:如果是MFC Regular DLL,则默认情况下不会在各个MFC Extension DLL里查找资源。需要做如下处理后才行:
1、MFC Extension DLL导出一个函数
__declspec(dllexport) void WINAPI ExtFunc() { new CDynLinkLibrary(ExtDLL); } |
2、在MFC Regular DLL的InitInstance函数里导入调用该函数
BOOL CRegDllApp::InitInstance() { {// AFX_MANAGE_STATE(AfxGetStaticModuleState()); ExtFunc(); } return CWinApp::InitInstance(); } |
现在MFC Regular DLL里的代码CDialog(_T("DlgRes")).DoModal();在执行时就会在MFC Extension DLL里查找资源"DlgRes"了。
3.3 代码解析
MFC模块状态是一个AFX_MODULE_STATE,它里面有一个成员变量
CTypedSimpleList<CDynLinkLibrary*> m_libraryList;
它是exe模块用到的所有的MFC Extension DLL链表。MFC Extension DLL是通过DllMain完成初始化的,其典型代码如下:
static AFX_EXTENSION_MODULE TestDLL = { NULL, NULL };
extern "C" int APIENTRY DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) { if (dwReason == DLL_PROCESS_ATTACH) { if (!AfxInitExtensionModule(TestDLL, hInstance)) return 0; new CDynLinkLibrary(TestDLL); } else if (dwReason == DLL_PROCESS_DETACH) { AfxTermExtensionModule(TestDLL); } return 1; // ok } |
new CDynLinkLibrary(TestDLL)将创建一个CDynLinkLibrary对象,并将自己加入m_libraryList,其代码如下:
CDynLinkLibrary::CDynLinkLibrary(AFX_EXTENSION_MODULE&,BOOL) { ... ... ... m_pModuleState->m_libraryList.AddHead(this); ... ... ... } |
C:\Program Files\Microsoft Visual Studio\VC98\MFC\SRC\DLLINIT.CPP里AfxFindResourceHandle函数说明了资源查找的顺序:
HINSTANCE AFXAPI AfxFindResourceHandle(LPCTSTR lpszName, LPCTSTR lpszType) { HINSTANCE hInst; ... ... if(!pModuleState->m_bSystem) {//第1步 hInst = AfxGetResourceHandle(); } ... ... for(CDynLinkLibrary* pDLL = pModuleState->m_libraryList; pDLL != NULL;pDLL = pDLL->m_pNextDLL) {//第2步 if(!pDLL->m_bSystem && pDLL->m_hResource != NULL && ::FindResource(pDLL->m_hResource, lpszName, lpszType)) { return pDLL->m_hResource; } } ... ... hInst = pModuleState->m_appLangDLL; //第3步 ... ... if(!pModuleState->m_bSystem) {//第4步 hInst = AfxGetResourceHandle(); } ... ... for(pDLL = pModuleState->m_libraryList; pDLL != NULL; pDLL = pDLL->m_pNextDLL) {//第5步 if(pDLL->m_bSystem && pDLL->m_hResource != NULL && ::FindResource(pDLL->m_hResource, lpszName, lpszType)) { return pDLL->m_hResource; } } ... ... return AfxGetResourceHandle();//第6步 } |
注意:第2步与第5步的区别在于MFC Extension DLL一个是非系统的(!pDLL->m_bSystem),另一个是系统的(pDLL->m_bSystem)。MFC42.DLL就是一个系统的MFC Extension DLL。
注意CDynLinkLibrary构造函数里的代码m_pModuleState->m_libraryList.AddHead(this);其中的m_pModuleState是exe的模块状态,即AfxGetAppModuleState()的返回值。默认情况下MFC Extension DLL中的new CDynLinkLibrary(TestDLL)不会加入MFC Regular DLL的m_pModuleState->m_libraryList,在此情况下MFC Regular DLL需要的资源不可能在MFC Extension DLL里找到。下面的代码把MFC Extension DLL中的new CDynLinkLibrary(TestDLL)加入到MFC Regular DLL的m_pModuleState->m_libraryList。
BOOL CRegDllApp::InitInstance() { {// AFX_MANAGE_STATE(AfxGetStaticModuleState()); ExtFunc(); } return CWinApp::InitInstance(); } |