首页 > 代码库 > 动态链接库开发说明

动态链接库开发说明

1 基本概念    1

1.1 一个简单的例子    1

1.1.1 新建一个VC++项目    1

1.1.2 添加源文件    3

1.1.3 输入源代码    6

1.1.4 __declspec(dllexport)    7

1.1.5 WINAPI    7

1.1.6 导出符号    7

1.1.7 DEF文件    8

1.2 调用动态库    9

1.2.1 隐式链接    9

1.2.2 显式链接    10

1.3 导出数据    11

1.3.1 隐式链接    11

1.3.2 显式链接    12

1.4 导出类    12

1.4.1 成员类    12

1.4.2 导出模板类    13

1.4.3 内联成员函数    13

1.4.4 友元函数    14

1.4.5 嵌套类    14

1.4.6 静态成员变量    15

1.4.7 查看导出    15

1.5 导入类    16

1.5.1 内联成员函数    17

2 MFC Regular DLL    18

2.1 三种DLL    18

2.1.1 non-MFC Win32 DLL    18

2.1.2 MFC Regular DLL    18

2.1.3 MFC Extension DLL    18

2.2 模块状态    18

2.3 InitInstance    19

2.4 AfxGetApp    20

2.5 PreTranslateMessage    20

2.6 OnIdle    21

3 MFC Extension DLL    22

3.1 显式链接    22

3.2 查找资源    22

3.3 代码解析    23

 

 

1 基本概念

1.1 一个简单的例子

下面将使用VC++创建一个动态链接库文件。这个文件将导出两个函数StringReverseAStringReverseW,前者将一个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)修饰符用来导出函数StringReverseAStringReverseW。它还可以导出变量和类,这个后面介绍。

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确实导出了两个函数。请注意:每个导出函数都有一个序号,它是一个正整数。

技术分享

不过有意思的是:导出函数的名称并不是StringReverseAStringReverseW,而是_StringReverseA@4_StringReverseW@4

如果把Test.c改名为Test.cpp,则导出的名称更为复杂。请参考下图:

技术分享

这是什么原因呢?因为Test.c的扩展名为cVC++使用C编译器进行编译。Test.cpp的扩展名为cppVC++使用C++编译器进行编译。C++为了实现函数重载,编译时会根据参数类型和个数对函数名进行再次命名。

1.1.7 DEF文件

如何防止VC++编译器生成dll时将导出函数名更改掉?答案就是使用模块定义文件。请在VC++项目里增加模块定义文件Test.def。这个文件名可以是1.defA.def……只要扩展名是def即可。编辑Test.def,使其内容如下:

EXPORTS

StringReverseA

StringReverseW

上述内容表示:导出函数StringReverseAStringReverseW。此时,这两个函数前面的__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指定的目录。不用记这么多,最保险的做法就是将dllexe放在同一文件夹下。如果为了多个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); //卸载动态库文件

}

}

需要说明的是

1LoadLibrary("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");

3GetProcAddress的第二个参数可以指定函数名,如:"StringReverseA"。还可以指定为序号,如:GetProcAddress(hDll,(LPCSTR)1)GetProcAddress(hDll,MAKEINTRESOURCEA(1))。其中1表示导出函数的序号为1GetProcAddress如何区分第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 = http://www.mamicode.com/v;}>

int        GetValue();

private:

int        m_Value;

public:

static int    s_nValue;

};

int CTest::s_nValue = http://www.mamicode.com/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:

//友元函数

friend CTest 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 DLLMFC Regular DLLMFC 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里调用了AfxOleInitMFC Regular DLL是没有什么影响的,后者需要再次初始化COM库。但是AfxOleInit这个函数在MFC Regular DLL里没有发挥应有的作用(可能是MFCBUG吧)。

请参考如下代码:

class CDllApp : public CWinApp

{

public:

BOOL InitInstance()

{

AfxEnableControlContainer();

OleInitialize(NULL);

return TRUE;

}

int ExitInstance()

{

OleUninitialize();

return CWinApp::ExitInstance();

}

}theApp;

顺便说一句OleInitializeCoInitializeCoInitializeEx的区别:CoInitializeExCoInitialize的扩展;OleInitialize将调用CoInitialize初始化COM库,并且还做了其它一些工作,以支持OLE拖放等操作。

2.4 AfxGetApp

AfxGetApp() 等价于 AfxGetModuleState()->m_pCurrentWinApp,而AfxGetModuleState()则是当前模块状态。

所以AFX_MANAGE_STATE(AfxGetStaticModuleState());之后,AfxGetModuleState()就是AfxGetStaticModuleState(),即当前模块状态是本dll模块状态。AfxGetApp()将返回本dllCWinApp全局对象的地址(即theApp的地址)。

如果从exe调用dll的导出函数,而导出函数没有调用AFX_MANAGE_STATE(AfxGetStaticModuleState()),则AfxGetModuleState()将是AfxGetAppModuleState(),即当前模块状态是exe模块状态。AfxGetApp()将返回exeCWinApp全局对象的地址。如果exe不是MFC程序,而是VB6.0VC#程序,则AfxGetAppModuleState的返回值是不能使用的。

2.5 PreTranslateMessage

虽然MFC Regular DLL派生了CWinApp类,并有一个theApp全局对象。但它不包含CWinApp::Run机制,主消息由exe负责接收、分发。如果DLL 生成了无模式对话框或有自己的主框架窗口,则它应该导出函数来调用 PreTranslateMessageexe程序需要调用这个导出函数。示例代码如下:

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.0VC#程序,该如何处理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 DLLEXE)里查找,然后在各个MFC Extension DLL里查找,最后在MFC42.DLL里查找。

注意:如果是MFC Regular DLL,则默认情况下不会在各个MFC Extension DLL里查找资源。需要做如下处理后才行:

1MFC Extension DLL导出一个函数

__declspec(dllexport) void WINAPI ExtFunc()

{

new CDynLinkLibrary(ExtDLL);

}

2、在MFC Regular DLLInitInstance函数里导入调用该函数

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.CPPAfxFindResourceHandle函数说明了资源查找的顺序:

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_pModuleStateexe的模块状态,即AfxGetAppModuleState()的返回值。默认情况下MFC Extension DLL中的new CDynLinkLibrary(TestDLL)不会加入MFC Regular DLLm_pModuleState->m_libraryList,在此情况下MFC Regular DLL需要的资源不可能在MFC Extension DLL里找到。下面的代码把MFC Extension DLL中的new CDynLinkLibrary(TestDLL)加入到MFC Regular DLLm_pModuleState->m_libraryList

BOOL CRegDllApp::InitInstance()

{

{//

AFX_MANAGE_STATE(AfxGetStaticModuleState());

ExtFunc();

}

return CWinApp::InitInstance();

}

 

动态链接库开发说明