由于最近写DLL文件频繁遇到bug,今天抽空仔细研究一下动态链接库的基础知识。《VC++深入详解》当然是首选参考资料咯。
DLL是和Windows操作系统一起诞生的,其历史悠久,经历了多年的考验,非常值得学习学习。当然这家伙也造成了DLL Hell。(陈皓注:DLL Hell——DLL灾难,就是微软的DLL升级时因为不同版本可能造成应用程序无法运行的灾难,首当其冲的是COM编程,相信大家都知道某些木马或是病毒更改了一些系统的DLL可以导致整个Windows不举,这就是DLL Hell)。不过这也提醒我们了,不了解DLL是很可怕的。
强大的Windows API中所有的函数都包含在DLL中。最重要的有三:
- Kernel32.dll:包含那些用于管理内存、进程和线程的函数,如CreateThread。[Base Services]
- User32.dll:包含那些用于执行用户界面任务的函数,如CreateWindow。[Windows USER]
- GDI32.dll:包含那些用于画图和显示文本的函数。[Graphics Device Interface]
关于静态库和动态库,静态库就像是打包,把所有需要的东西塞进exe,一个exe走遍天下;动态库却是左右逢源,只有在运行时方才加载。我描述的很不专业,但也能看出后者是一种很聪明的做法,但是极容易出问题。但相信大多数程序员都喜欢灵活的方案,DLL就是这样流行起来的,我们可以选择任何语言进行编写DLL,不一定是VC,也可以是VB。(貌似只属于微软的语言有这种权利)而且这也符合高内聚低耦合的设计原则。当然,它还会保证你的程序不会很臃肿。(很难想象如果没有DLL,Windows会成什么样子)。因为一份DLL可以被多个进程共享。以上就是很明显的优势了。
如果想让DLL导出一些函数,需要在函数前加入:_declspec(dllexport) 的导出标示符。书中也介绍了dumpbin命令的使用,对于调试很有帮助。
DLL的加载有两种方式:
- 隐式链接
- 显示加载
关于隐式链接的实现书上实在是讲的有点乱,我梳理了一下。一般来说我们用隐式链接的方式需要三个东西:
- dll
- lib
- 头文件
#include "Dll1.h"
#pragma comment(lib,"Dll1.lib")
然后在需要加载的地方写以上代码即可。这是隐式链接使用dll的通常方式。这类dll应该如何写呢?
首先我们需要声明标示符:_declspec(dllimport)。这玩意比较文明的方式是写在上述的头文件里。
#ifdef DLL1_API
#else
#define DLL1_API _declspec(dllimport)
#endif
然后在每个需要导出的函数前加上DLL1_API就行了,这名字是自己起的。多牛逼都可以,如WINDOWS_API。这样我们在cpp文件里只用加上:
#define DLL1_API _declspec(dllexport)
#include "Dll1.h"
就可以了。这东西眼熟么?不就是上文所说的导出标示符么…
最后一个书中着重讲解的问题,即名字改编问题的解决方案。我觉得最后那种依靠def文件的方式比较合适。这里面有两种函数,一种是普通函数,另一种是类函数。对于类函数需要做以下特殊照顾(这个书上可没写,但我仍旧拿书上代码举例)。
LIBRARY Dll1
EXPORTS
add @1
subtract @2
?output@Point@@QAEXHH@Z @3
?test@Point@@QAEXXZ @4
在def文件里加这几句话,前面和书上一样,最后两行如此丑陋,是因为它们是类函数。而后面接着的@1之类的是自定义编号。那两行丑陋的东西是如何得来的呢?
在VC6下,settings->Link,勾上Generate mapfile。编译后,在debug下有一个.map文件,用记事本打开,找到类似下面这样的语句:
0001:00000030 ?add@@YAHHH@Z 10001030 f Dll1.obj
0001:00000060 ?subtract@@YAHHH@Z 10001060 f Dll1.obj
0001:00000090 ?output@Point@@QAEXHH@Z 10001090 f Dll1.obj
0001:00000180 ?test@Point@@QAEXXZ 10001180 f Dll1.obj
可以知道丑陋之物是哪来的了吧!
下面说说显示加载,也叫做“动态加载方式”。它动在什么地方呢?我觉得在于它的轻便和随叫随到。轻在哪里?它只需要dll,其他都都不需要。随叫随到是指需要时才加载dll,不用了可以关闭。
需要将dll放在工程下,然后写类似下面的代码:
// 动态加载dll
HINSTANCE hInst;
hInst = LoadLibrary("Dll3.dll");
// 定义函数指针类型
typedef int (*ADDPROC)(int a,int b);
// 获取dll导出函数
/*ADDPROC Add = (ADDPROC)GetProcAddress(hInst,"?add@@YAHHH@Z");*/
ADDPROC Add = (ADDPROC)GetProcAddress(hInst,MAKEINTRESOURCE(1));
if (!Add)
{
MessageBox("获取函数地址失败!");
return;
}
需要注重注意两个函数:LoadLibrary(LPCTSTR);和GetProAddress(HMODULE,LPCSTR);前者导入dll(隐式链接其实也是调用这个函数来实现的),后者获取导出函数。参数一为前者的返回值,参数二有两种方案:注释的那种是通常的方法,获取函数名。可以看到这里选取的函数名与上文中丑陋之物是一个东西,OK,这就是函数名。要保持一致。另外如果你得知函数的编号,就是上文中所述@1的字样,那么可以用MAKEINTRESOURCE宏实现转换。
如果不再使用该dll,应该释放dll。如下:
FreeLibrary(hInst);
好了,动态加载其实就这么多内容,其实与隐式链接有很多想通之处。如果你的程序里大量使用dll,隐式链接一次性搞定的方案比较适合你,但如果只是偶尔一用(如曾经我在VC6里想使用CImage类的时候,在2008做了一个dll供VC6调用),那么还是推荐显示加载。
如何选择取决于你的实际情况,永远不要脱离上下文编程!
关于MFC DLL的内容只是略看了一下,毕竟MFC这个古老的东西日后必然会离我们而去的…