言前:由于最近复习了下动态链接库,所以决定写一个动态链接库专题。动态链接库网上各种达人已近写过很多了,资料也是一大把一大把的。但是我还是想写一份,因为网上讲的都很分散,讲的不是很系统,而且很多问题没有深究。因此我自己还是写一份吧,因为说明了是浅析。所以说我不可能讲的太深入,但是本文还是立足于有一定动态链接库编程基础的读者。 动态链接库专题我准备分为4篇文章来介绍 1.Win32 动态链接库 2.MFC常规动态链接库 3.MFC扩展动态链接库 4.各种链接库总结对比
好吧,现在就开始Win32 动态链接库编程吧。首先还是简单的介绍下基本知识(由于本人比较懒,所以只是大概提一下基本知识,这点上不会很全面,但是本人还是强烈建议大家,还是首先在网上系统的学习一下动态链接库的最基本知识。如动态链接库在内存上的映射方式,查看动态链接库的导入导出啊等等之类的最最最基本的知识)
基础知识:
1.动态链接库有很多的优点:如隐藏实现细节,实现代码共享,便于升级产品,可以在不同语言之间交互等等。
2.我们在编写好了动态链接库以后,我们必须导出我们留给用户使用的接口,然后用户在自己的程序里导入这些接口以便自己使用。导出的方式会有两种,一种是使用dllimport/dllexport ,还有一种是使用def 模块定义文件来导出,在后面我们会详细全面的介绍这种到出方法
3.调用:一共有两种调用动态链接库的方方式
动态调用:在程序中使用 LoadLibrary()/FreeLibrary() 动态的获得函数,类成员变量的指针。然后通过指针调用函数/成员函数。这种方法比较适合大型项目中使用,用户在我们要使用动态链接库的时候才去加载,使用完了以后就释放掉,这样对内存的利用效率还是比较高的。
静态调用:使用#pragma comment(lib,"XXXX.lib")或在编译器中设置,相关选项,来使用动态链接库,这种方式比较方便,不用我们显示的加载和释放动态链接库。程序在运行时会自动的去加载我们制定的动态链接库。在这种情况下我们可以用dumpbin 的imports 命令来查看到导入的动态链接库的接口。如果是动态调用的话,我们是无法查看到我们自己导出的动态链接库的imports信息的哦
动态链接库调用路径
4.我们导出的动态链接库通常,编译器在编译的时候通常都会给我们的函数修改名称这导致了,导出的函数的名称相应的发生了改变。下面便是编译器改名的具体规则。
对于C语言编译器
对于__stdcall调用约定,编译器和链接器会在输出函数名前加上一个下划线前缀,函数名后面加上一个“@”符号和其参数的字节数,例如_functionname@number。__cdecl调用约定仅在输出函数名前加上一个下划线前缀,例如_functionname。__fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,例如@functionname@number
对于C++
函数修饰都是以一个“?”开始,后面紧跟函数的名字,再后面是参数表的开始标识和按照参数类型代号拼出的参数表。对于__stdcall方式,参数表的开始标识是“@@YG”,对于__cdecl方式则是“@@YA”,对于__fastcall方式则是“@@YI”。参数表的拼写代号如下所示:
X--void
D--char
E--unsigned char
F--short
H--int
I--unsigned int
J--long
K--unsigned long(DWORD)
M--float
N--double
_N--bool
U--struct
对于C++的类成员函数(其调用方式是thiscall),函数的名字修饰与非成员的C++函数稍有不同,首先就是在函数名字和参数表之间插入以“@”字符引导的类名;其次是参数表的开始标识不同,公有(public)成员函数的标识是“@@QAE”,保护(protected)成员函数的标识是“@@IAE”,私有(private)成员函数的标识是“@@AAE”,如果函数声明使用了const关键字,则相应的标识应分别为“@@QBE”,“@@IBE”和“@@ABE”。如果参数类型是类实例的引用,则使用“AAV1”,对于const类型的引用,则使用“ABV1”。
假如我们不想编译器修改名称采用C++那种方式,我们可以指定以C语言的方式导出动态链接库,或者用Def文件指定我们导出文件的名字,(但是这样种方式可能遇到冲突,比如,我们定义了2个类,两个类都有同样名字的Create()函数,如果我们这两个类的Create函数有要用def指定文件导出,但是我们又想将这两个函数都以Create名称导出的话,此时便会有点小麻烦,后面我会提供一个例子来解决这个问题)。
5.DLLMAIN:
系统是在什么时候调用DllMain函数的呢?静态链接时,或动态链接时调用LoadLibrary和FreeLibrary都会调用DllMain函数。DllMain的第二个参数fdwReason指明了系统调用Dll的原因,它可能是::
DLL_PROCESS_ATTACH、
DLL_PROCESS_DETACH、
DLL_THREAD_ATTACH、
DLL_THREAD_DETACH。
以下从这四种情况来分析系统何时调用了DllMain。
1 DLL_PROCESS_ATTACH
大家都知道,一个程序要调用Dll里的函数,首先要先把DLL文件映射到进程的地址空间。要把一个DLL文件映射到进程的地址空间,有两种方法:静态链接和动态链接的LoadLibrary或者LoadLibraryEx。
当一个DLL文件被映射到进程的地址空间时,系统调用该DLL的DllMain函数,传递的fdwReason参数为DLL_PROCESS_ATTACH,这种调用只会发生在第一次映射时。如果同一个进程后来为已经映射进来的DLL再次调用LoadLibrary或者LoadLibraryEx,操作系统只会增加DLL的使用次数,它不会再用DLL_PROCESS_ATTACH调用DLL的DllMain函数。不同进程用LoadLibrary同一个DLL时,每个进程的第一次映射都会用DLL_PROCESS_ATTACH调用DLL的DllMain函数。
可参考DllMainTest的DLL_PROCESS_ATTACH_Test函数。
2 DLL_PROCESS_DETACH
当DLL被从进程的地址空间解除映射时,系统调用了它的DllMain,传递的fdwReason值是DLL_PROCESS_DETACH。当DLL处理该值时,它应该执行进程相关的清理工作。
那么什么时候DLL被从进程的地址空间解除映射呢?两种情况:
FreeLibrary解除DLL映射(有几个LoadLibrary,就要有几个FreeLibrary)
进程结束而解除DLL映射,在进程结束前还没有解除DLL的映射,进程结束后会解除DLL映射。(如果进程的终结是因为调用了TerminateProcess,系统就不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。)
注意:当用DLL_PROCESS_ATTACH调用DLL的DllMain函数时,如果返回FALSE,说明没有初始化成功,系统仍会用DLL_PROCESS_DETACH调用DLL的DllMain函数。因此,必须确保清理那些没有成功初始化的东西。
可参考DllMainTest的DLL_PROCESS_DETACH_Test函数。
3 DLL_THREAD_ATTACH
当进程创建一线程时,系统查看当前映射到进程地址空间中的所有DLL文件映像,并用值DLL_THREAD_ATTACH调用DLL的DllMain函数。
新创建的线程负责执行这次的DLL的DllMain函数,只有当所有的DLL都处理完这一通知后,系统才允许进程开始执行它的线程函数。
注意跟DLL_PROCESS_ATTACH的区别,我们在前面说过,第n(n>=2)次以后地把DLL映像文件映射到进程的地址空间时,是不再用DLL_PROCESS_ATTACH调用DllMain的。而DLL_THREAD_ATTACH不同,进程中的每次建立线程,都会用值DLL_THREAD_ATTACH调用DllMain函数,哪怕是线程中建立线程也一样。
4 DLL_THREAD_DETACH
如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread),系统查看当前映射到进程空间中的所有DLL文件映像,并用DLL_THREAD_DETACH来调用DllMain函数,通知所有的DLL去执行线程级的清理工作。
注意:如果线程的结束是因为系统中的一个线程调用了TerminateThread,系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数。
下面就通过def、和常规导出方法 对比着,讲解动态链接库
一、以_declspec方式导入导出 :
这种方式是最传统的方式,我相信很多人也最熟悉这种方式导出、导入数据库,通常我们通过定义宏来,来导出函数、类成员函数如下
导出:定义宏
#define DLLPORT _declspec(dllexport)
XXX.H/
#ifdef DLLPORT
#else
#define _declspec(dllimport)
#endif
///
导出函数
DLLPORT void Display(string txt);
在函数面前 加上我们先前定义的宏,这样便导出了函数。如果我们使用dumpbin 命令查询下 函数导出情况,我会发现:
我们注意到确实,函数是被导出了,但是被编译器修改了名字
如果我们不想让编译器改名,我们可以先申明extern “C”
将定义语言该为:#define DLLPORT extern"C" _declspec(dllexport)
重新编译,再用dumpbin 命令查看,导出函数没有被改名
(当然为了不想让函数被改名,我们也可以采用def模块定义文件,后面会详细讲解模块定义文件)
同理我们也可以导出一个变量:
DLLPORT int g_nTest = 2;
我们可以看到导出的变量同样被修改了名称,当然如果我们同样用extern"C"声明 ,我们也同样会看到变量也就不会改名了。
调用///
HMODULE hLibModule= LoadLibrary(TEXT("Win32_Dll_Test.dll"));//加载动态链接库,你懂的
typedef void(* PDISPLAY)(string str);
PDISPLAY pds = (PDISPLAY)GetProcAddress(hLibModule, "Display");
string str = "动态调DLL 函数:Dispaly..";
pds(str);
//调用导出变量
int *p;
p = (int *)GetProcAddress(hLibModule,"g_nTest");
cout<<"动态调用g_nTest: "<<*p<
动态 调用都是 先定义函数、变量的指针,然后或的函数、变量的地址,最后通过指针调用
当然,我们也可以使用静态加载
#pragma comment(lib, "Win32_Dll_Test.lib")//在头文件中告诉编译器在程序运行时加载动态链接库
调用时:
Display("动态调DLL 函数:Dispaly..");
cout<<"动态调用g_nTest: "<
导出类 :Win32 也可以导出类,但是Win32 DLL 导出的类中不能使用任何与MFC相关的东西。
为了导出类我们只需要 如下面定义我们的类
class DLLPORT Rectangle
{
public:
float m_Area;
public :
float CalcArea(float x, float y);
};
我们可以看到,类被导出了,但是请注意!在这里,我们并没有看到我们类里面定义的那个成员变量被导出。那么是不是那个成员变量 float m_Area 没有被导出呢?是不是由于没有导出,我们就不能在外面直接使用这个成员变量呢? 答案是否定的,我个人感觉,成员变量是导出了的,只是没有显示而已。因为后面你会看到,我们将能直接从外面使用,访问到该变量。这久隐含的说明了成员变量都被导出了,不是么?
(注意:在导出类时,是不能有extern "C"的,其中的原因 我想大家稍微想想就会明白的)
当然 ,如果你不想导出类的话,而是只想导出类里面的部分函数,那么你只需要将类内部要导出的函数 加上DLLPORT就可以了,这就知道出了指定的函数
class Rectangle
{
public:
float m_Area;//注意:我们不能导出成员变量 如果我们这样: DLLPORT float m_Area 编译器将会报错!
public :
DLLPORT float CalcArea(float x, float y);
};
通过dumpBin 我们可以发现,我们确实导出了 指定函数
//调用/
动态调用:
HMODULE hLibModule = LoadLibrary(TEXT("Win32_Dll_Test.dll"));
typedef float (haoer::Rectangle::*PFN)(float a,float b);
haoer::Rectangle *pRect = new haoer::Rectangle();
PFN pfn = (PFN)GetProcAddress(hLibModule,"?CalcArea@Rectangle@@QAEMMM@Z");
(pRect->*pfn)(2,2);
delete pRect;
FreeLibrary(hLibModule);
静态调用:
Rectangle Rect;
Rect.Calc(2,2);//访问类成员函数
cout<
结果:
二、采用def 文件定义导出
首先我们讲讲模块定义文件的知识
///
请查看:http://blog.csdn.net/lh844386434/article/details/6732539
///
使用def的方法 1.新建一个记事本 ,将其更名为 YourProgram.def
2.加入到你的工程里,并编写def 详细的类容
3.配置 编译器(如下图)
用def 导出 函数和变量
相信如果你看了,我上面给出链接的那篇,def详细的语法文章的话, 你可能已经对def文件的语法很熟悉了,下面我们就来看看导出函数,与变量的语法
//头文件
void Display(string txt);
class Rectangle
{
public:
float m_Area;
public :
float CalcArea(float x, float y);
};
//源文件中
int g_nTest = 2;
float Rectangle::CalcArea(float x, float y)
{
m_Area = x*y;
return m_Area;
}
void Display(string txt)
{
cout<<"动态链接库说: "<
接着定义def 文件
LIBARAY "Win32_Dll_Test" //指定dll的名称
EXPORTS //导出文件的申明
Disp=Display @1 //这句表明我们将Display 的函数导出,并且导出接口名字为Disp 也就是说,我们再外部想调用display的函数是,只能用Disp来调用
Calc=CalcArea @2 //这句同理,也是导出了CalcArea函数,不过请注意,这里与Display 不同,这里导出的是类成员函数
g_nTest @3 DATA //导出数据和导出函数一样, 不过唯一不同的就是,在导出数据必须要加DATA声明
可以看到的确,导出了,而且函数也是我们指定的名字 。def看起看来似乎也很方便,但是,这样做其实会遇到一个问题:
假如 我们现在要导出两个类A、B 但是呢A、B 中都有一个Test 函数,我们想把两个类里面的Test函数都导出来,并且都保持原来的名字(即2个名字都未Test)那怎么办了?
为了解决这种问题,其实我们可以效仿DX的调用方式,设计一个接口。通过指定不同的ID号,来访问不同的Test
具体实现规则请参见 这篇帖子: http://blog.csdn.net/lengxiao_wang/article/details/1546187
def 导出类
正如上面我们第一这个Rectangle类一样,要像导出他,我 们需按照下面的步骤来做:
class Rectangle
{
public:
float m_Area;
public :
float CalcArea(float x, float y);
};
0002:00000460 ?CalcArea@Rectangle@@QAEMMM@Z 10011460 f Win32_Dll_Test.obj
0002:000004b0 ?Display@@YAXV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z 100114b0 f Win32_Dll_Test.obj
5.在Exports中,指定导出 XXX.Def文件中,使之成为下面的样子(序号自己指定)
?CalcArea@Rectangle@@QAEMMM@Z @1
Display@@YAXV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z @2
Tips:Map文件的用途 不仅仅如此哦,Map文件还是查看程序崩溃的一种有效方法