动态连接库
动态连接库,简称
DLL(Dynamic-Link Library)
,它是基于
Windows
程序设计的一个非常重要的组成部分。在建立应用程序的可执行文件时,不必将
DLL
连接到程序中,而是在运行时动态装载
DLL
,装载时
DLL
被映射到进程的地址空间中。
一.
DLL
概述
使用普通的函数库,在程序链接时将库中的代码拷贝到可执行文件中,这是一种静态链接,在多个同样的程序执行时,系统保留了许多重复的代码副本,造成内存资源浪费。使用
DLL
的动态链接并不是将库代码拷贝,只是在程序中记录了函数的入口点和接口,在程序执行时才将库代码装入内存。不管多少程序使用
DLL
,内存中都只有一个
DLL
的副本,当没有程序使用时,系统就将它移出内存,减少了对内存和磁盘的要求。
DLL
是一种基于
Windows
的程序模块,它不仅可以包含可执行的代码,还可以包含有数据,各种资源,扩大了库文件的使用范围。比如:在系统目录下有一个
Comdlg32.dll
文件,它包含了公共对话框的代码和资源。有些设备驱动程序也是由动态链接库实现的
(
扩展名一般是
drv)
。
二.
MFC
中的
DLL
MFC
类库本身是用
DLL
形式实现的共享库。在新建一个工程时,你可以选择使用静态库的还是动态的
DLL
库。其中,动态共享库就是
Visual C++6.0
安装在系统目录和其他目录下的一些
DLL
,例如安装在系统
System32
下的
MFC42.dll
文件。
工程建好以后,也可以使用
Project|Settings…
菜单项,选择对话框中的
General
标签,其中的内容可以改变使用动态共享库还是静态库的设置。但是需要注意的是,使用共享库编出的应用程序在发布时,要将开发时所使用的
VC++
对应版本的
MFC DLL
文件一同发布,主要是考虑到这种情况:用户使用你编制的软件,但用户计算机上没有安装相应的
VC++
或者是版本不同,他的系统中就没有这些
DLL
或和她们版本不同,那你的软件将不能在用户的计算机上运行。
三.
DLL
入出口函数
DLL
程序本身并不能运行,它需要一个入出口函数,就像
main
函数一样,在应用程序使用
DLL
中的内容之前,系统先调用入出口函数完成
DLL
的初始化和终止工作。
一个
DLL
可以有一个入出口函数,系统在某些时候会调用这个
DLL
入出口函数。通常是完成针对应用程序的初始化和结束处理。如果建立的是只有资源的
DLL
或不需要这种处理的
DLL
,就不必实现此函数。
VC++
已经定义了简单的
DllMain
等
DLL
入出口点函数,它完成了一些初始化工作,包括对
C
运行时库调用的支持等。所以如果你的
DLL
程序中没有这样的函数,链接器会自动将这个缺省的
DllMain
链接上。
一般
DLL
的入出口函数是
DllMain
函数,在
MFC AppWizard
自动生成的两种
Regular DLL
中则是另外一种形式,下面分别介绍。
(1)DllMain
函数
DllMain
函数在系统调入或撤除这个
DLL
时调用,这些动作一般发生在应用程序使用
LoadLibrary
和
FreeLibrary
等函数以及进程线程启动终止的时候。一般它的结构如下:
BOOL APIENTRY
DllMain(HINSTANCE hInstance,DWORD dwReason,LPVOID lpReserved)
{
switch(deReason)
{
case DLL_PROCESS_ATTACH;
break;
case DLL_THREAD_ATTACH;
break;
case DLL_THREAD_DETACH;
break;
case DLL_PROCESS_DETACH;
break;
}
return TRUE;
}
其中,参数
hInstance
是
DLL
模块句柄,
lpReserved
用于指定
DLL
初始化和清除的一些内容的指针,参数
deReason
表明了调用
DllMain
函数的原因:
DLL_PROCESS_ATTACH
:
DLL
被链接到当前进程的地址空间中。
DLL_THREAD_ATTACH
:当前进程创建一个新线程,新线程要访问
DLL
。
DLL_THREAD_DETACH
:表示线程与
DLL
分离。
DLL_PROCESS_DETACH
:表示进程和
DLL
分离。
根据列出的原因,进行相应的处理工作。
(1)MFC AppWizard
生成的
RegularDLL
的入出口
每个
Regular DLL
都有
MFC AppWizard
自动生成的一个
CWinApp
派生类的对象,与
MFC
应用程序一样,它是在
CWinApp
派生类的成员函数
InitInstance
和
ExitInstance
中完成初始化和终止工作的。
实际上,
MFC
提供了一个最基本的
DllMain
函数,你在这种
DLL
中不必自己编写
DllMain
函数,由
MFC
提供的这个函数在装载
DLL
时调用
InitInstance
,而在
DLL
退出时调用
ExitInstance
。所需要完成的初始化和终止工作就在这两个函数中完成。
四.
从
DLL
中导出函数
知道了
DLL
的入出口函数,
DLL
就要开始被用户应用程序使用了,有哪些内容可以被用户使用呢?下面将分别介绍
DLL
中的函数,数据和资源。
DLL
中定义有两种函数:导出函数和内部函数,内部函数只能在
DLL
内部使用,它的定义和使用和普通程序一样。导出函数可以被其他模块调用,其他模块就要知道该
DLL
导出了哪些函数,函数定义接口等信息。
DLL
中包含有导出表,其中有每个导出函数的名字,只有导出表中的函数可以被其他可执行程序调用。下面将介绍导出函数的三中方法。
(1)
使用
DEF
文件导出函数
模块定义文件
(DEF)
是由一个或多个用于描述
DLL
属性的语句组成的文本文件。
NAME
语句,指出生成程序或
DLL
的文件名;
LIBRARY
语句,指出
DLL
的内部名字,这个语句告诉链接器要生成的是
DLL
;
DESCRIPITON
语句,描述
DLL
的用途;
STACKSIZE
语句,设置堆栈大小;
SECTIONS
语句,设置段属性;
EXPORTS
语句,列出被导出函数的名字,以及其他信息;
VERSION
语句给出该
DLL
的版本号;
分号开头的语句是注释。
例子如下:
LIBRARY “MyDll”
DESCRIPTION ‘TEST DLL’
EXPORTS
;
在此输出函数
Func1 OtherName=Func1
Func1 @1
Func2 @2
Func3 @3 NONAME
DEF
文件中的字符串如果包含空格,分号或和关键词相同,要用双引号引起来。
EXPORTS
语句后,函数名首先出现,还可以象
Func1 OtherName=Func1
语句一样将同一个函数以不同的名字输出,也就是说,输出的名字不一定是函数的原名。在
@
之后选择一个序号,在使用
DLL
的应用程序中也可以通过这个序号来调用
DLL
中的函数。加上
NONAME
标志,表明不输出函数名而只输出函数的序号。这就意味着应用程序只能用序号访问
DLL
中的函数。
新建一个工程时,使用
MFC AppWizard(dll)
自动创建
DLL
,它会创建一个
DEF
文件的框架并添加到这个工程中,而
Win32 Dynamic-Link Library
来生成
DLL
,都没有直接生成
DEF
文件,如果需要,必须自己创建并加入到工程中。
在编译建立
DLL
时,编译链接器查找工程中是否有
DEF
文件,如果有,将使用这个文件来生成一个导出文件
(.exp)
和一个导入库文件
(.lib)
。然后使用导出文件来生成一个
DLL
文件。若没有,则不能生成
LIB
文件,对于想隐式连接这个
DLL
的用户应用程序来说,将因找不到
DLL
导出的函数(就在
LIB
文件中)而在链接时失败。
(1)
使用关键字
_declspec(dllexport)
在定义函数时,可以使用关键字
_delspec(dllexport)
来从
DLL
中导出数据,函数和类或者类的成员函数。
例如:如下这样使用关键字来导出函数:
void _declspec(dllexport) Func1(void)
它用于类定义时,与在函数定义时一样使用,导出了类中的所有
public
数据成员和成员函数。
Class _declspec(dllexport) CMyExportClass:public CObject
{
//……………….
}
如果使用这个关键字,则可以不需要
DEF
文件。编译时可以生成
LIB
文件提供给应用程序使用。
与这个关键字相对应,还有
_declspec(dllimport)
关键字可以用于在应用程序中引入
DLL
中的数据,函数和类及对象等。
使用时还要注意一个区别:
_declspec
用在声明变量或对象的类型名之前,例如:
_declspec(dllimport) class X{} varX;
这个属性应用在
varX
这个对象上,说明引入的是
DLL
中的
varX
,这成了
DLL
导出变量的方法。
_declspec
放在
class
或
struct
关键字之后,应用在用户定义的类型名,例如:
Class _declspec(dllimport) X{};
说明引入的是
X
这种类定义结构,这样就可以在用户的应用程序中使用这个类定义。
(1)
使用
AFX_EXT_CLASS
导出
MFC
扩展
DLL
使用宏
AFX_EXT_CLASS
来导出类,链接这种
DLL
的应用程序或其他
DLL
使用这个宏来导入类。
MFC
自动生成的程序框架直接支持了在
DLL
和在应用程序中的这个宏定义,这样,使用
AFX_EXT_CLASS
,
MFC
扩展
DLL
和应用程序就可以使用相同的头文件,给开发工作带来便利。
如果定义了
_AFXDLL
和
_AFXEXT
,这表明目标文件是
MFC
扩展
DLL
,
AFX_EXT_CLASS
被
MFC
定义成了关键字
_declspec(dllexport)
,而只定义了
_AFXDLL,
没有定义
_AFXEXT
,这个宏被定义成了
_declspec(dllimport)
,供应用程序使用。实际上,在
MFC
框架中,
_AFXDLL
表明使用共享
MFC DLL
。这个宏可以导出整个类,如:
class AFX_EXT_CLASS CMyExportClass::public CObject{….}
也可以导出类中的某个成员:
class CExampleDialog:public CDialog
{
public:
AFX_EXT_CLASS CExampleDialog();
AFX_EXT_CLASS int DoModal();
}
五.
DLL
中的数据和内存
(1)
从
DLL
中导出数据
DLL
既可以导出函数,也可以导出数据供应用程序使用。
1>
使用
DEF
文件
CONSTANT
关键词导出
在
DEF
文件中导出语句
EXPORTS
除了
NONAME
标志外,还有一个
CONSTANT
标志,表明前面的导出名不是函数名,而是一个数据变量。
例如:在
DLL
中定义了一个整型变量:
int nVariable = 0;
在
DEF
文件中使用下列语句:
EXPORTS
nVariable @5 CONSTANT
在用户应用程序中,用以下语句来使用
DLL
导出的数据:
extern int nVariable;
printf(“DLL
中的
nVariable=%d”,*(int*)nVariable);
注意,这种方法使用的并不是变量本身,而是
DLL
中导出变量的指针,应用程序必须通过强制指针转换来使用。
这样使用,变量命名的约定(
Windows
中用前缀表示变量的类型,指针变量前缀一般是
p
)就和其他变量不统一了。另一个问题就是和
DLL
中的使用表示法不一样,可能给一同开发
DLL
和用户应用程序带来混乱。
可以采用以下的方法解决上述问题:
在应用程序中定义:
extern int nVariable;
#define nVariable *(int *)nVariable
2>
或在
DEF
文件中写成
EXPORTS
pVariable = nVariable @5 CONSTANT
3>
应用程序中设计如下:
extern int pVariable;
printf(“DLL
中的
nVariable=%d”,*(int*)pVariable);
这样用不同的方法给变量起了别名,和命名约定与
DLL
中的名字就一致了。
其实使用
_declspec(dllimpot)
关键词就简单多了。
_declspec(dllimport) int nVariable;
printf(“DLL
中的
nVariable=%d”,nVariable);
4>
使用
DEF
文件
DATA
关键词导出
在
DEF
文件中用
DATA
关键词来代替
CONSTANT
使用,其他没有区别。
5>
使用
_declspec(dllexport)
关键词导出
也可以不使用
DEF
文件,而在源程序中使用
_declspec(dllexport)
关键词来修饰定义要导出的变量。
在用户应用程序中也要使用
_declspec(dllimport)
关键词来引入对
DLL
中导出变量的使用。
(2)
多个进程共享
DLL
中的数据和内存
首先说明一下
DLL
数据共享和导出数据的区别。导出数据是指这个
DLL
中的数据在应用程序视野之内,并可以使用,但它不一定是多进程共享的,而
DLL
中共享数据,是指多进程调用
DLL
时内存中只保留一个数据副本供它们共同拥有,它不一定导出,可能只是
DLL
内部使用而应用程序无法使用。
在
DLL
中定义的数据和申请的内存,缺省的一般都是每一个调用它的进程保留一份副本,不管是导出的还是
DLL
内部使用没有导出的。这可以看作是私有的。要想共享使用同一份
DLL
中数据和内存,需要设计实现。
在多线程中,同一个进程下的所有线程都共享使用这个进程的所有资源,对于
DLL
中的数据和内存也不例外。下面讨论在多进程中,也就是在不同的应用程序之间共享使用的情况。
1>
共享数据
在程序设计中,可以使用
#pragma data_seg(“.DatSegName”)
编译指令在编译这个
DLL
程序的指定声明一个数据段。
例如:
#pragma data_seg(“.MyDataSegName”)
int nVariable;
#pragma data_seg()
这样在
DLL
中声明了一个名为
.MyDataSegName
的数据段,其中的内容就是
nVariable
。名字前面以一个小点打头,通常这样约定:代码段名前加下划线,数据段名前打点。
如果不指明这个数据段的属性,它和其他段没什么区别,也就成为每个调用
DLL
的进程私有的了。在
DEF
文件中,使用
SECTIONS
语句来指明这个段的属性。
SECTIOS
.MyDataSegName READ WRITE SHARED
SHARED
标志表明共享属性。
这样,当许多进程调用
DLL
时,它们都使用的是同一个
nVariable
变量。
2>
动态分配共享内存
当在
DLL
中需要使用动态分配的内存作为共享内存,在多个进程中使用的时候,可以使用内存映射文件实现。
在第一个进程调用创建内存映射文件的程序时,它真正创建了这个对象,其他进程在用同一个内存映射文件名来创建时,系统返回的是同一个对象在不同进程中可以使用的对象句柄。这样在多个进程中就实现了共享这个内存映射文件。
只有当所有使用这个
DLL
的进程终止后,系统才将这个
DLL
从内存中释放,同时将这个内存映射文件对象删除。
六.
程序链接
DLL
不能单独运行,必须和用户应用程序相连接才能使用。
链接
DLL
到一个应用程序中主要有两种方式:隐式链接和显式链接。
(1)
隐式链接
使用
DLL
的应用程序先链接到编译
DLL
时生成的导入库
LIB
文件,在执行这个应用程序时,系统也装载它所需要的
DLL
。
采用这种方法,在应用程序退出之前,
DLL
一直存在与该程序运行进程的地址空间中。
要使用隐式链接,用于应用程序必须能够从
DLL
开发者那里获得以下信息:
1)
包含导出函数以及类声明的头文件,在程序开发时要知道函数名和函数接口信息。
2)
DLL
的导入库
LIB
文件,应用程序在编译链接时需要。
3)
实际的
DLL
文件,它是在应用程序运行时所必需的。
应用程序中加入含有导出函数的头文件,这样在编程时调用导出函数和调用其他函数完全一样。
可以将
DLL
项目中的输出对外接口的头文件拷贝到自己的工程中,并填加到项目中来,或者在程序里直接用
include
语句将这个文件加上路径名来使用。
将
DLL
的
LIB
文件加入应用程序中,可以使用
Project|Add To Project|Files…
菜单项弹出的对话框来选择相应的
LIB
文件,也可以用另外的方法,在
Project|Settings.…
弹出对话框中选择
Link
标签,在其中的
”Object/Library Modules”
输入指定的
LIB
文件名。在应用程序编译链接时就可以找到这个导入库文件。
(2)
显式链接
显式链接是指应用程序在运行时通过函数调用来显示装载和下载
DLL
,并通过函数指针来调用
DLL
的导出函数。
1)
调用
LoadLibrary
或
AfxLoadLibrary
函数装载
DLL
并得到模块句柄。
AfxLoadLibrary
函数原型如下:
HINSTANCE AFXAPI AfxLoadLibrary(LPCTSTR lpszModuleName);
参数
lpszModuleName
给出
DLL
或
EXE
文件名,返回得到相应模块的句柄。
2)
调用
GetProcAddress
来获取导出函数的指针。
FARPROC GetProcAddress(HMODULE hModule, //DLL
模块的句柄
LPCSTR lpProcName //
要获取的函数的名字
);
这个函数通过给定的
lpProcName
函数的名字获得函数指针来调用。前面提到的
DEF
文件中可以将输出函数定义成
NONAME
的,只输出一个函数的序号,如果知道都是哪些函数的话,在此也可以这么使用:
GetProcAddress(hMyDllModule,MAKEINTRESOURCE(MyFuncID));
MyFuncID
即输出函数的序号,
MAKEINTRESOURCE
是将整数转换成所使用字符串。
3)
在使用完毕以后,调用
FreeLibrary
或
AfxFreeLibrary
函数来释放
DLL
。
AfxFreeLibrary
函数原型如下:
BOOL AFXAPI AfxFreeLibrary(HINSTANCE hInstLib);
hInstLib
就是前面装入的模块句柄,用这个函数将
DLL
从一个应用程序中卸载掉。
因为是使用指针来调用函数,并没有用函数名,所以在开发应用程序时,就不再需要太多的
DLL
信息,但对于函数接口必须要清楚,运行时也要提供相关的
DLL
文件。
使用显式链接,不需要
DLL
项目提供头文件,但使用函数的序号或只是函数指针来调用,很容易发生错误,设计时要协调好
DLL
和应用程序的接口。
七.
DLL
的使用和调试
(1)
DLL
的使用
系统运行一个调用
DLL
的应用程序时,将在下列位置查找该
DLL
:
1)
含有该应用程序文件的目录。
2)
当前执行所在目录。
3)
Windows
的系统目录
(system
或
system32)
。
4)
Windows
目录。
5)
在环境变量
PATH
中列出的目录。
若找不到这个
DLL
文件,系统将显式对话框提示并立即终止程序执行。所以在应用程序设计时要考虑到它使用的
DLL
的目录位置问题。
一个设计良好的应用程序,要考虑到本身所带的
DLL
问题,在软件安装时,将相关的程序和各种库文件,安装在各自目录中,而当删除时,又能将这些不再需要的文件清除干净,减少系统垃圾。
(2)
如何调试
DLL
由于
DLL
本身是不可执行的,给它的开发和调试工作带来了一定困难,现在
VC++6.0
的集成开发环境,强大的编辑调试功能基本上解决了这个问题。
1>
同时使用
DLL
和应用程序的工程来调试
DLL
如果开发
DLL
和用户应用程序两个工程,需要调试
DLL
,将两个工程在同一个工作区打开。
可以采用的方法有:将开发
DLL
程序的工程添加到开发应用程序工程的工作区中。使用
Project|Insert Project into Workspace…..
菜单弹出的对话框选择,也可以在已经打开一个工程的情况下,直接使用
File|New…..
菜单选择
Project
标签来创建另一个新工程,选中添加到当前工作区。
这样一个工作区中,有两个程序的工程同时进行开发。
为了能够调试
DLL
程序,两个工程都使用
Win32 Debug
版本。在
Project|Settings….
的对话框
Link
标签下都选中
Generate Debug Info
。
在
Project|Settings…..
对话框的
Debug
标签下,
Category
选择
Addtioal DLLs,
将要调试的
DLL
文件加入其中。每一个
Modules
前面的选中框表示在开始调试前是否装入这个模块文件。
同时,在
Project|Dependencies….
对话框选择应用程序的工程依赖于
DLL
的工程,在
dll
程序发生改动时,编译用户应用程序,可以根据文件新旧比较把
DLL
的工程也编译链接了。
DLL
工程在程序改动后,编译链接生成新版本出来,为了不要在
DLL
和应用程序两个工程之间经常来回手工拷贝
dll
文件,在
DLL
工程的
Project|Settings…
对话框中
Post-Build Step
标签下可加上将编译好的
DLL
拷贝到应用程序能找到的目录的一个命令,如:
”copy /Debug/MyDll.dll c:/MyApp/Debug”
,这样做的目的是在经常编译调试
DLL
的同时,每编译一次之后,都执行这个拷贝命令,将最新版本的
DLL
文件提交给应用程序使用。
如果开发的应用程序使用隐式链接
DLL
,它需要从
DLL
工程中获取包含
DLL
导出信息的头文件和编译生成的导入库
LIB
文件。头文件在程序中用
#include
语句加入,使用
Project|Add to project|Files…..
菜单弹出的对话框将
DLL
工程的
LIB
文件加入到应用程序的工程中。
2>
使用应用程序的工程来调试
DLL
打开应用程序的工程,在
Project|Settings….
菜单弹出框的
Debug
标签下,
Category
选择
General
,在
Program Arguments
中指定应用程序命令行参数
(
可以不设置
)
。
Category
选择
Addtioal DLLs
,指定调试的
DLL
文件。如果使用远程调试
(
在
Build|Debugger Remote Connection….
菜单中设置
)
,要给出完整的网络路径。
这个
DLL
必须是编译成
Win32 Debug
版本的程序,包含有调试信息。这样尽管
DLL
的源程序不是这个工程的组成部分,也可以在应用程序和
DLL
的源程序中设置断点。
3>
使用
DLL
的工程来调试
DLL
打开
DLL
的工程,在
Project|Settings….
菜单弹出对话框的
Debug
标签下,
Category
标签选择
General
,为调试这个
DLL
指定一个可执行程序,它可以就是另外开发的使用该
DLL
的用户应用程序。
4>
只有
DLL
文件和源程序来调试
DLL
或许你只有
DLL
文件和源程序,但没有工程文件,也就不知道源程序和这个
DLL
工程的关系,这种情况下也可以进行调试。使用
File|Open….
菜单打开
DLL
文件,这个
DLL
文件必须是以前编译的包含有调试信息的文件,在
Project|Settings…..
菜单弹出对话框的
Debug
标签下,
Category
选择
General
,为调试这个
Dll
指定一个可执行程序,就可以开始调试了。
通过上面的设置,无论是开发应用程序还是制作
DLL
,都简化了操作,而且确保使用的是最新的
DLL
版本。作好这些设置工作以后,就可以利用应用程序来对
DLL
程序进行调试了。在调试的过程中,可以从应用程序单步跟踪到
DLL
程序中去,在
DLL
中设置断点,在应用程序调用
DLL
中的程序,执行到这个断点时,也会中断,以便检查此时的
DLL
中的状态。
终于写完了,呵呵,写了半天,快累死了
^_^
,周末本来应该好好休息的,可是自己现在不总结一下,那就更没有时间了。还是我最喜欢的一句话,痛并快乐着
^_*
。希望对大家有所帮助。