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.armjishu.com/bbs/viewtopic.php?id=1046]
相关文章
相关标签/搜索