简述DLL编程

来源:互联网 发布:tw域名注册 编辑:程序博客网 时间:2024/06/03 18:54
本文例子的源文件下载地址为:http://download.csdn.net/download/ljianhui/5606107

一、什么是DLL

动态链接库英文为DLL,是Dynamic Link Library 的缩写形式,DLL是一个包含可由多个程序同时使用的代码和数据的库,DLL不是可执行文件。动态链接提供了一种方法,使进程可以调用不属于其可执行代码的函数。函数的可执行代码位于一个 DLL 中,该 DLL 包含一个或多个已被编译、链接并与使用它们的进程分开存储的函数。DLL 还有助于共享数据和资源。多个应用程序可同时访问内存中单个DLL 副本的内容。DLL 是一个包含可由多个程序同时使用的代码和数据的库。

动态链接库通常都不能直接运行,也不能接收消息。它们是一些独立的文件,其中包含能被可执行程序或其它DLL调用来完成某项工作的函数。只有在其它模块调用动态链接库中的函数时,它才发挥作用。    

二、静态库与动态库的区别

静态链接库与动态链接库都是共享代码的方式,静态库中的函数和数据被编译进一个二进制文件(通常扩展名为.LIB)。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件(.EXE文件)。

但是若使用 DLL ,该DLL 不必被包含在最终EXE 文件中,使用动态库的时候,往往提供两个文件:一个引入库(后缀为.lib,但是它与静态库有本质上的区别)和一个DLL。引入库包含被DLL导出的函数和变量的符号名,DLL包含实际的函数和数据。在编译链接可执行文件时,只需要链接引入库,DLL中的函数代码和数据并不复制到可执行文件中,而是在运行的时候,再去加载DLL,访问DLL中导出的函数。

静态链接库和动态链接库的另外一个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。

DLL 的编制与具体的编程语言及编译器无关。只要遵循约定的DLL 接口规范和调用方式,用各种语言编写的DLL 都可以相互调用。譬如Windows 提供的系统DLL (其中包括了Windows API),在任何开发环境中都能被调用,不在乎其是Visual BasicVisual C++还是Delphi

三、如何编写一个动态库(dll)
下面以在VS2010中使用VC编写的例子说明如何编写。

1、函数的导出和调用
新建一个Win32的DLL工程,工程名为VCDLL,新建一个C++的源文件,如dlltest.cpp在里面添加如下代码:
__declspec(dllexport)int add(int a, int b)
{
return a+b;
}
__declspec(dllexport)int sub(int a, int b)
{
return a-b;
}
按Build就会在你的Debug目录上看到两个前面说到的重要的文件,VCDLL.dll和VCDLL.lib。或许你会问这个__declspec(dllexport)是什么其实它是一个修饰语句,用来说明这个add和sub函数都是导出函数。那么什么又是导出函数呢?dll里的函数或类或数据成员(类和数据成员的应用下面再说)不同于一般的文件,导出函数就是说这个函数可供应用程序调用,而没有这个修饰语句的函数就是非导出函数,它只能在dll的内部被调用,而不能被应用程序调用。这就像类的public和private修饰的那样。

那有没有什么工具能看到一个dll文件里有什么函数是导出函数呢?答案是有的,在VC6.0中有一个叫Depends的图形界面工具可以查看,但是VS2010没有自带这个工具,但是有一个很好用的文本命令工具——dumpbin。打开VS2010的命令提示工具,用cd命令把目录切换到.dll的目录下,输入dumpbin -exports VCDLL.dll,就可以看到它的导出情况了,如下图:
TM截图20130601135012
中间那里可以看到导出的函数为add和sub。ordinal为函数的序号,hint表示提示码,rva是表示函数在dll中的地址,name表示函数名。

细心的读者可能会注意到,函数名有点怪,函数名被添加了很多奇怪的符号。这是因为在C++中为了支持函数重载,所以编译器在编译链接时,它往往会去篡改函数的名字,按照自己定义的法则去改变函数的名字。不同的C++的编译器定义的改名的法则并不相同,这就导致了一个严重的问题,就是用一个编译器编写的dll在另一个编译器中使用时,会因为改名的法则不同而找不到导出的函数,从而导致调用出错。解决的方法有很多种,下面再详述。

如果在声明一个函数时没有__declspec(dllexport)这个修饰语句时,会有什么的结果呢,把前面代码两个函数中的__declspec(dllexport)去掉,重新Build一下,用同样查看的方法,结果如下:
TM截图20130601134845

前面已经说过,一个dll是无法独立地运行的,它必须要被应用程序调用才能发挥它的作用。在VS2010中新建一个win32的控制台程序DLLCall来测试之前所写的dll,代码如下:
#include "stdafx.h"
#include <iostream>

#pragma comment(lib, "VCDLL.lib") //静态加载

using std::cout;
using std::cin;
using std::endl;

__declspec(dllexport)int add(int a, int b);    //函数声明
__declspec(dllexport)int sub(int a, int b);

int _tmain(int argc, _TCHAR* argv[])
{
cout<<"5+3 = "<<add(5,3)<<endl;    //调用add函数
cout<<"5-3 = "<<sub(5,3)<<endl;    //调用sub函数
return 0;
}
调用dll有两种方式,动态调用(加载)和静态调用(加载),上面的代码用的是静态调用,动态调用下面再给例子。

#pragma comment(lib, "VCDLL.lib")表示加载VCDLL.lib,前面说过,这是一个引入库,它使得程序运行时程序会自动加载它所对应的VCDLL.dll。在调用的文件里,我们还要声明一下add和sub函数,以告诉编译器,add和sub是什么,这里也可以用C语言中的关键字extern来声明,但是__declspec(dllexport)是专门用来声明这个函数是在dll中的,而且用__declspec(dllexport)声明比用extern声明高效。

在这里还需要注意一点,就是VCDLL.dll和VCDLL.lib文件要与DLLCall工程的代码文件在要同一个目录下,否则会报错,找不到.lib文件。

运行结果如下:
TM截图20130601142410

2、类的导出和调用
为了方便管理可以在VCDLL工程中添加一个头文件,dlltest.h,内容如下:
#ifndef _DLLTEST_HEAHER_INCLUDE
#define _DLLTEST_HEAHER_INCLUDE

__declspec(dllexport)int add(int a, int b);
__declspec(dllexport)int sub(int a, int b);
class __declspec(dllexport) CTest
{
public:
CTest(){}
int Max(int a, int b);
};

#endif

则dlltest.cpp文件的代码变为:
#include "dlltest.h"
int add(int a, int b)
{
return a+b;
}
int sub(int a, int b)
{
return a-b;
}


int CTest::Max(int a, int b)
{
return a>b ? a:b;
}
因为在头文件中已经把它们声明为__declspec(dllexport),所以在.cpp文件中可不再重复声明。

用同样的方法查看dll的导出情况如下图所示:
TM截图20130601171204

ordinal为3的就是我们的Max函数,在这里,也可以看出它是属于CTest类的成员函数。这里需要说明一下,就是即使是整个类导出,但是类的成员也同样受类的访问权限的约束。至于哪些奇怪的符号,与之前说的函数改名是一样的。

由于加了一个头文件,代码改动较大,调用dll的代码如下:
#include "stdafx.h"
#include <iostream>
#include "..\VCDLL\dlltest.h"    //注意,根据你的工程的路径改变此路径,此路径不是绝对的

#pragma comment(lib, "VCDLL.lib") //静态加载

using std::cout;
using std::cin;
using std::endl;



int _tmain(int argc, _TCHAR* argv[])
{
cout<<"5+3 = "<<add(5,3)<<endl;
cout<<"5-3 = "<<sub(5,3)<<endl;
CTest ct;            //声明一个对象
cout<<ct.Max(5,3)<<endl;    //调用函数。
return 0;
}
运行结果为:
TM截图20130601174541

如果我把dlltest.h文件中类的声明改成如下声明,结果又会怎么呢?
class  CTest
{
public:
CTest(){}
__declspec(dllexport)int Max(int a, int b);
};

运行结果还是一样的。当我们这样声明时,我们可以创建类的对象,但是我们只能使用导出的成员,没有导出的成员即使是public的,也不能被调用。

3、变量的导出和调用
在dlltest.h内添加代码:__declspec(dllexport)int n = 0;
n为全局变量,查看导出情况如下:ordinal为3的为int n;
TM截图20130601180010

调用文件添加代码:cout<<n<<endl;运行结果为
TM截图20130601180242

这里有个问题,就是如果两个程序同时访问n会发生什么情况呢?我做了一个小试验,两个应该程序分别对n进行加1,发现结果就像只有一个程序在访问这个dll一样。两个程序都产生了相同的结果,输出都为1.为什么会是这样,Windows为了解决这个问题,在共享的数据页里实现了一种叫写入时复制的技术,即一个程序改变了dll中的共享数据的值,系统则会重新分配一个内存空间,存放改变了的数,并把改变这个数据的程序中引用这个数据的指针指向新的值的内存。

4、DLL的动态调用
无论是动态调用还是静态调用,都是调用程序的问题,所以之前写的dll根本不需要改变,下面是把前面的所有功能变成动态调用的代码。
#include "stdafx.h"
#include <iostream>
#include <Windows.h>
#include "..\VCDLL\dlltest.h"

using std::cout;
using std::cin;
using std::endl;


int _tmain(int argc, _TCHAR* argv[])
{
HINSTANCE hInst = NULL;    
hInst = LoadLibrary(_T("VCDLL.dll")); //动态加载dll
if(!hInst)    //加载失败
{
cout<<"load the dll fail!"<<endl;
return 0;
}

typedef int (*FunPtr)(int a,int b);    //定义一个函数指针的类型
FunPtr pAdd = NULL;                    //定义一个函数指针变量
pAdd = (FunPtr)GetProcAddress(hInst,"add");    //获得函数地址
if(pAdd)
{
cout<<"5+3 = "<<(*pAdd)(5,3)<<endl;    //调用函数
}
FunPtr pSub = NULL;
pSub = (FunPtr)GetProcAddress(hInst,"sub");
if(pSub)
{
cout<<"5-3 = "<<(*pSub)(5,3)<<endl;
}
int *pN = NULL;
pN = (int*)GetProcAddress(hInst,"n");
if(pN)
{
cout<<*pN<<endl;
}
FreeLibrary(hInst);    //使dll的引用计数减1,引用计数为0时,释放dll库(即不存在于内存中)

return 0;
}
上面可以看到,没有动态地调用一个类,用动态加载的方法是很难(至少我不知道怎么直接调用一个类),在DLL中导出的类是无法通过动态加载的方式进行使用的,必须在编译时将.LIB文件链接进去。类的成员函数无法单独导出。用GetProcessAddress函数只能导出普通函数,但可以在普通函数中使用DLL中的类和它的方法,因此可以用普通函数将类的公有方法进行包装,然后进行导出,这样就可以用动态加载的方法使用DLL中的类的功能了。

直接运行上面的代码,可以看到什么都什么都没有输出,为什么呢?很简单,因为GetProcAddress找不到名为add等的函数和变量,为什么会找不到呢?还记得前面说过的编译为了支持重载而把函数名根据本编译器的法则把它改名了,所以编译出来的函数名并不是add等,那么有什么办法解决这个问题呢?下面就来说说,这个方法还可以解决上面的不同编译所写的函数互相调用之间的问题。

解决篡改函数名导致问题的方法:
1、在声明导出函数或变量时在前面加上extern “C”(注意C一定要大写),如add函数就会变成extern “C” __declspec(dllexport)int add(int a, int b);这样就告诉编译器编译时以C语言的处理方法来处理函数,那么函数名就为真正的add,但是extern “C”不能用来声明一个类。

2、在dll的工程中新建一个.def的模块定义文件(;号后是注释内容),在里面写上要导出的函数或变量。如下:
LIBRARY VCDLL

EXPORTS
add
sub
n DATA
这两种方法所产生的结果是一样的。用dumpbin查看函数和变量名如下图所示,
TM截图20130602014146
可以看到函数名变成了真正的add、sub和n(ordinal分别为5、7、6),当然这样也是有代价的,就是这两种方法都不能实现函数重载,而且变量名也不能与函数名相同。

注意:
我们知道,C和C++的函数有很多的调用约定,如__stdcall、__cdecl等。我们调用函数所用的函数指针要与dll中的函数声明的调用方式相同,否则会出错。C++的普遍函数的默认调用约定为__cdecl。例如上面的代码的FunPtr如果被定义为一个__stdcall的函数指针,即typedef int (__stdcall *FunPtr)(int a,int b),调用就会出错。

DLL的工程也有一个main函数,叫DLLMain,不过它并不是很重要,若不定义,则自动生成。

5、动态加载与静态加载的比较

动态加载比较灵活,可以在需要的时候才加载进来,即它是在进程产生之后才加载的,但是使用却相对麻烦,而静态加载则在一个工程编译时链接加载的,即在进程产生之前加载的,但相对不灵活,只能满足一般的需求,因为有时可能会加载了许多我们并不用到的dll,但是用起来却相对简单,特别是对类的导出会变得简单很多。


原创粉丝点击