一、前言
自从微软推出 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 开发环境下,打开 FileNewProject 选项,可以选择 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) |
参数中, 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 |
该动态链接库编译成功后,打开 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 并在该文件头删除 #i nclude 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) |
参数 hinstDll 存放 DLL 的句柄,参数 reason 指明调用函数的原因, lpReserved 是一个被系统所保留的参数。对于隐式链接是一个非零值,对于显式链接值是零。
在 MFC 下建立 DLL 文件,会自动生成 def 文件框架,其它与建立传统的 Non-MFC DLL 没有什么区别,只要在相应的头文件写入关键字 _declspec(dllexport) 函数类型和函数名等,或在生成的 def 文件中 EXPORTS 下输入函数名就可以了。需要注意的是在向其它开发人员分发 MFC 扩展 DLL 时,不要忘记提供描述 DLL 中类的头文件以及相应的 .LIB 文件和 DLL 本身,此后开发人员就能充分利用你开发的扩展 DLL 了。
应用程序使用DLL可以采用两种方式:一种是隐式链接,另一种是显式链接。在使用DLL之前首先要知道DLL中函数的结构信息。Visual C++6.0在VCin目录下提供了一个名为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 |
在创建 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 函数的例子。
#i nclude |
在上例中使用类型定义关键字 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 中的函数速度很快,但是要记住函数的使用序号,否则会发生错误。