一、引言
在发行的应用程序中,经常包含动态链接库dll,它包含执行一定功能的函数供其他程序调用。Windows API函数都包含在DLL中,其中有三个最重要的DLL:
Kernel32.dll 包含用于管理内存、进程、线程的函数。
User32.dll 包含哪些用于执行用户界面的函数
GDI32.dll 包含哪些用于画图和显示文本的函数
使用动态链接库的好处:
1. 可以跨语言调用 如你用c语言编写的dll可以被delphi调用
2. 可以作为第三方库提供给其他开发人员,进行二次开发
3. 可以节省磁盘和内存空间 如果多个应用程序调用同一个dll,该dll只放入内存一次,应用进程将dll页面映射到各自的地址空间中。多个进程共享dll的同一份代码,节省内存空间。
二、创建与使用
2.1创建dll
首先打开vs2008,新建项目testDll,选择控制台程序->动态链接库->空项目,然后添加新建源文件dll1.cpp,添加代码
_declspec(dllexport) int add(int a, int b)
{
returna+b;
}
_declspec(dllexport) int sub(int a, int b)
{
returna-b;
}
为了让dll导出一些函数,在函数名字前添加一些标识符_declspec(dllexport)。
编译、连接,在Debug下会生成testDll.lib, testDll.exp(用不到) ,testDll.dll.
2.2 Dumpbin工具
为了查看dll中的导出函数,可以利用vs提供的工具dumpbin,目录在…/vc/bin下,该目录下的vcvars32.bat是一个批处理工具,用来建立vc的编译环境,如果dumpbin不能执行,可以先执行下vcvars32.bat。在命令行下输入dumpbin /exports testDll.dll,结果如下
关键看ordinal hint RVA name下面的信息,ordinal下的1、2是导出函数的序号,hint是提示码,RVA列出的地址是导出函数在DLL模块中的位置,name是导出函数的名称,这些名称有些奇怪,add的名称是? add@@YAHHH@Z,c++为了支持重载,按照自己的规则修改了函数的名字,称为“名字改编”。
2.3使用dll
动态库有两种引用方式:隐式连接和显式连接。首先我们看隐式连接。
新建一测试dll项目testLib,添加代码
extern int add(int,int);
extern intsub(int,int);
int _tmain(int argc, _TCHAR*argv[])
{
int result1 = add(2,5);
int result2 = sub(6,3);
cout<<result1<<" "<<result2;
}
编译,报以下错误
1>TestLib.obj : error LNK2001: 无法解析的外部符号"int __cdeclsub(int,int)" (?sub@@YAHHH@Z)
1>TestLib.obj : error LNK2001: 无法解析的外部符号"int__cdecl add(int,int)" (?add@@YAHHH@Z)
这是调用库函数经常见到的错误,原因是没有找到函数的实现。
我们把testDll.lib,testDll.dll拷贝到测试程序的目录下,然后选中工程名右击选择属性,在弹出的对话框选择链接器->输入,添加testDll.lib,重新编译,没问题了。
除了使用extern声明引用外部函数外,还可以使用标识符_declspec(dllimport)表明函数是从dll导入的。下面我们修改一下导入函数的声明,如下
_declspec(dllimport) int add(int,int);
_declspec(dllimport)int sub(int,int);
一般来说,客户程序并不知道函数的原型,所以我们在发布动态库时,除了提供*.dll和*.lib文件,还应提供导出函数声明的头文件*.h。为此,我们在动态库的工程中添加头文件dll1.h供库程序和客户程序使用,定义一个宏区分是导入还是导出函数,dll1.h头文件代码如下
#ifdef DLL1_API
#else
#define DLL1_API_declspec(dllimport)
#endif
DLL1_API int add(int,int);
DLL1_API intsub(int,int);
然后再dll1.cpp中添加宏定义和引用头文件,
#define DLL1_API _declspec(dllexport)
#include "dll1.h"
把testDll1.lib,testDll1.dll和dll1.h拷贝到测试程序的目录下,添加引用头文件,编译运行。好了,到此动态库的基本使用已经完成了。
2.4如何从dll中导出类
我们在dll1.h中添加以下代码:
class DLL1_API Point
{
public:
voidoutput(int a, inty);
};
在dll1.cpp中实现output函数
void Point::output(int x, int y)
{
std::cout<<x<<" "<<y;
}
在testlib工程中添加测试代码:
Point p;
p.output(1,1);
编译运行。
我们使用dumpbin工具查看dll导出函数,如下, dll导出了一个类Point,还导出了类的成员函数output。
2.5 extern ”C” 的使用
我们还记得查看导出的函数名字进行了改编,如果用C语言调用c++编写的dll,将找不到导出函数。我们希望动态链接库文件在编译时,导出函数的名字不要发生改编,这时需要在定义导出函数时添加限定符extern ”C”.下面我们对库程序进行修改,对Point的有关代码注释,代码如下:
dll1.h如下
#ifdef DLL1_API
#else
#define DLL1_APIextern "C" _declspec(dllimport)
#endif
DLL1_API int add(int,int);
DLL1_API int sub(int,int);
dll1.cpp如下
#define DLL1_API extern "C" _declspec(dllexport)
#include "dll1.h"
#include <iostream>
int add(int a, int b)
{
returna+b;
}
int sub(int a, int b)
{
returna-b;
}
使用dumpbin工具查看导出函数如下,可以看出导出函数名字没有发生改编。
利用extern "C"可以解决c语言和c++之间的相互调用时的函数命名问题,但是不能用于导出一个类的成员函数,只能用于导出全局函数,所以我们注释了Point类。
但是,如果我们改变了调用约定,即使加上了限定符extern "C",函数的名字也会发生改变。
调用约定主要规定了一个函数被调用时参数的入栈方向和调用者还是被调用者清除栈。主要的调用方式有_cdcel,_stdcall,fastcall,thiscall,nakedcall.C/C++语言函数的默认调用方式是_cdcel,_stdcall是PASCAL程序的缺省调用方式, windows的API函数也是这种方式,我们将常看到函数名称前有WINAPI修饰。
在dll库的函数声明和定义中都加上_stdcall调用约定,重新编译程序。然后使用dumpbin工具查看导出函数名字,可以看到add函数的名字为_add@8,函数名字前加了一下划线,后面加了@和数字8,表示参数所占的字节数。
2.6 使用模块定时文件导出
我们知道,C语言和Delphi的调用约定是不同的,如果我们用C语言编写一个dll,用delphi调用,在导出函数中应指定标准调用约定_stdcall,但仍会出现问题,因为函数名字会发生改变。解决方法是通过一个称为模块定义文件(def)的方式解决名字改编问题。
我们再建一个项目dll2,为该项目添加一个源文件dll2.cpp,代码如下
int _stdcall add(int a, int b)
{
returna+b;
}
然后添加一个模块定义文件dll2.def,代码如下
LIBRARY "dll2"
EXPORTS
add
其中LIBRARY语句制订动态链接库的名称,该名称与生成的动态链接库的名称一定要匹配,EXPORTS语句表明DLL将要导出的函数,以及为这些导出函数指定的符号名。编译dll2,然后用dumpbin工具查看导出函数名,如下
可见,导出函数名是按def文件指定输出的,没有发生名字改编。
2.7 显式加载dll
基本函数:
HMODULE LoadLibrary(LPCTSTR lpFileName);
FARPROC GetProcAddress(HMODULE hModule,LPCSTRlpProcName);
在测试程序中添加
HMODULE h = LoadLibrary("dll2.dll");
typedefint (*Fun)(int,int);
Fun f = (Fun)GetProcAddress(h,"add");
if(!f)
return-1;
int d = f(3,6);
FreeLibrary(h);
输出结果d=9.
动态加载在需要时才加载DLL,而隐式连接方式实现比较简单,配置好,可以随时调用DLL导出的函数。
在dll2.cpp中函数前加上调用约定_stdcall,编译后的dll2.dll拷贝至测试程序下,发现测试程序不能正确执行,必须声明函数指针时也加上调用约定。
当DLL中的导出函数采用的是标准调用约定时,访问该DLL的客户端程序也应该采用该类型类访问相应的导出函数。
2.8
如果到处函数发生名字改编,动态加载DLL会发生什么问题?
我们修改第一个项目dll1的导出函数,不加extern”C”和_stdcall,add导出函数名字为?add@@YAHHH@Z,然后调用该函数发现找不到该函数。
为了验证?add@@YAHHH@Z就是add的名字,修改获取地址的那行代码,
Funf = (Fun)GetProcAddress(h,"?add@@YAHHH@Z");
发现正确运行。
除了使用导出函数的名称外,还可以使用导出函数的序号来访问该函数,使用MAKEINTERSOURCE宏把指定的函数序号转化为指定的函数名字字符串。修改如下
Funf = (Fun)GetProcAddress(h,MAKEINTRESOURCE(1));
发现能正确输出。
2.9 注意
像extern "C", _declspec(dllexport),_declspec(dllimport)标志符可以只在函数声明的时候指定,在实现时可以不加,但是调用约定_stdcall必须在声明和实现时都加上,否则编译器认为是不同的函数,出现重定义错误,使用显式调用时函数指针也要加上_stdcall。