DLL编写教程

  • Post author:
  • Post category:其他



DLL


编写教程


半年不能上网,最近网络终于通了,终于可以更新博客了,写点什么呢?决定最近写一个编程技术系列,其内容是一些通用的编程技术。例如

DLL



COM



Socket

,多线程等等。这些技术的特点就是使用广泛,但是误解很多;网上教程很多,但是几乎没有什么优质良品。我以近几个月来的编程经验发现,很有必要好好的总结一下这些编程技术了。一来对自己是总结提高,二来可以方便光顾我博客的朋友。


好了,废话少说,言归正传。第一篇就是《

DLL

编写教程》,为什么起这么土的名字呢?为什么不叫《轻轻松松写

DLL

》或者《

DLL

一日通》呢?或者更

nb

的《深入简出

DLL

》呢?呵呵,常常上网搜索资料的弟兄自然知道。


本文对通用的

DLL

技术做了一个总结,并提供了源代码打包下载,下载地址为:


http://www.blogjava.net/Files/wxb_nudt/DLL_SRC.rar

DLL

的优点


简单的说,

dll

有以下几个优点:

1)



节省内存。同一个软件模块,若是以源代码的形式重用,则会被编译到不同的可执行程序中,同时运行这些

exe

时这些模块的二进制码会被重复加载到内存中。如果使用

dll

,则只在内存中加载一次,所有使用该

dll

的进程会共享此块内存(当然,像

dll

中的全局变量这种东西是会被每个进程复制一份的)。

2)



不需编译的软件系统升级,若一个软件系统使用了

dll

,则该

dll

被改变(函数名不变)时,系统升级只需要更换此

dll

即可,不需要重新编译整个系统。事实上,很多软件都是以这种方式升级的。例如我们经常玩的星际、魔兽等游戏也是这样进行版本升级的。

3)


Dll

库可以供多种编程语言使用,例如用

c

编写的

dll

可以在

vb

中调用。这一点上

DLL

还做得很不够,因此在

dll

的基础上发明了

COM

技术,更好的解决了一系列问题。


最简单的

dll


开始写

dll

之前,你需要一个

c/c++

编译器和链接器,并关闭你的

IDE

。是的,把你的

VC



C++ BUILDER

之类的东东都关掉,并打开你以往只用来记电话的记事本程序。不这样做的话,你可能一辈子也不明白

dll

的真谛。我使用了

VC

自带的

cl

编译器和

link

链接器,它们一般都在

vc



bin

目录下。(若你没有在安装

vc

的时候选择注册环境变量,那么就立刻将它们的路径加入

path

吧)如果你还是因为离开了

IDE

而害怕到哭泣的话,你可以关闭这个页面并继续去看《

VC++

技术内幕》之类无聊的书了。


最简单的

dll

并不比

c



helloworld

难,只要一个

DllMain

函数即可,包含

objbase.h

头文件(支持

COM

技术的一个头文件)。若你觉得这个头文件名字难记,那么用

windows.H

也可以。源代码如下:

dll_nolib.cpp

#include <objbase.h>

#include <iostream.h>

BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)

{

HANDLE g_hModule;

switch(dwReason)

{

case DLL_PROCESS_ATTACH:

cout<<“Dll is attached!”<<endl;

g_hModule = (HINSTANCE)hModule;

break;

case DLL_PROCESS_DETACH:

cout<<“Dll is detached!”<<endl;

g_hModule=NULL;

break;

}

return true;

}


其中

DllMain

是每个

dll

的入口函数,如同

c



main

函数一样。

DllMain

带有三个参数,

hModule

表示本

dll

的实例句柄(听不懂就不理它,写过

windows

程序的自然懂),

dwReason

表示

dll

当前所处的状态,例如

DLL_PROCESS_ATTACH

表示

dll

刚刚被加载到一个进程中,

DLL_PROCESS_DETACH

表示

dll

刚刚从一个进程中卸载。当然还有表示加载到线程中和从线程中卸载的状态,这里省略。最后一个参数是一个保留参数(目前和

dll

的一些状态相关,但是很少使用)。


从上面的程序可以看出,当

dll

被加载到一个进程中时,

dll

打印

“Dll is attached!”

语句;当

dll

从进程中卸载时,打印

“Dll is detached!”

语句。


编译

dll

需要以下两条命令:

cl /c dll_nolib.cpp


这条命令会将

cpp

编译为

obj

文件,若不使用

/c

参数则

cl

还会试图继续将

obj

链接为

exe

,但是这里是一个

dll

,没有

main

函数,因此会报错。不要紧,继续使用链接命令。

Link /dll dll_nolib.obj


这条命令会生成

dll_nolib.dll



注意,因为编译命令比较简单,所以本文不讨论

nmake

,有兴趣的可以使用

nmake

,或者写个

bat

批处理来编译链接

dll



加载

DLL

(显式调用)


使用

dll

大体上有两种方式,显式调用和隐式调用。这里首先介绍显式调用。编写一个客户端程序:

dll_nolib_client.cpp

#include <windows.h>

#include <iostream.h>

int main(void)

{

//

加载我们的

dll

HINSTANCE hinst=::LoadLibrary(“dll_nolib.dll”);

if (NULL != hinst)

{

cout<<“dll loaded!”<<endl;

}

return 0;

}


注意,调用

dll

使用

LoadLibrary

函数,它的参数就是

dll

的路径和名称,返回值是

dll

的句柄。


使用如下命令编译链接客户端:

Cl dll_nolib_client.cpp


并执行

dll_nolib_client.exe

,得到如下结果:

Dll is attached!

dll loaded!

Dll is detached!


以上结果表明

dll

已经被客户端加载过。但是这样仅仅能够将

dll

加载到内存,不能找到

dll

中的函数。


使用

dumpbin

命令查看

DLL

中的函数

Dumpbin

命令可以查看一个

dll

中的输出函数符号名,键入如下命令:

Dumpbin –exports dll_nolib.dll


通过查看,发现

dll_nolib.dll

并没有输出任何函数。


如何在

dll

中定义输出函数


总体来说有两种方法,一种是添加一个

def

定义文件,在此文件中定义

dll

中要输出的函数;第二种是在源代码中待输出的函数前加上

__declspec(dllexport)

关键字。

Def

文件


首先写一个带有输出函数的

dll

,源代码如下:

dll_def.cpp

#include <objbase.h>

#include <iostream.h>

void FuncInDll (void)

{

cout<<“FuncInDll is called!”<<endl;

}

BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)

{

HANDLE g_hModule;

switch(dwReason)

{

case DLL_PROCESS_ATTACH:

g_hModule = (HINSTANCE)hModule;

break;

case DLL_PROCESS_DETACH:

g_hModule=NULL;

break;

}

return TRUE;

}


这个

dll



def

文件如下:

dll_def.def

;

; dll_def module-definition file

;

LIBRARY         dll_def.dll

DESCRIPTION     ‘(c)2007-2009 Wang Xuebin’

EXPORTS

FuncInDll @1 PRIVATE


你会发现

def

的语法很简单,首先是

LIBRARY

关键字,指定

dll

的名字;然后一个可选的关键字

DESCRIPTION

,后面写上版权等信息(不写也可以);最后是

EXPORTS

关键字,后面写上

dll

中所有要输出的函数名或变量名,然后接上

@

以及依次编号的数字(从

1



N

),最后接上修饰符。


用如下命令编译链接带有

def

文件的

dll


Cl /c dll_def.cpp

Link /dll dll_def.obj /def:dll_def.def


再调用

dumpbin

查看生成的

dll_def.dll


Dumpbin –exports dll_def.dll


得到如下结果:

Dump of file dll_def.dll

File Type: DLL

Section contains the following exports for dll_def.dll

0 characteristics

46E4EE98 time date stamp Mon Sep 10 15:13:28 2007

0.00 version

1 ordinal base

1 number of functions

1 number of names

ordinal hint RVA      name

1    0 00001000 FuncInDll

Summary

2000 .data

1000 .rdata

1000 .reloc

6000 .text


观察这一行

1    0 00001000 FuncInDll


会发现该

dll

输出了函数

FuncInDll



显式调用

DLL

中的函数


写一个

dll_def.dll

的客户端程序:

dll_def_client.cpp

#include <windows.h>

#include <iostream.h>

int main(void)

{

//

定义一个函数指针

typedef void (* DLLWITHLIB )(void);

//

定义一个函数指针变量

DLLWITHLIB pfFuncInDll = NULL;

//

加载我们的

dll

HINSTANCE hinst=::LoadLibrary(“dll_def.dll”);

if (NULL != hinst)

{

cout<<“dll loaded!”<<endl;

}

//

找到

dll



FuncInDll

函数

pfFuncInDll = (DLLWITHLIB)GetProcAddress(hinst, “FuncInDll”);

//

调用

dll

里的函数

if (NULL != pfFuncInDll)

{

(*pfFuncInDll)();

}

return 0;

}


有两个地方值得注意,第一是函数指针的定义和使用,不懂的随便找本

c++

书看看;第二是

GetProcAddress

的使用,这个

API

是用来查找

dll

中的函数地址的,第一个参数是

DLL

的句柄,即

LoadLibrary

返回的句柄,第二个参数是

dll

中的函数名称,即

dumpbin

中输出的函数名(注意,这里的函数名称指的是编译后的函数名,不一定等于

dll

源代码中的函数名)。


编译链接这个客户端程序,并执行会得到:

dll loaded!

FuncInDll is called!


这表明客户端成功调用了

dll

中的函数

FuncInDll


__declspec(dllexport)


为每个

dll



def

显得很繁杂,目前

def

使用已经比较少了,更多的是使用

__declspec(dllexport)

在源代码中定义

dll

的输出函数。

Dll

写法同上,去掉

def

文件,并在每个要输出的函数前面加上声明

__declspec(dllexport)

,例如:

__declspec(dllexport) void FuncInDll (void)


这里提供一个

dll

源程序

dll_withlib.cpp

,然后编译链接。链接时不需要指定

/DEF:

参数,直接加

/DLL

参数即可,

Cl /c dll_withlib.cpp

Link /dll dll_withlib.obj


然后使用

dumpbin

命令查看,得到:

1    0 00001000 ?FuncInDll@@YAXXZ


可知编译后的函数名为

?FuncInDll@@YAXXZ

,而并不是

FuncInDll

,这是因为

c++

编译器基于函数重载的考虑,会更改函数名,这样使用显式调用的时候,也必须使用这个更改后的函数名,这显然给客户带来麻烦。为了避免这种现象,可以使用

extern “C”

指令来命令

c++

编译器以

c

编译器的方式来命名该函数。修改后的函数声明为:

extern “C” __declspec(dllexport) void FuncInDll (void)

dumpbin

命令结果:

1    0 00001000 FuncInDll


这样,显式调用时只需查找函数名为

FuncInDll

的函数即可成功。

extern “C”


使用

extern “C”

关键字实际上相当于一个编译器的开关,它可以将

c++

语言的函数编译为

c

语言的函数名称。即保持编译后的函数符号名等于源代码中的函数名称。


隐式调用

DLL


显式调用显得非常复杂,每次都要

LoadLibrary

,并且每个函数都必须使用

GetProcAddress

来得到函数指针,这对于大量使用

dll

函数的客户是一种困扰。而隐式调用能够像使用

c

函数库一样使用

dll

中的函数,非常方便快捷。


下面是一个隐式调用的例子:

dll

包含两个文件

dll_withlibAndH.cpp



dll_withlibAndH.h



代码如下:

dll_withlibAndH.h

extern “C” __declspec(dllexport) void FuncInDll (void);

dll_withlibAndH.cpp

#include <objbase.h>

#include <iostream.h>

#include “dll_withLibAndH.h”//

看到没有,这就是我们增加的头文件

extern “C” __declspec(dllexport) void FuncInDll (void)

{

cout<<“FuncInDll is called!”<<endl;

}

BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)

{

HANDLE g_hModule;

switch(dwReason)

{

case DLL_PROCESS_ATTACH:

g_hModule = (HINSTANCE)hModule;

break;

case DLL_PROCESS_DETACH:

g_hModule=NULL;

break;

}

return TRUE;

}


编译链接命令:

Cl /c dll_withlibAndH.cpp

Link /dll dll_withlibAndH.obj


在进行隐式调用的时候需要在客户端引入头文件,并在链接时指明

dll

对应的

lib

文件(

dll

只要有函数输出,则链接的时候会产生一个与

dll

同名的

lib

文件)位置和名称。然后如同调用

api

函数库中的函数一样调用

dll

中的函数,不需要显式的

LoadLibrary



GetProcAddress

。使用最为方便。客户端代码如下:

dll_withlibAndH_client.cpp

#include “dll_withLibAndH.h”

//

注意路径,加载

dll

的另一种方法是

Project | setting | link

设置里

#pragma comment(lib,”dll_withLibAndH.lib”)

int main(void)

{

FuncInDll();//

只要这样我们就可以调用

dll

里的函数了

return 0;

}

__declspec(dllexport)



__declspec(dllimport)

配对使用


上面一种隐式调用的方法很不错,但是在调用

DLL

中的对象和重载函数时会出现问题。因为使用

extern “C”

修饰了输出函数,因此重载函数肯定是会出问题的,因为它们都将被编译为同一个输出符号串(

c

语言是不支持重载的)。


事实上不使用

extern “C”

是可行的,这时函数会被编译为

c++

符号串,例如(

?FuncInDll@@YAXH@Z



?FuncInDll@@YAXXZ

),当客户端也是

c++

时,也能正确的隐式调用。


这时要考虑一个情况:若

DLL1.CPP

是源,

DLL2.CPP

使用了

DLL1

中的函数,但同时

DLL2

也是一个

DLL

,也要输出一些函数供

Client.CPP

使用。那么在

DLL2

中如何声明所有的函数,其中包含了从

DLL1

中引入的函数,还包括自己要输出的函数。这个时候就需要同时使用

__declspec(dllexport)



__declspec(dllimport)

了。前者用来修饰本

dll

中的输出函数,后者用来修饰从其它

dll

中引入的函数。


所有的源代码包括

DLL1.H



DLL1.CPP



DLL2.H



DLL2.CPP



Client.cpp

。源代码可以在下载的包中找到。你可以编译链接并运行试试。


值得关注的是

DLL1



DLL2

中都使用的一个编码方法,见

DLL2.H

#ifdef DLL_DLL2_EXPORTS

#define DLL_DLL2_API __declspec(dllexport)

#else

#define DLL_DLL2_API __declspec(dllimport)

#endif

DLL_DLL2_API void FuncInDll2(void);

DLL_DLL2_API void FuncInDll2(int);


在头文件中以这种方式定义宏

DLL_DLL2_EXPORTS



DLL_DLL2_API

,可以确保

DLL

端的函数用

__declspec(dllexport)

修饰,而客户端的函数用

__declspec(dllimport)

修饰。当然,记得在编译

dll

时加上参数

/D “DLL_DLL2_EXPORTS”

,或者干脆就在

dll



cpp

文件第一行加上

#define DLL_DLL2_EXPORTS


VC

生成的代码也是这样的!事实证明,我是抄袭它的,

hoho


DLL

中的全局变量和对象


解决了重载函数的问题,那么

dll

中的全局变量和对象都不是问题了,只是有一点语法需要注意。如源代码所示:

dll_object.h

#ifdef DLL_OBJECT_EXPORTS

#define DLL_OBJECT_API __declspec(dllexport)

#else

#define DLL_OBJECT_API __declspec(dllimport)

#endif

DLL_OBJECT_API void FuncInDll(void);

extern DLL_OBJECT_API int g_nDll;

class DLL_OBJECT_API CDll_Object {

public:

CDll_Object(void);

show(void);

// TODO: add your methods here.

};

Cpp

文件

dll_object.cpp

如下:

#define DLL_OBJECT_EXPORTS

#include <objbase.h>

#include <iostream.h>

#include “dll_object.h”

DLL_OBJECT_API void FuncInDll(void)

{

cout<<“FuncInDll is called!”<<endl;

}

DLL_OBJECT_API int g_nDll = 9;

CDll_Object::CDll_Object()

{

cout<<“ctor of CDll_Object”<<endl;

}

CDll_Object::show()

{

cout<<“function show in class CDll_Object”<<endl;

}

BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)

{

HANDLE g_hModule;

switch(dwReason)

{

case DLL_PROCESS_ATTACH:

g_hModule = (HINSTANCE)hModule;

break;

case DLL_PROCESS_DETACH:

g_hModule=NULL;

break;

}

return TRUE;

}


编译链接完后

Dumpbin

一下,可以看到输出了

5

个符号:

1    0 00001040 ??0CDll_Object@@QAE@XZ

2    1 00001000 ??4CDll_Object@@QAEAAV0@ABV0@@Z

3    2 00001020 ?FuncInDll@@YAXXZ

4    3 00008040 ?g_nDll@@3HA

5    4 00001069 ?show@CDll_Object@@QAEHXZ


它们分别代表类

CDll_Object

,类的构造函数,

FuncInDll

函数,全局变量

g_nDll

和类的成员函数

show

。下面是客户端代码:

dll_object_client.cpp

#include “dll_object.h”

#include <iostream.h>

//

注意路径,加载

dll

的另一种方法是

Project | setting | link

设置里

#pragma comment(lib,”dll_object.lib”)

int main(void)

{

cout<<“call dll”<<endl;

cout<<“call function in dll”<<endl;

FuncInDll();//

只要这样我们就可以调用

dll

里的函数了

cout<<“global var in dll g_nDll =”<<g_nDll<<endl;

cout<<“call member function of class CDll_Object in dll”<<endl;

CDll_Object obj;

obj.show();

return 0;

}


运行这个客户端可以看到:

call dll

call function in dll

FuncInDll is called!

global var in dll g_nDll =9

call member function of class CDll_Object in dll

ctor of CDll_Object

function show in class CDll_Object


可知,在客户端成功的访问了

dll

中的全局变量,并创建了

dll

中定义的

C++

对象,还调用了该对象的成员函数。


中间的小结


牢记一点,说到底,

DLL

是对应

C

语言的动态链接技术,在输出

C

函数和变量时显得方便快捷;而在输出

C++

类、函数时需要通过各种手段,而且也并没有完美的解决方案,除非客户端也是

c++



记住,只有

COM

是对应

C++

语言的技术。


下面开始对各各问题一一小结。


显式调用和隐式调用


何时使用显式调用?何时使用隐式调用?我认为,只有一个时候使用显式调用是合理的,就是当客户端不是

C/C++

的时候。这时是无法隐式调用的。例如用

VB

调用

C++

写的

dll

。(

VB

我不会,所以没有例子)

Def



__declspec(dllexport)


其实

def

的功能相当于

extern “C” __declspec(dllexport)

,所以它也仅能处理

C

函数,而不能处理重载函数。而

__declspec(dllexport)



__declspec(dllimport)

配合使用能够适应任何情况,因此

__declspec(dllexport)

是更为先进的方法。所以,目前普遍的看法是不使用

def

文件,我也同意这个看法。


从其它语言调用

DLL


从其它编程语言中调用

DLL

,有两个最大的问题,第一个就是函数符号的问题,前面已经多次提过了。这里有个两难选择,若使用

extern “C”

,则函数名称保持不变,调用较方便,但是不支持函数重载等一系列

c++

功能;若不使用

extern “C”

,则调用前要查看编译后的符号,非常不方便。


第二个问题就是函数调用压栈顺序的问题,即

__cdecl



__stdcall

的问题。

__cdecl

是常规的

C/C++

调用约定,这种调用约定下,函数调用后栈的清理工作是由调用者完成的。

__stdcall

是标准的调用约定,即这些函数将在返回到调用者之前将参数从栈中删除。


这两个问题

DLL

都不能很好的解决,只能说凑合着用。但是在

COM

中,都得到了完美的解决。所以,要在

Windows

平台实现语言无关性,还是只有使用

COM

中间件。


总而言之,除非客户端也使用

C++

,否则

dll

是不便于支持函数重载、类等

c++

特性的。

DLL



c

函数的支持很好,我想这也是为什么

windows

的函数库使用

C



dll

实现的理由之一。




VC

中编写

DLL




VC

中创建、编译、链接

dll

是非常方便的,点击

file

à

New

à

Project

à

Win32 Dynamic-Link Library

,输入

dll

名称

dll_InVC

然后点击确定。然后选择

A DLL that export some symbols

,点击

Finish

。即可得到一个完整的

DLL



仔细观察其源代码,是不是有很多地方似曾相识啊,哈哈!


转载处:http://www.blogjava.net/wxb_nudt/archive/2007/09/11/144371.html