使用Visual C++ 6.0创建dll
一、前言
自从微软推出16位的Windows操作系统起,此后每种版本的Windows操作系统都非常依赖于动态链接库(DLL)中的函数和数据,实际上 Windows操作系统中几乎所有的内容都由DLL以一种或另外一种形式代表着,例如显示的字体和图标存储在GDI DLL中、显示Windows桌面和处理用户的输入所需要的代码被存储在一个User DLL中、Windows编程所需要的大量的API函数也被包含在Kernel DLL中。
在Windows操作系统中使用DLL有很多优点,最主要的一点是多个应用程序、甚至是不同语言编写的应用程序可以共享一个DLL文件,真正实现了资源"共享",大大缩小了应用程序的执行代码,更加有效的利用了内存;使用DLL的另一个优点是DLL文件作为一个单独的程序模块,封装性、独立性好,在软件需要升级的时候,开发人员只需要修改相应的 DLL文件就可以了,而且,当DLL中的函数改变后,只要不是参数的改变,程序代码并不需要重新编译。这在编程时十分有用,大大提高了软件开发和维护的效率。
既然DLL那么重要,所以搞清楚什么是DLL、如何在Windows操作系统中开发使用DLL是程序开发人员不得不解决的一个问题。本文针对这些问题,通过一个简单的例子,即在一个DLL中实现比较最大、最小整数这两个简单函数,全面地解析了在Visual C++编译环境下编程实现DLL的过程,文章中所用到的程序代码在Windows98系统、Visual C++6.0编译环境下通过。
二、DLL的概念
DLL是建立在客户/服务器通信的概念上,包含若干函数、类或资源的库文件,函数和数据被存储在一个DLL(服务器)上并由一个或多个客户导出而使用,这些客户可以是应用程序或者是其它的DLL。DLL库不同于静态库,在静态库情况下,函数和数据被编译进一个二进制文件(通常扩展名为*.LIB),Visual C++的编译器在处理程序代码时将从静态库中恢复这些函数和数据并把他们和应用程序中的其他模块组合在一起生成可执行文件。这个过程称为"静态链接",此时因为应用程序所需的全部内容都是从库中复制了出来,所以静态库本身并不需要与可执行文件一起发行。
在动态库的情况下,有两个文件,一个是引入库(.LIB)文件,一个是DLL文件,引入库文件包含被DLL导出的函数的名称和位置,DLL包含实际的函数和数据,应用程序使用LIB文件链接到所需要使用的DLL文件,库中的函数和数据并不复制到可执行文件中,因此在应用程序的可执行文件中,存放的不是被调用的函数代码,而是DLL中所要调用的函数的内存地址,这样当一个或多个应用程序运行是再把程序代码和被调用的函数代码链接起来,从而节省了内存资源。从上面的说明可以看出,DLL 和.LIB文件必须随应用程序一起发行,否则应用程序将会产生错误。
微软的Visual C++支持三种DLL,它们分别是Non-MFC Dll(非MFC动态库)、Regular Dll(常规DLL)、Extension Dll(扩展DLL)。Non-MFC DLL指的是不用MFC的类库结构,直接用C语言写的DLL,其导出的函数是标准的C接口,能被非MFC或MFC编写的应用程序所调用。Regular DLL:和下述的Extension Dlls一样,是用MFC类库编写的,它的一个明显的特点是在源文件里有一个继承CWinApp的类(注意:此类DLL虽然从CWinApp派生,但没有消息循环),被导出的函数是C函数、C++类或者C++成员函数(注意不要把术语C++类与MFC的微软基础C++类相混淆),调用常规DLL的应用程序不必是MFC应用程序,只要是能调用类C函数的应用程序就可以,它们可以是在Visual C++、Dephi、Visual Basic、Borland C等编译环境下利用DLL开发应用程序。
常规DLL又可细分成静态链接到MFC和动态链接到MFC上的,这两种常规DLL的区别将在下面介绍。与常规DLL相比,使用扩展DLL用于导出增强MFC基础类的函数或子类,用这种类型的动态链接库,可以用来输出一个从MFC所继承下来的类。
扩展DLL是使用MFC的动态链接版本所创建的,并且它只被用MFC类库所编写的应用程序所调用。例如你已经创建了一个从MFC的CtoolBar类的派生类用于创建一个新的工具栏,为了导出这个类,你必须把它放到一个MFC扩展的DLL中。扩展DLL 和常规DLL不一样,它没有一个从CWinApp继承而来的类的对象,所以,开发人员必须在DLL中的DllMain函数添加初始化代码和结束代码。
三、动态链接库的创建
在Visual C++6.0开发环境下,打开File/New/Project选项,可以选择Win32 Dynamic-Link Library或MFC AppWizard[dll]来以不同的方式来创建Non-MFC Dll、Regular Dll、Extension Dll等不同种类的动态链接库。
1. Win32 Dynamic-Link Library方式创建Non-MFC DLL动态链接库
每一个DLL必须有一个入口点,这就象我们用C编写的应用程序一样,必须有一个WINMAIN函数一样。在Non-MFC DLL中DllMain是一个缺省的入口函数,你不需要编写自己的DLL入口函数,用这个缺省的入口函数就能使动态链接库被调用时得到正确的初始化。如果应用程序的DLL需要分配额外的内存或资源时,或者说需要对每个进程或线程初始化和清除操作时,需要在相应的DLL工程的.CPP文件中对 DllMain()函数按照下面的格式书写。
BOOL APIENTRY DllMain(HANDLE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)
{
switch( ul_reason_for_call )
{
case DLL_PROCESS_ATTACH:
.......
case DLL_THREAD_ATTACH:
.......
case DLL_THREAD_DETACH:
.......
case DLL_PROCESS_DETACH:
.......
}
return TRUE;
}
参数中,hMoudle是动态库被调用时所传递来的一个指向自己的句柄(实际上,它是指向_DGROUP段的一个选择符);ul_reason_for_call是一个说明动态库被调原因的标志,当进程或线程装入或卸载动态链接库的时候,操作系统调用入口函数,并说明动态链接库被调用的原因,它所有的可能值为:DLL_PROCESS_ATTACH: 进程被调用、DLL_THREAD_ATTACH: 线程被调用、DLL_PROCESS_DETACH: 进程被停止、DLL_THREAD_DETACH: 线程被停止;lpReserved为保留参数。到此为止,DLL的入口函数已经写了,剩下部分的实现也不难,你可以在DLL工程中加入你所想要输出的函数或变量了。
我们已经知道DLL是包含若干个函数的库文件,应用程序使用DLL中的函数之前,应该先导出这些函数,以便供给应用程序使用。要导出这些函数有两种方法,一是在定义函数时使用导出关键字_declspec(dllexport),另外一种方法是在创建DLL文件时使用模块定义文件.Def。需要读者注意的是在使用第一种方法的时候,不能使用DEF文件。下面通过两个例子来说明如何使用这两种方法创建DLL文件。
1)使用导出函数关键字_declspec(dllexport)创建MyDll.dll,该动态链接库中有两个函数,分别用来实现得到两个数的最大和最小数。在MyDll.h和MyDLL.cpp文件中分别输入如下原代码:
//MyDLL.h
extern "C" _declspec(dllexport) int Max(int a, int b);
extern "C" _declspec(dllexport) int Min(int a, int b);
//MyDll.cpp
#include
#include"MyDll.h"
int Max(int a, int b)
{
if(a>=b)return a;
else
return b;
}
int Min(int a, int b)
{
if(a>=b)return b;
else
return a;
}
该动态链接库编译成功后,打开MyDll工程中的debug目录,可以看到MyDll.dll、MyDll.lib两个文件。LIB文件中包含 DLL文件名和DLL文件中的函数名等,该LIB文件只是对应该DLL文件的"映像文件",与DLL文件中,LIB文件的长度要小的多,在进行隐式链接 DLL时要用到它。读者可能已经注意到在MyDll.h中有关键字"extern C",它可以使其他编程语言访问你编写的DLL中的函数。
2)用.def文件创建工程MyDll
为了用.def文件创建DLL,请先删除上个例子创建的工程中的MyDll.h文件,保留MyDll.cpp并在该文件头删除#include MyDll.h语句,同时往该工程中加入一个文本文件,命名为MyDll.def,再在该文件中加入如下代码:
LIBRARY MyDll
EXPORTS
Max
Min
其中LIBRARY语句说明该def文件是属于相应DLL的,EXPORTS语句下列出要导出的函数名称。我们可以在.def文件中的导出函数后加 @n,如Max@1,Min@2,表示要导出的函数顺序号,在进行显式连时可以用到它。该DLL编译成功后,打开工程中的Debug目录,同样也会看到 MyDll.dll和MyDll.lib文件。
2.MFC AppWizard[dll]方式生成常规/扩展DLL
在MFC AppWizard[dll]下生成DLL文件又有三种方式,在创建DLL是,要根据实际情况选择创建DLL的方式。一种是常规DLL静态链接到MFC,另一种是常规DLL动态链接到MFC。两者的区别是:前者使用的是MFC的静态链接库,生成的DLL文件长度大,一般不使用这种方式,后者使用MFC的动态链接库,生成的DLL文件长度小;动态链接到MFC的规则DLL所有输出的函数应该以如下语句开始:
AFX_MANAGE_STATE(AfxGetStaticModuleState( )) //此语句用来正确地切换MFC模块状态
最后一种是MFC扩展DLL,这种DLL特点是用来建立MFC的派生类,Dll只被用MFC类库所编写的应用程序所调用。前面我们已经介绍过,Extension DLLs 和Regular DLLs不一样,它没有一个从CWinApp继承而来的类的对象,编译器默认了一个DLL入口函数DLLMain()作为对DLL的初始化,你可以在此函数中实现初始化,代码如下:
BOOL WINAPI APIENTRY DLLMain(HINSTANCE hinstDll,DWORD reason ,LPVOID flmpload)
{
switch(reason)
{
……………//初始化代码;
}
return true;
}
参数hinstDll存放DLL的句柄,参数reason指明调用函数的原因,lpReserved是一个被系统所保留的参数。对于隐式链接是一个非零值,对于显式链接值是零。
在MFC下建立DLL文件,会自动生成def文件框架,其它与建立传统的Non-MFC DLL没有什么区别,只要在相应的头文件写入关键字_declspec(dllexport)函数类型和函数名等,或在生成的def文件中EXPORTS 下输入函数名就可以了。需要注意的是在向其它开发人员分发MFC扩展DLL 时,不要忘记提供描述DLL中类的头文件以及相应的.LIB文件和DLL本身,此后开发人员就能充分利用你开发的扩展DLL了。
四、动态链接库DLL的链接
应用程序使用DLL可以采用两种方式:一种是隐式链接,另一种是显式链接。在使用DLL之前首先要知道DLL中函数的结构信息。Visual C++6.0在VC/bin目录下提供了一个名为Dumpbin.exe的小程序,用它可以查看DLL文件中的函数结构。另外,Windows系统将遵循下面的搜索顺序来定位DLL: 1.包含EXE文件的目录,2.进程的当前工作目录, 3.Windows系统目录, 4.Windows目录,5.列在Path环境变量中的一系列目录。
1.隐式链接
隐式链接就是在程序开始执行时就将DLL文件加载到应用程序当中。实现隐式链接很容易,只要将导入函数关键字_declspec(dllimport)函数名等写到应用程序相应的头文件中就可以了。下面的例子通过隐式链接调用MyDll.dll库中的Min函数。首先生成一个项目为TestDll,在DllTest.h、 DllTest.cpp文件中分别输入如下代码:
//Dlltest.h
#pragma comment(lib,"MyDll.lib")
extern "C"_declspec(dllimport) int Max(int a,int b);
extern "C"_declspec(dllimport) int Min(int a,int b);
//TestDll.cpp
#include
#include"Dlltest.h"
void main()
{int a;
a=min(8,10)
printf("比较的结果为%d/n",a);
}
在创建DllTest.exe文件之前,要先将MyDll.dll和MyDll.lib拷贝到当前工程所在的目录下面,也可以拷贝到 windows的System目录下。如果DLL使用的是def文件,要删除TestDll.h文件中关键字extern "C"。TestDll.h文件中的关键字Progam commit是要Visual C+的编译器在link时,链接到MyDll.lib文件,当然,开发人员也可以不使用#pragma comment(lib,"MyDll.lib")语句,而直接在工程的Setting->Link页的Object/Moduls栏填入 MyDll.lib既可。
2.显式链接
显式链接是应用程序在执行过程中随时可以加载DLL文件,也可以随时卸载 DLL文件,这是隐式链接所无法作到的,所以显式链接具有更好的灵活性,对于解释性语言更为合适。不过实现显式链接要麻烦一些。在应用程序中用 LoadLibrary或MFC提供的AfxLoadLibrary显式的将自己所做的动态链接库调进来,动态链接库的文件名即是上述两个函数的参数,此后再用GetProcAddress()获取想要引入的函数。自此,你就可以象使用如同在应用程序自定义的函数一样来调用此引入函数了。在应用程序退出之前,应该用FreeLibrary或MFC提供的AfxFreeLibrary释放动态链接库。下面是通过显式链接调用DLL中的Max函数的例子。
#include
#include
void main(void)
{
typedef int(*pMax)(int a,int b);
typedef int(*pMin)(int a,int b);
HINSTANCE hDLL;
PMax Max
HDLL=LoadLibrary("MyDll.dll");//加载动态链接库MyDll.dll文件;
Max=(pMax)GetProcAddress(hDLL,"Max");
A=Max(5,8);
Printf("比较的结果为%d/n",a);
FreeLibrary(hDLL);//卸载MyDll.dll文件;
}
在上例中使用类型定义关键字typedef,定义指向和DLL中相同的函数原型指针,然后通过LoadLibray()将DLL加载到当前的应用程序中并返回当前DLL文件的句柄,然后通过GetProcAddress()函数获取导入到应用程序中的函数指针,函数调用完毕后,使用 FreeLibrary()卸载DLL文件。在编译程序之前,首先要将DLL文件拷贝到工程所在的目录或Windows系统目录下。
使用显式链接应用程序编译时不需要使用相应的Lib文件。另外,使用GetProcAddress()函数时,可以利用 MAKEINTRESOURCE()函数直接使用DLL中函数出现的顺序号,如将GetProcAddress(hDLL,"Min")改为 GetProcAddress(hDLL, MAKEINTRESOURCE(2))(函数Min()在DLL中的顺序号是2),这样调用DLL中的函数速度很快,但是要记住函数的使用序号,否则会发生错误。
本文通过通俗易懂的方式,全面介绍了动态链接库的概念、动态链接库的创建和动态链接库的链接,并给出个简单明了的例子,相信读者看了本文后,能够创建自己的动态链接库并应用到后续的软件开发当中去了,当然,读者要熟练操作DLL,还需要在大量的实践中不断摸索,希望本文能起到抛砖引玉的作用。
1、创建win32 dll工程
用VC++6开发win32 dll是非常方便的,它提供了简单通用的向导可以为我们生成一个程序框架,而我们只需在在框架里边加入自己的函数就可以了,现在就看一下如何生成win32 dll程序。
首先,通过VC++6的AppWizard,创建一个“Win32 Dynamic-Link Library”类型的工程MyDll。
然后,在创建类型选择框中,选择“An empty Dll project”创建一个空的dll工程。
2、添加代码
此时,我们就拥有了一个Win32dll的工程框架,但这个工程中什么都没有,下一步就是在工程中添加代码了。同样使用AppWizard创建一个新的cpp文件dllFile.cpp。当然添加文件有多种途径,但尽量使用程序向导,因为它是学习新方法的最好方法。在文件中加入如下代码:
#include <windows.h>
BOOL __stdcall DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
return TRUE;
}
Windows.h是编写windows程序最基本的头文件,它里面包含了常用的宏及函数的定义,所以至少得包含它。而DllMain函数是win32dll的缺省入口函数,每一个dll中都要有入口函数。通过入口函数其它程序在调用(LoadLibrary)和释放(FreeLibrary)dll时对dll初始化和释放空间。如果在调用此dll时需要分配额外空间,则在此函数中必须同时包含分配和释放空间的代码,函数中ul_reason_for_cal参数表示调用此函数的目的。可以在DllMain中加入switch表示:
switch(ul_reason_for_call)
{
case DLL_PROCESS_ATTACH: //被LoadLibrary调用
break;
case DLL_THREAD_ATTACH: //创建新线程
break;
case DLL_THREAD_DETACH: //线程退出
break;
case DLL_PROCESS_DETACH: //被FreeLibrary调用
break;
}
DllMain的一个参数hModule表示此dll的句柄,在程序内部也可以通过GetModuleFileName函数获得它的值。lpReserved参数是个保留参数。DllMain的返回值表示调用是否成功。至于关键字__stdcall,它是dll函数的一种定义约定,我们将在后面详细讲它。到此为止一个最简单的win32dll程序就写好了,如果你觉得至少应包含一个自定义的函数,那我们再加入如下函数,作用是显示提示对话框的函数:
void __stdcall ShowMessage(const LPCTSTR &lpszMsg)
{
MessageBox(NULL,lpszMsg,"MyDll",MB_OK);
}
我们定义ShowMessage函数为标准调用方式(__stdcall),在以后的dll函数的编写中,我们将都用标准调用方式。
接下来就需要导出ShowMessage函数了,只有导出的函数才能被其它程序调用,为了避免编译器修饰函数,我们使用.DEF文件导出函数,工程中添加一个DllFile.def的文本文件,加入如下代码:
LIBRARY "MyDll"
EXPORTS
ShowMessage
3.编译链接dll
在VC++中可以通过设置编译选项来生成两种程序版本,一个是debug版本,另一个是Release版本。其中debug版本中包含了程序的调试信息,生成的dll文件一般比较大,但它可以进行跟踪调试,查看代码的执行过程。虽然dll不能单独执行,但debug版本的dll在其它程序调用时同样可以跟踪其中的代码。而release版本中没有了附加的调试信息,文件变小,程序执行起来也会快些。至于快多少,这就看你的整个程序的代码的多少和复杂度了。如果是最终发布给用户的程序,那就最好使用了release版本。对于刚刚编写好的dll代码,最好编译成debug版本,这样有助于调试错误。vc++编译器默认设置的是debug版本。
你可以通过菜单project->setting设置每个版本的编译选项,通过这些选项,你能够控制编译过程,优化程序等等,通常最常用的设置就是让编译器直接把生成的程序放到一个特定的目录里,这样一方面省去在众多的生成文件中寻找的你的程序文件的麻烦,另一方面,如果你编写的几个程序之间有调用关系的话,那么建议在编译时就让它们生成到同一个目录中或已确定的目录中。下面我们把dll的输出目录设置为bin。
不过要注意的是,如果你debug和release版本的输出目录设置为同一个目录,那你就得注意别把这两种版本的生成文件混淆了。
这一切设置好了,现在你可以编译程序了,如果顺利,那么一个完整的dll程序“MyDll.dll”就生成了。
使用Visual C++ .NET创建dll
创建win32 dll工程
使用Visual C++ .NET创建dll的过程和Visual C++ 6.0差不多,只是一些命令选项的名称和位置发生了变化,比如要创建一个空win32 dll工程,就需选中win32项目类型中的Win32 项目:
在接下来的应用程序向导窗口中,选择“应用程序设置”页中的“DLL”,并选中附加选项中的“空项目”就可以了。
添加代码:
再以后的添加文件和代码的过程和前面的Visual C++6.0中的完全一样,完全可以按照前面的方法,编写代码。
至于输出目录的设置,你需要执行菜单项目->属性命令,在项目属性中设置输出目录:
编译链接
就像预料中的那样一切ok,接下来我们要通过一个例子讲解如何在应用程序中调用dll并调试执行dll的导出函数。
1、 在应用程序中调用dll
前面我们已经编写好了一个dll,接下来我们要看看如何在程序中调用此dll,其实调用方法前面都讲过了,现在如何通过导入函数调用dll函数。
首先,我们创建一个mfc的对话框应用程序,并通过资源编辑器在窗口上添加一个按钮,然后在程序文件中添加如下声明:
__declspec(dllimport) void __stdcall ShowMessage(const LPCTSTR &lpszMsg);
并在按钮的消息响应函数中使用ShowMessage函数:
ShowMessage(_T("success of calling dll!"));
注意不要忘记了,除了要把MyDll.dll文件复制到生成的应用程序目录下外,还要把Mydll的导入库文件复制到工程目录下,并把它加入到visaul c++的链接库中:
最后编译执行,点击窗口上刚刚添加的按钮,其效果可能就像下图:
当然,也可以不使用导入函数的方法,而直接使用显示链接的方法调用dll。那么你可以通过如下方法调用MyDll.dll,执行效果和通过导入函数的方法无异,并且不需要导入库的支持:
typedef void (__stdcall *dllfunc)(const LPCTSTR &lpszMsg) ;
HINSTANCE hDll = LoadLibrary(_T("mydll.dll"));
if (NULL != hDll)
{
dllfunc ShowMessage = (dllfunc)GetProcAddress(hDll,_T("ShowMessage"));
if (NULL != ShowMessage)
{
ShowMessage(_T("success of calling dll!"));
}
}
FreeLibrary(hDll);
1、 调试动态链接库
其实上面所讲的,都是假设编写的dll没有任何错误,但实际中,dll中的代码出现意外错误也是常有的事,毕竟它和应用程序一样都是由人编写的,尤其当编写的dll较大时,更需要有办法跟踪调试程序了。但是由于dll不能被单独执行,当我们直接按F5执行时会出现如下界面:
不能执行!在可执行应用程序中又无法对dll中的代码设断点,那我们如何调试呢?其实利用前面我们说的设置输出文件目录的方法,再结合设置可执行文件就可以实现在dll工程中调试dll的目的,而且是一劳永逸的。
1、 首先,设置dll工程和应用程序的输出目录设置到同一目录中。
2、 然后设置dll工程中的可执行文件路径和应用程序中路径一样,比如我们把dll和应用程序的可以执行文件都输出到工程外的“bin”目录中,就如下所示:
1、 最后,你在dll工程中按F5命令执行这个应用程序,这时你就可以像调试应用程序那样给你的dll代码设置断点并调试代码了。
我建议你测试和调试dll代码时,同时开两个工程,一个是dll的,用于设置断点,察看运行过程等等。另一个是调用此dll的应用程序,在这里随时编写测试代码,就这么简单。
原创 使用c++开发excel插件 (第3章动态链接库(dynamic-link library))收藏
1、为什么要讲动态链接库,它和excel插件有什么关系。
2、什么是动态链接库,它们都有哪几类。
动态链接库和普通exe应用程序有什么区别。
3、如何编写动态链接库。
3.1、如何声明函数
3.2、如何导出函数,导出类。
3.3、编写一个简单的动态链接库
4、如何调用动态链接库。
5、如何调试动态链接库。
6、查看动态链接库的内容。
1、 为什么我们要讲动态链接库
一句话就是excel插件就是一个动态链接库,它和普通动态链接库除了功能之外,在形式上几乎没什么大的区别,它们的主要区别是:
2 xll文件中必须包含几个excel调用要求的几个函数
2 必须导出excel调用的函数,而其它dll不是必须的
2 Excel插件扩展名最好为xll,其实扩展名是什么并没什么关系,写成xll也是为了好区别。
可以这么认为,excel插件就是一种特殊的动态链接库。
2、 什么是动态链接库
动态链接库在MSDN里面定义是一个文件,一个包含了一个或多个被编译了的函数的文件。其实动态链接库是一个封装了一些可以被其它应用程序共享的程序、数据以及资源的程序库。动态链接库的出现使大型系统更容易被划分为多个模块。动态链接是一种程序调用方法,通过动态链接我们可以使用公用的程序模块,甚至可以调用其它软件中的程序。它提供了一种开发可重用模块的有效方法。通常把一些可能被其它一个或多个应用程序调用的程序模块封装在一起,然后编译链接成库。动态链接库文件的扩展名一般是dll,也有可能是drv和sys等,当然也可以自定义其它扩展名,只要文件的格式在正确即可。在dll中每个函数都含有完整的可执行代码,但它们不可以单独执行,只能在其它应用程序调用时执行。因为它缺少自己的堆栈空间。也正因此,一个dll可以被多个应用程序同时使用,应用程序在调用它时是把它映射到自己的进程空间,并使用应用程序的自己的堆栈空间。
那既然有动态链接,那是否有静态链接呢,回答是肯定的。动态链接就是相对于静态连接而言的。所谓静态链接是在编译链接时把要调用的程序代码的副本复制到调用程序中,成为调用程序自身的一部分,而在执行时应用程序也不再需要静态链接库了。所以当一个静态库被多个程序使用时,每个程序内部都会包含一个相应函数的副本,导致应用程序文件较大,在执行时也会占用更多内存。而动态链接库并没有把函数代码的副本复制到应用程序的内部,仅仅是保存了函数所在的地址,不会占用多少空间。在执行时应用程序也只是把动态库映射到自己的进程空间,并不会在内存中生成多个副本。那么同一个DLL副本在内存中被多个程序调用会不会像会影响呢?对于普通的开发人员来说大可不必担心,win32系统会为我们处理这个过程。
通常情况下,当应用程序调用dll时,win32系统首先把dll调入到系统的全局堆栈中,然后通过内存映射文件,让每个调用进程都有该dll的一份影像。
动态链接库与静态链接库的比较:
动态链接库
静态链接库
可否单独执行
否
否
调用程序以何种方式调用其中函数
通过使用函数的地址
复制副本到调用程序
被多个程序调用时,在内存中的存在方式
单个副本
多个副本
应用程序链接过程
在调用时才链接
在生成应用程序时链接
通常文件扩展名
Dll、sys等
LIB
是否可以被其它语言调用
可以
否
Windows本身就含有大量的dll,下表将说明 Win32 API 中几个常用的 DLL。
DLL
内容说明
GDI32.dll
用于设备输出的图形设备接口 (GDI) 函数,例如用于绘图和字体管理的函数。
Kernel32.dll
用于内存管理和资源处理的低级别操作系统函数。
User32.dll
用于消息处理、计时器、菜单和通信的 Windows 管理函数。
综上所诉使用动态链接库较静态链接库有许多优点。使用dll不仅可以节省内存,减少交换操作,也节省磁盘空间,试想下如果在win32系统中每个应用程序都包含一个实现windows的基本功能的副本那将是什么样的情景。使用dll可以降低维护以及升级成本。总结起来它有如下一些优点:
2 节省内存:dll在内存中只有一个副本,而使用静态链接库的应用程序在内存中都会加载一个代码副本。
2 节省磁盘空间:对于使用dll的程序,它自身内部并不保存dll的副本,在磁盘上只要有一个副本就可以了,但是对于使用静态库的程序,每个程序内部都有一个代码副本,那如果你的程序中把gdi32.dll、user32.dll等基本的windows功能实现的代码都包含进程序内部,那你的单个程序就可能大到几百兆。下图显示了一个简单应用程序testapp.exe所调用的dll,正是有这些dll的支持,几K字节的程序就能够拥有丰富的功能。
2 可以降低升级维护的成本。在对软件系统进行升级维护时,只要不更改调用接口,修改dll中的代码不会对调用它程序产生任何影响。而静态链接库则没有如此方便,只要它自身代码被改变,那所有调用它的程序都需要重新编译链接。
2 Dll可以被多种语言调用:无论是何种语言编写的应用程序,只要它遵循dll的函数的调用约定,就可以调用此dll。关于dll的调用约定在后面章节中将详细介绍。
2 可以扩展MFC库的功能:通过创建MFC扩展dll,可以从现有 MFC 类派生类,创建具有扩展功能的MFC动态库,以供MFC应用程序使用。
2 创建纯资源dll:我们可以创建具有某种国家语言的资源dll,通过调用不同的资源dll就可以轻松实现具有多国语言的应用程序。
动态链接库也是程序,它与可执行应用程序除了在使用时能否单独执行的区别外,它们内部还是有有些本质的区别,下面的表中列出了动态链接库于普通应用程序的一些区别:
动态链接库
应用程序
可否单独执行
否
可以
是否可以存在多个实例
否,只能有一个实例
可以
是否拥有堆栈
否
有
是否包含导出表
有
没有
可否被其它程序调用
可以
自身可以被调用,但其中的函数不可以。
通常文件扩展名
Dll、sys等
Exe
通过上面的介绍我们知道的了动态链接库是什么,也了解了它和静态库以及普通应用的程序的区别。那我们现在想知道,是不是所有的dll都一样呢。可以这么理解,从文件格式来看它们都是一样的,因为都遵循win32的定义标准,但是,从用途以及生成方式来看它又可以分成四种类型: win32 dll,com dll,mfc规则动态链接库以及MFC扩展动态链接库。下面我们将简单了解一下这几种dll。
2.1、Win32 dll也就是符合32位windows系统环境的动态链接库,是最普通的动态链接库,它可以由不同语言编写,比如VB,C#等都可以编写win32 dll。大多数语言编写的dll都可以相互调用。通常把一些通用的又经常被其他程序调用的模块封装成win32 dll,就大大方便的程序的调用和代码的重用。本书主要讲的xll插件就使用此类,我们将在下一节讲解如何创建win32 dll。
2.2、com dll是一类按照com(Component Object Model,组件对象模型)规范编写的dll,由于它的语言无关性,使它的用途广泛。使用com不仅可以编写excel插件,而且可以为所有支持com规范的程序编写插件。但它编写起来比较复杂,在本书中暂不作讲解。
2.3、MFC规则的动态链接库,是在内部使用了MFC dll并按照MFC编写规则编写的dll。在这类DLL 中,必须在所有导出函数的开始处添加 AFX_MANAGE_STATE 宏,以将当前模块的状态设置为 DLL 的状态。为此,需将下列代码行添加到从 DLL 导出的函数的开始处:
AFX_MANAGE_STATE(AfxGetStaticModuleState( ));
就像下面的代码:
extern "C" _declspec(dllexport) LPCTSTR __stdcall GetString()
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
return(NULL);
}
动态链接到 MFC 的规则 DLL 的主要特点:
2 在编译选项中需要定义_AFXDLL和_USRDLL。
2 在所有导出函数的开始处添加 AFX_MANAGE_STATE 宏
2 这类 DLL 必须实例化 CWinApp 派生类。
2 此类型的 DLL的入口函数使用 MFC 提供的 DllMain,其他与在标准 MFC 应用程序中一样。
2 应用程序的主消息泵必须调用从 DLL 导出的例程来调用CWinApp::PreTranslateMessage
2 与在标准 MFC 应用程序中一样,将DLL的初始化操作放到 CWinApp::InitInstance 成员函数中。卸载 DLL 前,将从 MFC 提供的 DllMain 函数调用 CWinApp 派生类的 CWinApp::ExitInstance 成员函数。
2 发布时,必须同时包含共享的MFC dll。
2 函数通常是通过标准 C 接口从规则 DLL 导出。
2 规则 DLL 内的所有内存分配都应在该 DLL 内进行;DLL 不应向调用可执行文件传递或从调用可执行文件接收指向MFC对象的指针和由MFC分配的指向内存的指针,否则需要创建另外一种dll,即扩展MFC DLL。
2.4、MFC 扩展 DLL 是通常实现从现有 Microsoft 基础类库类派生的可重用类的 DLL。是对现有的MFC 类库的扩展,那么使用此dll的程序必须也是MFC规则的应用程序。比如比较流行MFC扩展库有BCG库。由于它和本书所要介绍的内容,无太大关系,创建excel插件也不会使用此方法,所以此处也不作进一步介绍了。
现在我们知道了几种常见的dll,也对它们各自的创建方法有了一定的了解,那么我们在实际使用中如何确定要使用哪种dll更合适呢。下面列出了几种创建dll的原则供参考:
2 尽量不使MFC规则的dll,如果你的程序中不需要调用MFC中类,就创建普通的win32 dll,否则在发布你的DLL是不得不包含庞大的MFC类库。另外在你的程序中尽量使用STL,可以降低使用MFC的几率。
2 如果确实需要MFC dll的支持,则尽量使用动态链接到MFC规则DLL,这样一方面可以减小生成的DLL的文件大小,也可以减少调用时的内存占用,而且在编译时也比静态链接MFC库要快。
2 如果此dll只可能用于MFC规则的应用程序中,那么可以考虑使用MFC规则DLL或MFC扩展DLL,但是如果 DLL 实现从现有 MFC 类派生的可重用类,或如果需要在应用程序和 DLL 之间传递 MFC 派生的对象,则必须生成扩展 DLL。
现在我们已对常见的几种动态链接库有了深入的了解,下一步就看看如何编写动态链接库。
3、如何编写动态链接库
这一节中我们将介绍如何编写win32 dll,在正式编写程序之前我们需要先了解一些关于在dll编写函数或类的知识。
3.1调用约定
所谓调用约定是指在告诉编译器函数将以何种方式被调用,在函数声明过程中,通过使用调用约定关键字显示声明函数的调用方式,调用方式的不同将会影响到函数在被调用过程中的执行方式,主要是指:
2 函数调用时参数传递到堆栈上的顺序
2 是由谁来清除调用后的堆栈中的参数。
2 函数的名称修饰约定。
最常见的有三种调用方式,它们分别是__cdecl,__stdcall和__fastcall,其中__cdecl是vc++默认的调用约定,在默认情况下,如果函数没有被显示声明为其它调用方式都会被使用c调用方式__cdecl。但是建议不要使用__cdecl调用约定,因为在这种调用约定下,对传递参数的堆栈的清除工作是由调用者完成的,这对于调用不同语言编写的dll 时是个很困惑的事情,除非函数中的参数数量不能确定,就像C中的fprint函数一样。尽量使用__stdcall约定,这种约定下函数的参数堆栈由被调用者自己处理,就避免了以后调用函数时还得处理堆栈的麻烦。而且以__stdcall约定修饰的函数,符合大多数程序的调用方式,使用范围也较广,系统中许多函数都是使用的__stdcall,下面是windef.h中的一段定义,是否觉得很面熟呢:
#define CALLBACK __stdcall
#define WINAPI __stdcall
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
__fastcall调用约定和__stdcall的调用约定差不多,但它使用寄存器传递参数,使调用过程更快些。
3.2修饰名:
修饰名是在编译函数定义或函数原型期间由编译器创建的字符串。C和C++程序中的函数在内部通过其修饰名加以识别。对于不同的调用约定,编译器产生的函数的修饰名也不同。对于C函数,__cdecl命名约定使用以下划线(__)开头的函数名,不执行任何大小写转换。而对于__stdcall调用方式时名称修饰用下划线(_)作为符号名的前缀,并在符号的后面追加一个@符号,@符号后是参数列表中的字节数—即所需的堆栈空间。例如声明如下一个函数:
Int _stdcall func (char *pStr)
对于__stdcall方式其修饰名为_func@4
对于__cdecl方式其修饰名为_func。
注意:在win32系统中所有的指针都是4字节。
通常情况下,我们并不需要知道函数的修饰名,编译器通常可以识别未加修饰的名称。但也有例外,比如下面两种情况就需要指定修饰形式的名称:
2 对于重载函数和特殊成员函数(比如构造函数和析构函数),必须指定修饰名
2 当引用C或C++函数名的程序集源文件中也必须使用修饰名。
C函数的修饰形式取决于其声明中使用的调用约定,如下所示:
以Int func (char *pStr)为例
调用约定
修饰方式
例
__cdecl
前导下划线(_)
_func
__stdcall
前导下划线(_)和结尾@符号,后面是表示参数列表中的字节数
_func@4
__fastcall
与__stdcall相同,但前置符不是下划线而是@符号
@func@4
C++函数的修饰名包含下列信息:
2 函数名
2 函数所属的类(如果函数是成员函数)。这可能包括封装函数的类的类,等等。
2 函数所属的名名空间(如果函数是某个命名空间的组成部分)。
2 函数的参数类型。
2 调用约定。
2 函数的返回类型
函数名和类名在修饰名中以代码形式存在。修饰名的其余部分仅对编译器和链接器具有内部意义的代码。下面是未修饰的和修饰的C++名称的事例。
未修饰名
修饰名
int __stdcall func(char *pStr)
?func@@YGHPAD@Z
int CTest::func(char *pStr)
?func@CTest@@QAEHPAD@Z
那么我们如何知道一个函数编译后的修饰名是什么样的,这就可以借助DUMPBIN工具,它是个命令行工具,你可以在命令行方式下键入dumpbin /symbols <obj或lib文件名>,下图显示了一个ShowMessage函数的原型和修饰名:
下面我们来看一下有关三种调用约定及其对修饰名的影响的比较:
__cdecal
__stdcall
__fastcall
参数传递顺序
从右向左压入堆栈
从右向左压入堆栈
前两个字(DWOR)或更小的参数被放入ECX和EDX寄存器中,其它参数以从右向左的顺序压入堆栈
是否支持可变参数
支持
不支持
不支持
谁来清除堆栈中的参数
由调用函数移除堆栈中参数
由被调用函数在调用结束时移除堆栈中的参数
由被调用函数在调用结束时移除堆栈中的参数
名称修饰
对于C函数:
前导下划线 (_)
对于C函数:
前导下划线(_)和结尾@符号,后面是表示参数列表中的字节数
对于C函数:
与__stdcall相同,但前置符不是下划线而是@符号
对应的编译器参数
/Gz
/Gd
/Gr
3.3、导出函数
DLL和可执行程序最大的区别就是dll文件中包含到处函数表,表中列出了所有可被其它程序调用的函数,只有通过这些函数才能执行dll中代码,而且只有导出表中的函数才能被其它程序调用。同时为了让其它应用程序(比如用vb,pascal等语言写的应用程序)可以调用C/C++DLL中的函数,必须让编译器导出正确的函数,而不做任何名称修饰。现在我们来看一下如何导出dll中的函数,有两种方法可以实现我们的目的:
2 在生成dll时,创建一个模块定义(.def)文件并使用此.DEF文件。
2 在函数的定义中使用__declspec(dllexport)关键字。
不过值得注意的是在使用上面两种方法时,确保使用__stdcall调用约定,因为在其它语言的程序中使用堆栈的方法也许和C++的不同。下面我们来分别看一下如何使用这两种方法。
模块定义(.DEF)文件是一种用来定义dll属性的文本文件,它通过模块定义脚本对dll的属性进行描述,其中也包括对导出函数的定义。如果你不使用__declspec(dllexport)关键字的话就必须使用模块定义(.DEF)文件导出dll函数。下面我们将看到一个简单的.DEF文件:
; ExcelAddin.def : Declares the module parameters for the DLL.
LIBRARY "ExcelAddin"
DESCRIPTION 'ExcelAddin Windows Dynamic Link Library'
EXPORTS
; Explicit exports can go here
xlAutoOpen
xlAutoClose
xlAddInManagerInfo
MyMotd
MyFunc
上面文本描述了动态链接库ExcelAddin.dll中的导出函数列表。其中,LIBRARY语句指定了此文件是对ExcelAddin动态库的描述,DESCRIPTION指明了此dll的描述信息,EXPORTS下面列出了所有要导出的函数名。注意模块定义文件中的注释必须以“;”开头,可以有多行,但不能和定义语句在同一行。
如果使用.DEF文件定义输出函数信息则文件中必须至少包含下面基本的模块定义语句:
2 文件中的第一个语句必须是LIBRARY语句。通LIBRARY语句将此.DEF文件标识为所属DLL。所以LIBRARY语句的后面紧跟的是DLL的名称。链接器会将此名称放到DLL的导入库中。
2 EXPORTS语句列出导出函数的名称,如果是系统自动生成的,有时还会列出DLL导出函数的序号值。当然也可以手动在函数名的后面加上@符号和一个数字,给函数分配序号值。当指定序号值时,序号值的范围必须是从1到N,其中N是DLL导出函数的个数。
有关.DEF文件中的模块定义语法在MSDN里有详细的描述,这里就不做解释了。如果你创建的是MFC规则的dll则向导会首先替你创建一个.DEF框架,并添加到项目中。你只需再此文件中添加函数名既可。对于普通win32 dll就需要手动创建.DEF文件并把它添加到项目中。
另一种方法是使用__declspec(dllexport)关键字,通过设定关键字同样可以从dll中导出数据、函数、类、或类成员函数。如果仅仅是简单的导出函数的话就可以不使用.DEF文件。但它并不能完全替代.DEF 文件,因为有许多导出指令(如序号、NONAME等)只能在.DEF文件中使用。在通常情况下使用__declspec(dllexport)关键字较便利,尤其是当你要导出C++修饰符时,你不必了解复杂的修饰方法。所以如果你对导出函数没有具体要求就最好使用__declspec(dllexport)关键字。
要导出函数,__declspec(dllexport)关键字必须出现在调用约定关键字的左边(如果指定了关键)。例如:
__declspec(dllexport) void __stdcall func(void);
若要导出类中所有公共数据成员和成员函数,关键字必须出现在类名的左边,例如:
Class __declspec(dllexport) CXlTest
{
……类的内容
};
为了使代码书写起来更方便,也为了提高代码的可读性,我们可以用一个宏来代替__declspec(dllexport),例如:
#define DllExport __declspec(dllexport)
如果你想让C++编写的DLL可以在C语言程序中使用,则应该让编译器以C链接而不是C++链接来声明这些函数。通常情况下C++编译器使用C++调用约定和C++名称修饰方式。要指定C链接,只需为函数声明指定extern“C”。例如:
Extern “C” __declspec(dllexport) int func(void);
那么反过来,如果是用C编写的DLL如何用在C++语言中呢。此时我们可以使用__cplusplus预处理器宏确定正在编译的语言,然后如果是从C++语言模块使用,则用C链接声明这些函数。如果在为DLL提供的头文件中使用此方法,则这些函数可以原封不同地由C和C++用户使用。下面代码显示可由C和C++客户端应用程序使用的头文件:
#ifdef __cplusplus
extern "C" {
#endif
__declspec( dllimport ) void func(void);
__declspec( dllimport ) void GetString(char *pChar);
#ifdef __cplusplus
}
#endif
如果我们现在所使用的C头文件并没有使用上面的方法怎么办呢,是不是需要重新编译那些代码呢,通过另一种方法同样可以达到上面的效果,例如:
extern "C" {
#include "ExcelAddin.h"
}
我们已经知道了两种导出方法,那么我们在实际使用中使用哪种导出方法较合适呢。我们现在来看一下两种方法的优缺点:
使用.DEF文件的优缺点:
通过.DEF文件可以控制导出函数的序号,当你把新添加的函数放置到.DEF文件中时如果分配一个更高的序号值,就可以保证现有的应用程序继续正常使用新的DLL。另外值得一提的是MFC DLL中的函数也是用.DEF文件导出的。另外在.DEF文件还可以使用NONAME属性导出函数,该属性仅将序号放到DLL的导出表中。对具有大量导出函数的DLL,使用NONAME属性可以减小DLL文件的大小。
但使用.DEF文件的主要缺点是:.DEF维护起来比较麻烦,如果需要把修饰名放到.DEF文件中,还需要通过其他方法(可以使用Dumpbin工具)先获得修饰名。而且修饰名随编译器的版本不同而不同,这就需要链接此DLL的应用程序也得使用相同版本的编译器。
使用__declspec(dllexport)的优缺点:
使用__declspec(dllexport)非常方便,因为不需要考虑维护.DEF文件和获取导出函数的修饰名。但是,由于无法控制编译器生成的导出序号,当用新导出重新生成DLL,有时还需要重新生成应用程序。
导入函数
如果你的程序里要调用dll中的公共符号,那你需要导入这些公共符号。使用__declspec(dllimport)可以导入任何由.DEF文件或由__declspec(dllexport)关键字导出的变量、函数、类等。使用时只需在符号定义前加上次关键字,通常为了提高代码的可读性也为导入关键字定义一个宏:
#define DllImport __declspec( dllimport )
DllImport int iNum;
DllImport void func(void);
但是在上面的声明中如果不使用--__declspec(dllimport)同样可以使用。那么程序如何知道func函数是在dll中,又是如何执行的呢。其实如果不使用导入关键字声明的话,编译器是通过生成形实转换程序(thunk)来实现的。这样带来直接后果是代码生成的代码变大,而其降低了缓存性能,而使用__declspec(dllimport),编译器会生成间接调用,从编译器生成的代码来看使用关键字不仅生成的代码较少,而且调用效率也比较高,所以在程序中导入dll中的符号时一定要使用__declspec(dllimport)关键字。但注意由于它生成的是间接调用代码,所以导入关键字不要用在dll内部函数上。通常为了使dll和客户端应用程序可以使用相同的头文件,我们可以使用预处理宏来指示是生成dll还是应用程序。例如
#ifdef _EXPORTING
#define CLASS_DECLSPEC __declspec(dllexport)
#else
#define CLASS_DECLSPEC __declspec(dllimport)
#endif
class CLASS_DECLSPEC CTest
{
};
链接dll
前面我们所讲的导入只是在给出了如何在应用程序中使用dll中的函数、变量等。那么应用程序中具体是如何与dll建立链接的呢。应用程序有两种方法链接到dll的方法,这两种方法一种是隐式链接,另一种是显示链接:
隐式链接是指在程序中没有明确的使用加载动态链接库的方法(LoadLibrary),那么显示链接就是明确的使用了链接方法。
隐式链接
在隐式链接下,是由编译器在生成程序时链接到dll的导入库文件(.LIB),所以要使用隐式链接就必须有对应dll的导入库文件。当然在程序执行时还必须要dll文件。所以如果你要使用隐式连接那么以下几项是必不可少的:
2 包含导出函数和C++ 类的声明的头文件(.H 文件)。
2 要链接的导入库(.LIB files)。(生成 DLL 时链接器创建导入库。)
2 实际的 DLL(.DLL 文件)。
在编写应用程序时,包含dll的导出函数的定义文件。如果你是dll的提供者,你可以定义一个或几个包含导出函数或类的头文件,也可以把dll中的头文件直接提供给用户,文件中的函数定义方法就是我们在上一节导入中所讲的那样。有了头文件使用者才能知道哪些函数是可以使用的。但是一旦包含了这些头文件,那么对这些头文件的中的函数或类的调用就和本地调用没有任何区别了。
在生成应用程序时,应用程序必须能够链接到dll的导入库文件,因为导入库文件中才真正包含函数或类的描述信息。通常我们可以在编译器中加入我们要链接的库名即可以了。
对于vc++6,你可以在project->setting->lib属性页中设置。虽然链接了导入库,但应用程序中仍不包含具体的程序代码,所以在执行时还需要让程序能够定位 .DLL文件。
显式链接
相比隐式链接,显示连接更灵活一些。在显式链接下,应用程序并不需要在程序生成时链接dll的导入库文件,也不需要dll的导入函数文件(.h),在应用程序中通过应用程序需要通过dll加载函数(LoadLibrary)来加载dll文件,并通过函数指针调用dll的导出函数。具体我们需要通过以下方法显示调用dll。
2 使用LoadLibrary函数加载dll文件并获取dll的模块句柄。
2 使用GetProcAddress获取dll中的导出函数的指针,在应用程序中通过函数指针调用dll中的函数。
2 最后通过 FreeLibrary函数释放此次调用。
下面演示了在应用程序中调用test.dll中的导出函数“func”的方法,调用其它函数的方法与此都类似:
typedef int (__stdcall *dllfunc)(char*) ; //定义函数指针
HINSTANCE hDll = LoadLibrary(_T("test.dll")); //获得dll的句柄
//如果成功载入test.dll则继续获得函数的地址
if (NULL != hDll)
{
dllfunc func = (dllfunc)GetProcAddress(hDll,_T("func"));//获得函数func的地址
//如果地址获得到了,则执行
if (NULL != func)
{
int iResult = func(_T("success of calling dll!"));//调用dll中func函数
}
}
FreeLibrary(hDll);
确定要使用的链接方法
同样象前面几节那样,当我们了解了这些链接方法后,需要知道在何时使用何种方法才是最合适的。下面我们来看一下这两种链接方法的优缺点:
隐式链接
如果应用程序使用隐式链接,那么应用程序在被编译时,dll函数调用在对象代码中生成一个外部函数引用,此时应用程序与此dll的导入库(.LIB)文件进行链接,并加载调用dll中函数的代码。这些代码包含完整的对函数的调用信息,但不包含函数的执行代码。当应用程序启动时,程序会根据这些信息查找并调用函数。应用程序在启动时必须能够定位要调用的dll文件,如果没有找到则应用程序会提示错误终止执行进程,否则系统将dll模块映射到进程的地址空间中。当然仅仅能够定位到dll也并不代表dll能加载成功,所有DLL都具有入口点函数,通常情况下就是我们看到的winmain,main函数,也可以指定其它函数,这些函数负责着对dll的初始化工作,在dll被加载时首先执行入口点函数,如果入口函数调用不成功,即返回FALSE,同样也会导致dll加载失败。只有dll的初始化成功,此dll才可以被使用。最后在调用dll函数执行时,系统修改进程的可执行代码以提供dll函数的起始地址。
显式链接
大部分应用程序都使用隐式链接,因为这种方法使用起来方便。但有时候也需要使用显示链接,因为显示链接也有一些隐式链接无法替代的优点。下面我们来看一些使用显式链接的常见原因:
2 在应用程序启动时无法确定dll的名称,比如我们常见到的资源dll,应用程序启动时需要根据配置文件中信息才能知道要载入哪种资源的dll。
2 使用隐式链接的程序如果再启动时没有找到dll,则进程就被立刻终止。但是对于显示链接的程序只在显示加载dll时才查找dll文件,而且可以通过编写处理加载失败的代码以避免进程被迫终止。
2 如果应用程序使用的dll较多,则如果在启动时加载所有的dll就会导致程序启动较慢,而显示链接只在需要时才加载,而且是需要哪个就加载哪个,就不会出现启动过慢得情况。当然你可以让程序只隐式链接那些在启动时就需要的dll。
2 显示链接的一个重要优点是,由于dll不需要与导入库(.LIB)文件链接,当dll被修改后,只要应用程序中调用的dll函数的定义没变,应用程序就不需要重新链接。而隐式链接需要重新链接导入库。
使用显示链接时一定要注意,在使用完dll时一定要通过FreeLibrary函数释放dll。
关于LoadLibrary 和 AfxLoadLibrary
对于显示链接dll的程序必须使用显示载入函数载入dll文件,windows系统提供了两个API函数来实现此功能,一个是LoadLibrary,通常使用这个函数就可以,对于MFC扩展dll的加载,必须使用AfxLoadLibrary,而不是LoadLibrary。这两个函数通过在指定和默认的的系统路径下搜索指定的dll文件。如果找到则将dll映射到调用进程的地址空间,并返回此dll的模块句柄。否则返回空(NULL),但是也许你会问,如果此dll已经被映射到进程的地址空间中,那调用此函数会不会错误呢,由于dll是通过引用数来控制多次应用的,所以多次对同一dll引用不会出错也不会载入多个dll副本。另外要注意的一点是,这两个函数都是通过一定顺序在系统中寻找dll文件的,所以在调用dll时一定要清楚此时调用的是哪个路径下的dll,否则可能会出现一些让你困惑的问题。下面列出了windows搜索dll的路径顺序:
2 当前进程的可执行模块所在的目录。
2 当前目录。
2 Windows 系统目录。GetSystemDirectory 函数检索此目录的路径。
2 Windows 目录。GetWindowsDirectory 函数检索此目录的路径。
2 PATH 环境变量中列出的目录。
注意 未使用 LIBPATH 环境变量。
LoadLibrary和AfxLoadLibrary的使用方法一样,它们的参数中接收dll的文件名,在程序中使用时可以像如下代码:
HINSTANCE hDll = LoadLibrary(_T("test.dll"));
if (NULL != hDll)
{
//使用dll函数
}
也可以直接指定dll路径,比如:
HINSTANCE hDll = LoadLibrary(_T("d://Mydll/test.dll"));
if (NULL != hDll)
{
……
//使用dll函数
}
关于FreeLibrary 和 AfxFreeLibrary
在显示调用dll的应用程序中,当不再需要 DLL 模块时,调用FreeLibrary来递减模块的引用数,直到引用数为零,此函数便从进程的地址空间中取消模块的映射。对于MFC扩展dll的卸载是通过AfxFreeLibrary来实现,其它的由FreeLibrary处理。
FreeLibrary和AfxFreeLibrary的用法也相同,它们的参数是dll的句柄,例如
FreeLibrary(hDll);
通常也可以使用GetModuleHandle来获得dll模块句柄。
关于GetProcAddress
GetProcAddress是用来获得显式链接得dll的函数地址,它有两个参数,一个是dll模块句柄,另一个是函数的修饰名,注意不一定和函数名相同。通过此函数返回函数的地址,我们就可以使用函数指针调用函数了,但由于没有经过编译时得类型检查,所以要确保函数至指针的参数要与函数原型保持一致以避免调用错误。通常我们根据导出函数的原型定义一个typedef,然后再定义函数指针。例如:
typedef int (__stdcall *dllfunc)(char*) ;
typedef int(__stdcall *dllfnTest)(void);
……
dllfunc func = (dllfunc)GetProcAddress(hDll,_T("func"));//函数原型是func
dllfnTest fnTest = (dllfnTest) GetProcAddress(hDll,_T("_fnTest@0"));//函数原型是fnTest
……
怎样用C++编写标准动态链接库(DLL)
阅读(119) 评论(0) 发表时间:2008年08月02日 10:09
本文地址:http://qzone.qq.com/blog/19460476-1217642979
由于公司项目需要,需要编写一个标准动态DLL在ASP.NET下调用,苦心钻研两天后,已出成果,现将实现步骤及注意细节纪录下来,以备查阅。
第一步:创建Win32 Dynamic-Link Library 工程
第二步:C/C++文件
第三步:写代码
extern "C" __declspec(dllexport)
int __stdcall functionName(int a,intb); //方法接收两个参数
int APIENTRY functionName(int a,int b) // APIENTRY 此关键字不可少
{
return (a+b);
}
这样就创建了简单的标准动态链接库(DLL)文件。
在C#中的调用
1、WinForm中的调用
在WinForm调用很简单,将DLL文件拷到项目的Bin文件夹下,不需要在程序里添加引用,因为此DLL不是COM组件,添加引用时也会报错。
引用using System.Runtime.InteropServices;命名空间
[DllImport("DllName.dll", CharSet=CharSet.Ansi)]
static extern int functionName(int a,int b);//声明外部的标准动态库, 跟Win32API是一样的
调用:int intValue = functionName(1,2);
2、在Web中的调用
如果把此dll文件拷到system32文件夹后,调用方法和WinForm是一样的。
但一般情况下,我们部署网站时DLL文件都在Bin文件夹下,不习惯把某一个DLL在手动拷到System32文件夹下,这时的解决办法如下。
首先写个类,此类是将C++里写的函数转换为委托
public class DllInvoke
{
[DllImport("kernel32.dll")]
private extern static IntPtr LoadLibraryA(String path);
[DllImport("kernel32.dll")]
private extern static IntPtr GetProcAddress(IntPtr lib, String funcName);
[DllImport("kernel32.dll")]
private extern static bool FreeLibrary(IntPtr lib);
private IntPtr hLib;
public DllInvoke(String DLLPath)
{
hLib = LoadLibraryA(DLLPath);
}
~DllInvoke()
{
FreeLibrary(hLib);
}
//将要执行的函数转换为委托
public Delegate Invoke(String APIName, Type t)
{
IntPtr api = GetProcAddress(hLib, APIName);
string s = api.ToString("X");
return (Delegate)Marshal.GetDelegateForFunctionPointer(api, t);
}
}
然后在调用的页面添加using System.Runtime.InteropServices;命名空间;
声明委托public delegate string functionName();//
调用:
DllInvoke dll = new DllInvoke(@"~/Bin/DllName.dll");
functionName compile=(functionName)dll.Invoke("functionName",typeof(functionName));
Response.Write(compile(1,2).ToString());
说明:红字的functionName这个是C++里的方法名,C++在编译成DLL的时候方法名会被改写。
查看改写后方法名的方法:在.NET命令行下输入:dumpbin /exports c:/DllName.dll
或在VS6.0的工具里运行Depends,打开要查看的DLL即可。
那怎样让C++不改动我的方法名呢?
只要添加def(项目名.def)文件,在此文件中输入如下代码:
LIBRARY 你的项目名称
EXPORTS 你的方法名
如果有多个方法跟在后面写就可以了。
用 vc 6.0 下的cl.exe 和 link.exe工具,请读下文:
声明:下面这篇文章不是我写的,源自:一个叫,有容乃大 的博客
如何手工编写动态链接库(windows dll)
1.本例介绍在命令行(Console)环境下制作dll的方法
2.读者动手前,请确保在windows中安装有编译、链接工具和必要的函数库文件。
3.本例使用C语言实现.
4.本例中使用路径均为我机器上的绝对路径,读者需根据实际情况调整。
工具要求:
Microsoft的编译器cl.exe
MIcrosoft链接器link.exe
dll制作步骤:
1.编写dll函数实现源代码hello.c
#include
int say_hello(char* name)
{
printf( "hello %s/n ", name);
return 1;
}
2.编写dll函数输出定义文件hello.def.
LIBRARY hello
EXPORTS
say_hello @1
3.编译dll源码,生成dll,lib文件.
3.1 新建命令行窗口
3.2 设置PATH | INCLUDE | LIB 3个环境变量.
SET PATH=K:/vcnet/vc7/bin;%PATH%
SET INCLUDE=K:/vcnet/vc7/include;%INCLUDE%
SET LIB=K:/vsnet/Vc7/lib;%LIB%
3.3 编译hello.c
cd K:/Source/dllsample (hello.c和hello.def所在目录)
cl /c hello.c
3.4 链接hello.obj,生成hello.dll,hello.lib两个文件.
link /def:hello.def /dll hello.obj
4.测试dll函数.
4.1 编写测试代码 test.c
extern int say_hello(char* name);
int main(int argc,char** argv)
{
say_hello( "robbie ");
return 0;
}
4.2 编译测试代码test.c
cl /c test.c
4.3 链接test.obj和 hello.lib,生成可执行文件test.exe
link test.obj hello.lib
4.4 运行test.exe,屏幕输出:
hello robbie
至此,一个dll构造完毕.
下面是我自己的一点补充:
如果要在c++下,或者win32 mfc下使用标准c写的dll,必须把上面的声明
extern int say_hello(char* name);改成:extern "C " int say_hello(char* name);