浅谈C++ DLL的编写

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库不同于静态库,在静态库情况下,函数和数据被编译进一个二进制文件(通常扩展名为*.LIB), Visual C++的编译器在处理程序代码时将从静态库中恢复这些函数和数据并把他们和应用程序中的其他模块组合在一起生成可执行文件。这个过程称为”静态链接”,此时因为应用程序所需的全部内容都是从库中复制了出来,所以静态库本身并不需要与可执行文件一起发行。

  在动态库的情况下,有两个文件,一个是引入库(.LIB)文件,一个是DLL文件,引入库文件包含被DLL导出的函数的名称和位置,DLL包含实际的函数和数据,应用程序使用LIB文件链接到所需要使用的DLL文件,库中的函数和数据并不复制到可执行文件中,因此在应用程序的可执行文件中,存放的不是被调用的函数代码,而是DLL中所要调用的函数的内存地址,这样当一个或多个应用程序运行是再把程序代码和被调用的函数代码链接起来,从而节省了内存资源。从上面的说明可以看出,DLL文件必须随应用程序一起发行,否则应用程序将会产生错误。

微软的Visual C++支持三种DLL,它们分别是Basic Dll(不使用MFC动态库的DLL)、Regular Dll(常规DLL)、Extension Dll(扩展DLL)。
- Basic DLL指的是不用MFC的类库结构,直接用C语言写的DLL,其导出的函数是标准的C接口,能被非MFC或MFC编写的应用程序所调用。也称为Non-MFC DLL。
- 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开发应用程序。Regular DLL又可细分成静态链接到MFC和动态链接到MFC上的,这两种常规DLL的区别将在下面介绍。与常规DLL相比,使用扩展DLL用于导出增强MFC基础类的函数或子类,用这种类型的动态链接库,可以用来输出一个从MFC所继承下来的类。
- Extenxion DLL是使用MFC的动态链接版本所创建的,并且它只被用MFC类库所编写的应用程序所调用。例如你已经创建了一个从MFC的CtoolBar类的派生类用于创建一个新的工具栏,为了导出这个类,你必须把它放到一个MFC扩展的DLL中。扩展DLL 和常规DLL不一样,它没有一个从CWinApp继承而来的类的对象,所以,开发人员必须在DLL中的DllMain函数添加初始化代码和结束代码。

Basic DLL

每一个DLL必须有一个入口点,这就象我们用C编写的应用程序一样,必须有一个WINMAIN函数一样。在Non-MFC DLL中DllMain是一个缺省的入口函数,你不需要编写自己的DLL入口函数,用这个缺省的入口函数就能使动态链接库被调用时得到正确的初始化。DLLMain函数不仅在将DLL链接加载到进程时被调用,在DLL模块与进程分离时(以及其它时候)也被调用。

如果程序员没有为DLL模块编写一个DLLMain函数,系统会从其运行库中引入一个不做任何操作的缺省DLLMain函数版本。在单个线程启动和终止时,DLLMain函数也被调用。

如果应用程序的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文件。

使用导出函数关键字

使用 _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中的函数。

用.def文件创建工程MyDll

为了用.def文件创建DLL,请先删除上个例子创建的工程中的MyDll.h文件,保留MyDll.cpp并在该文件头删除#include MyDll.h语句,同时往该工程中加入一个文本文件,命名为MyDll.def,再在该文件中加入如下代码:

LIBRARY MyDll  //注意这里的MyDll是工程名如果不同则应用程序连接库时会发生连接错误
EXPORTS
Max @1,
Min @2,

  其中LIBRARY语句说明该def文件是属于相应DLL的,EXPORTS语句下列出要导出的函数名称。我们可以在.def文件中的导出函数后加 @n,如Max@1,Min@2,表示要导出的函数顺序号,在进行显式连时可以用到它。该DLL编译成功后,打开工程中的Debug目录,同样也会看到 MyDll.dll和MyDll.lib文件。

MFC DLL——Regular/Extension

MFC 的 AppWizard 可以让我们创建 MFC 库支持的两种 DLL :扩展的 DLL 和正规的 DLL 。这两种类型的区别是什么呢?

  • 扩展 DLL 支持 C++ 接口,换句话说,该 DLL 可以导出整个类,客户可以构造这些类的对象或从这些类进行派生。扩展 DLL 动态连接到 MFC 库的 DLL 版本的代码,因此,扩展 DLL 要求客户程序被动态连接到 MFC 库 (AppWizard 默认设置 ) ,并且客户程序和扩展 DLL 要一致连接到 MFC DLL 的相同版本 (mfc42.dll 、mfcd42.dll 等 ) 。扩展 DLL 很小,我们可以创建一个简单的扩展 DLL ,大约 10KB 左右,它的装载会很快。
  • 如果我们需要一个 DLL ,并希望它可以被任何 Win32 编程环境 ( 包括 Visual Basic 5.0) 装载,那么我们必须使用正规 DLL 。这里有一个很大的限制就是,正规 DLL 可以导出 C 风格的函数,但不能导出 C++ 类、成员函数或重载函数,因为每一个 C++ 编译器都有自己修饰名字的方法。不过,我们可以在正规 DLL 内部使用 C++ 类 ( 特别是 MFC 库的类 ) 。

MFC Regular DLL

  当我们创建 MFC 正规 DLL 时,我们可以选择静态连接或动态连接到 MFC 库。如果选择了静态连接, DLL 将包括所有它需要的 MFC 库代码的拷贝,因此它可以独立于 MFC 库。一个典型的 Release 版本静态连接的正规 DLL 大约为 144KB 左右。如果选择动态连接,大小可降到 17KB 左右,但必须保证适当的 MFC DLL 在目标机器上存在。

当用 AppWizard 产生Regular DLL 时, DllMain 函数将出现在框架内,并且我们会得到一个 CWinApp 的派生类 ( 和一个该类的全局对象 ) ,就像 EXE 程序的情形一样。我们可以通过重载 CWinApp::InitInstance 和 CWinApp::ExitInstance 函数获得控制。然而大多数情况下,我们不用重载这两函数。我们只要编写 C 函数,然后用 __declspec(dllexport) 修饰符导出这些函数 ( 或者在工程的 DEF 文件里加入函数入口 ) 即可。

使用AFX_MANAGE_STATE 宏

动态链接到MFC的Regular DLL需要链接到mfc42.dll。当 mfc42.dll 作为进程的一部分被装入时,它把数据存放在一些可靠的全局变量里。如果我们从一个 MFC 程序或扩展 DLL 中调用 MFC 函数,则 mfc42.dll 会知道如何代表调用进程去设置这些全局变量。然而,如果我们从一个正规 MFC DLL 中调用进入 mfc42.dll ,则全局变量并不同步,其后果不可预知。为了解决这个问题,请在正规 DLL 所有导出函数的开始处,插入下面的代码行:

AFX_MANAGE_STATE(AfxGetStaticModuleState());

如果 MFC 代码被静态连接,则该宏不会有任何影响。

MFC Extension DLL

  最后一种是MFC扩展DLL,这种DLL特点是用来建立MFC的派生类,Dll只能被用MFC共享库所编写的应用程序所调用。在程序和扩展dll之间,可以传递MFC对象,被传递的对象存在于创建它们的模块中。当使用共享版的MFC dll时候函数被正确的输出,因此你可以在程序以及扩展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有以下特性和要求:

  1. 使用扩展dll的可执行程序必须是MFC程序,且编译的时候定义了 _AFXDLL 。这个定义是在项目属性的C/C++的预处理器–预处理定义中设置的。

  2. 扩展dll可以由使用了动态链接到MFC的regular dll使用(使用了MFC共享版dll)

  3. 扩展dll编译时应该使用_AFXEXT 定义,这样可以强制_AFXDLL也被定义,确保从MFC的头文件中获取正确的声明。也可以确保AFX_EXT_CLASS被定义为__declspec(dllexport)。这个宏就可以声明这个类及对象可以从dll中输出。

  4. 扩展dll中不能实例化一个从CWinApp继承的类。而应该使用使用这个扩展dll的程序中提供这个对象。

  5. 扩展dll应该提供DllMain函数去执行必要的初始化。

如果你正在使用模块定义文件输出,也就是使用.def文件来输出dll中的类和函数,那么在头文件的开始和结尾放置一下代码:

#undef AFX_DATA
#define AFX_DATA AFX_EXT_DATA
// <body of your header file>
#undef AFX_DATA
#define AFX_DATA

这四行代码可以你的代码正确编译,省略了可能引起你的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环境变量中的一系列目录。

隐式链接

  隐式链接就是在程序开始执行时就将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);
}

//注意如果程序发生连接错误则要把lib文件加要工程里去
在创建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既可。

显式链接

显式链接是应用程序在执行过程中随时可以加载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中的函数速度很快,但是要记住函数的使用序号,否则会发生错误。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值