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