基于
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
开发环境下,打开
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) { 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 #i nclude #i nclude"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
并在该文件头删除
#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) { 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中函数的结构信息。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 #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 #i nclude #i nclude"Dlltest.h" void main() {int a; a=min(8,10) printf("
比较的结果为
%d "
,
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
函数的例子。
#i nclude #i nclude 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 "
,
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
中的函数速度很快,但是要记住函数的使用序号,否则会发生错误。