三年多前我在blog.csdn.net上半转载过一篇文章,关于DLL入门的,不过内容有些凌乱,加上非原创,我打算重新写一下,更突出“快速”,内容嘛,就比较精简了,虽然精简,看完后写写DLL肯定是没问题的,相信我。
一,快速生成一个DLL
哦,对了,讲解还是用VC++ 6.0(简称VC6)来讲解。在VC6下new一个“Win32 Dynamic-Link Library”的Project,就叫“dllTest”吧,注意不要选择“MFC AppWizard(dll)”,因为……这不属于本文的内容,其实最重要的是:我不太懂(^_^),然后呢,选择“An empty DLL project”即可。
接下来就是创建以这两个文件,并添加到Project中去:
lib.h
#ifndef LIB_H
#define LIB_H
extern "C" int __declspec(dllexport) add(int x,int y);
#endif
lib.cpp
#include "lib.h"
int add(int x,int y)
{
return x + y;
}
Build!(这么简单的程序不会通不过吧?)你就能看到你的生成目录下有个文件,叫“dllTest.dll”,这就是我们第一个dll,没几行代码,简单吧,但麻雀虽小五脏俱全,我们使用Visual Studio提供的工具“Depends”打开这个文件看看,如图所示,你能看到我们导出的这个函数,add。
这个超微型的dll和普通程序的不同在于add函数的声明,多了一个__declspec(dllexport),这个是关键,这个修饰符告诉VC6,这个函数需要导出,而前面的extern "C"的意思是这个函数是以C语言的标准来调用的,如果不加extern "C"的话,会怎么样呢?自己试试看吧,Build后再用Depends看看生成的dll。
![](http://www.cppblog.com/images/cppblog_com/guogangj/fast_dll_1.png)
二、使用这个DLL
用VC6的向导创建一个“Win32 Console Application”的Project,叫“CallDllTest”,然后选择“"A Hello, Word!"Application”,为什么用Console?简单呗。
把CallDllTest.cpp的内容换成以下:
CallDllTest.cpp
#include "stdafx.h"
#include "windows.h"
typedef int ( * lpAddFun)(int,int);
int main(int argc, char* argv[])
{
HINSTANCE hDll; //DLL handle
lpAddFun addFun; //Function pointer
hDll = LoadLibrary("dllTest.dll");
if (hDll != NULL)
{
addFun = (lpAddFun)GetProcAddress(hDll,"add");
if(addFun!=NULL)
{
int result = addFun(2,3);
printf("%d",result);
}
FreeLibrary(hDll);
}
return 0;
}
Build,然后<Ctrl>+<F5>运行,就能看到结果了,打印了一个“5”出来,说明正常了,有点需要说明的是你得把前面生产的那个DLL和这次生成的EXE放在同一目录下方可。代码很简单,思路就是定义函数指针类型,加载DLL,获取要调用的函数的指针,然后调用,这么一个过程。
三、静态链接
刚才使用LoadLibrary这个API加载DLL的方式叫动态链接,现在来介绍静态链接,其实静态链接用得还更多,只不过你不一定注意到而已,不信的话现在查看一下你刚创建的CallDllTest这个Project的Project Settings,如图:
![](http://www.cppblog.com/images/cppblog_com/guogangj/fast_dll_2.PNG)
注意我画红线的这个地方,Kernel32.lib,user32.lib和gdi32.lib等,这些lib叫做“导入库”,并不包含真正有效的代码,真正有效的代码是存放在DLL中的,刚才我们所使用的LoadLibrary这个API,其实就是通过Kernel32.lib链接到Kernel32.dll中去的,也就是说LoadLibrary的实现存在于Kernel32.dll中,而我们是通过Kernel32.lib来找到它的,如果还有兴趣的话,可以用Depends看看Kernel32.dll,看看里面是否有LoadLibrary,里面函数很多,别看花眼了哦,这可是Windows的核心库之一,但可能你并没有找到LoadLibrary,而是找到了,LoadLibraryA和LoadLibraryW,这很正常,因为很多需要使用到字符串的API,都有两个版本,一个是窄字符版,一个是宽字符版,在程序编译链接的时候,编译器会根据你的选项来跟你选择其中一个,这里暂时不展开了。
那如何静态使用前面生成的那个dllTest.dll呢?回到刚才dllTest.dll的那个生成目录,你会发现一个叫dllTest.lib的文件,这就是我前面提到“导入库”,现在改一下CallDllTest.cpp。
CallDllTest.cpp
#include "stdafx.h"
#pragma comment(lib, "dllTest.lib")
extern "C" int __declspec(dllimport) add(int x,int y);
int main(int argc, char* argv[])
{
int result = add(2,3);
printf("%d",result);
return 0;
}
dllTest.lib这个文件得复制到CallDllTest这个Project的目录下,否则会报找不到文件,执行结果如何?跟刚才是一样的,这种方式就叫静态链接,区别是什么?不需要在运行时调用LoadLibrary了,我个人觉得静态链接用得更多一些。
注意看代码:
extern "C" int __declspec(dllimport) add(int x,int y);
这句话很关键,__declspec(dllimport)对应了DllTest中的__declspec(dllexport),表示该函数将从dll中导入,那你也许要问了:“我怎么知道这个dll中有这个函数?并且还知道这个函数的参数类型和返回值类型?该不会也是用Depends去看吧?”呃……这怎么说呢?如果这个dll是你写的,你当然知道啦,但如果这个Dll不是你写的话,它的作者往往会提供一个头文件,就好像你要使用LoadLibrary,你得包含“windows.h”这个头文件一样,否则就出现符号未定义的编译错误,那么我们改一下lib.h这个头文件。
lib.h
#ifndef LIB_H
#define LIB_H
#ifdef DLLTEST_EXPORTS
extern "C" int __declspec(dllexport) add(int x,int y);
#else
extern "C" int __declspec(dllimport) add(int x,int y);
#endif
#endif
在DllTest这个Project中,DLLTEST_EXPORTS是被定义了的,如图:
![](http://www.cppblog.com/images/cppblog_com/guogangj/fast_dll_3.png)
所以使用dllexport,而在别的Project中,则使用dllimport。在CallDllTest中include这个lib.h,就可以了,当然你也可以写得更好,我这里仅仅是demo。
四、DLL中的main函数
大家都知道C语言的程序是从main开始的,到了Windows环境下,则换成了WinMain,但也差不多,那DLL有没有类似的入口呢?答案是肯定的,我们来改一下DllTest的lib.cpp。
lib.cpp
#include "lib.h"
#include "windows.h"
#include "stdio.h"
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
printf("\nprocess attach of DLL");
break;
case DLL_THREAD_ATTACH:
printf("\nthread attach of DLL");
break;
case DLL_THREAD_DETACH:
printf("\nthread detach of DLL");
break;
case DLL_PROCESS_DETACH:
printf("\nprocess detach of DLL");
break;
}
return TRUE;
}
int add(int x,int y)
{
return x + y;
}
你能看到一个叫“DllMain”的函数,它就是dll的入口,oh,当然了,我这篇文章所讲的都是针对Windows操作系统的,Linux的可不一样哦,甚至一般来说,Linux的DLL都不叫“DLL”。
好了,你再按照前面的方法去调用下这个DLL,(记得拷贝这个dll到相应目录去)这时候你就能看到执行结果中多了“process attach of DLL”和“process detach of DLL”,这是很显而易见的,一个进程连接和断开连接到这个dll的时候,DllMain就会被调用,且传递的ul_reason_for_call参数分别是DLL_PROCESS_ATTACH和DLL_PROCESS_DETACH,那什么时候会有“DLL_THREAD_ATTACH”和“DLL_THREAD_DETACH”?当库已经加载,创建新的线程和销毁线程的时候,会分别使用THREAD_ATTACH和THREAD_DETACH参数来调DllMain。
五、调用方式
前面我们导入,导出函数的时候都加了一个“extern "C"”,那不加会怎么样呢?如果再涉及到这几个修饰符:__stdcall,__cdecl和__fastcall,那又会怎样呢?我画了两个表,大家可以比较下,体会下。
这是三种不同调用方式的比较:
![](http://www.cppblog.com/images/cppblog_com/guogangj/fast_dll_4.png)
这是命名修饰在不同方式下的比较:
![](http://www.cppblog.com/images/cppblog_com/guogangj/fast_dll_5.png)
如果dll和exe的命名理解不一致,就有可能出错。通常来说,我是习惯于用“extern "C"”和“__cdecl”的组合。
六、导出变量
前面只说了如何导出函数,那如何导出一个变量呢?方法类似,甚至可以说几乎一样,看代码:
lib.cpp
#include "lib.h"
#include "windows.h"
#include "stdio.h"
int iExportInt;
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
iExportInt = 112;
break;
}
return TRUE;
}
lib.h
#ifndef LIB_H
#define LIB_H
#ifdef DLLTEST_EXPORTS
extern "C" int __declspec(dllexport) iExportInt;
#else
extern "C" int __declspec(dllimport) iExportInt;
#endif
#endif
这样,就可以了,和导出函数是没什么差别吧?同样,你也可以用Depends观察生成的DLL,你会发现iExportInt这个导出符号,也就是我们导出的这个变量了。现在看CallDllTest的代码:
CallDllTest.cpp
#include "stdafx.h"
#include "lib.h"
#pragma comment(lib, "dllTest.lib")
int main(int argc, char* argv[])
{
printf("%d\n", iExportInt);
iExportInt++;
printf("%d\n", iExportInt);
return 0;
}
输出是什么?112和113,说明成功了。不知道你这个时候有没有想到一个问题,那就是如果两个进程同时调用dllTest.dll,而且同时修改和读取iExportInt,那会不会乱掉呢?要不要做一个互斥锁呢?答案是:不会,不需要。这得益于Windows内存管理的一个底层实现技术,叫Copy-on-write,在调用执行“iExportInt++”的时候,其实并非真正修改了DLL中的值,而是做了一份拷贝,通过内存映射机制,使得程序访问的那个“iExportInt”其实是那份拷贝,而另一进程使用的也是自己的拷贝,互不干涉。
七、共享内存
紧接着前面这个问题,那如果我企图通过DLL来让不同进程共享一段内存,而不是让系统执行默认的Copy-on-write操作,那怎么办呢?有办法!这是Microsoft提供的一个方法,个人觉得是个不错的进程间通信的方法,比如两个进程需要交换一大块数据,而且这一大块数据变化比较频繁,通过数据库啊,文件啊,就显得有点慢,如果通过socket啊,管道啊,就显得有些不直接,还是直接使用共享内存来得直接,但很多人并不知道这个功能,我这里跟大家分享下。
lib.h
#ifndef LIB_H
#define LIB_H
#ifdef DLLTEST_EXPORTS
extern "C" __declspec(dllexport) unsigned char dataChunk[100000];
#else
extern "C" __declspec(dllimport) unsigned char dataChunk[100000];
#endif
#endif
lib.cpp
#include "lib.h"
#pragma data_seg("shared")
unsigned char dataChunk[100000]={0};
#pragma data_seg()
#pragma comment(linker, "/SECTION:shared,RWS")
Build一下,然后用Depends看看输出情况。现在来改CallDllText.cpp。这里有非常要注意的两个地方,一是“={0}”这个初始化,这是必须的,否则共享将不起作用,Microsoft规定了共享段一定需要先初始化,哪怕只是给第一个元素赋个随便什么值都好,如这个例子,我只是给第一个元素赋值了个0;另一处要注意的是“/SECTION:shared,RWS"这个字符串,中间可别要有空格,否则你同样会发现共享不起作用,我当时调试就很郁闷,因为我习惯在英文逗号后加个空格。
CallDllText.cpp
#include "stdafx.h"
#include "windows.h"
#include "lib.h"
#pragma comment(lib, "dllTest.lib")
int main(int argc, char* argv[])
{
printf("%d\n", dataChunk[0]++);
Sleep(10000);
return 0;
}
Sleep(10000)会让程序堵塞10秒钟,这样就可以运行多个程序的副本,来观察共享的效果。
八、结束
DLL涵盖的知识面相当广,本文只是篇入门级的文章,介绍了一些比较实用的内容而已,如果要进一步学习,需要看看《Windows核心编程》这种经典著作,关于DLL的很多内容我都没有提及到,比如DLL的导出方法其实有好几种,我介绍的只是其中一种,但我认为我介绍的方法是最好用而且是最简单的。我们写程序,是为了实现某些应用,而不是为了炫耀某些技术,所以我是偏向于使用成熟,可靠和易行的方法。