最近研究在DLL中导出类,探寻最佳的DLL导出类的方法和技术。把整个过程记录一下,防止遗忘。
动态链接库(英语:Dynamic-link library,缩写为DLL)是微软公司在微软视窗操作系统中实现共享函数库概念的一种实现方式。这些库函数的扩展名是.DLL、.OCX(包含ActiveX控制的库)或者.DRV(旧式的系统驱动程序)。
所谓动态链接,就是把一些经常会共用的代码(静态链接的OBJ程序库)制作成DLL档,当可执行文件调用到DLL档内的函数时,Windows操作系统才会把DLL档加载存储器内,DLL档本身的结构就是可执行档,当程序有需求时函数才进行链接。通过动态链接方式,存储器浪费的情形将可大幅降低。静态链接库则是直接链接到可执行文件。
使用DLL导出C函数或全局变量很简单,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// 下列 ifdef 块是创建使从 DLL 导出更简单的 // 宏的标准方法。此 DLL 中的所有文件都是用命令行上定义的 DLLDEMO_EXPORTS // 符号编译的。在使用此 DLL 的 // 任何其他项目上不应定义此符号。这样,源文件中包含此文件的任何其他项目都会将 // DLLDEMO_API 函数视为是从 DLL 导入的,而此 DLL 则将用此宏定义的 // 符号视为是被导出的。 #ifdef DLLDEMO_EXPORTS #define DLLDEMO_API __declspec(dllexport) #else #define DLLDEMO_API __declspec(dllimport) #endif extern "C" extern DLLDEMO_API int nDllDemo; //不使用extern "C"将导致函数名字改编 DLLDEMO_API int fnDllDemo(int); extern "C" DLLDEMO_API int fnExternCDllDemo(int); |
进程/线程加载时,可以通过DllMain函数通知DLL相关信息,提供对应处理的机会。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
BOOL WINAPI DLLMain(HINSTANCE hinstDLL,DWORD fdwReason,LPVOID fImpLoad) { switch(fdwReason) { case DLL_PROCESS_ATTACH: //当这个DLL第一次被映射到了这个进程的地址空间时。DLLMain函数的返回值为FALSE,说明DLL的初始化没有成功,系统就会终结整个进程,去掉所有文件映象,之后显示一个对话框告诉用户进程不能启动。 break; case DLL_THREAD_ATTACH: //一个线程被创建,新创建的线程负责执行这次的DllMain函数。系统不会让进程已经存在的线程以DLL_THREAD_ATTACH的值来调用DllMain函数。主线程永远不会以DLL_THREAD_ATTACH的值来调用DllMain函数。系统是顺序调用DllMain函数的,一个线程执行完DllMain函数才会让另外一个线程执行DllMain函数。 break; case DLL_THREAD_DETACH: //如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread)。线程调用了TerminateThread,系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数。 break; case DLL_PROCESS_DETACH: //这个DLL从进程的地址空间中解除映射。如果进程的终结是因为调用了TerminateProcess,系统就不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。 break; } return(TRUE); } |
采用静态调用方法,DLL最终将打包到生成的EXE中。静态调用方法步骤如下[2]:
动态调用DLL的步骤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
int main() { HMODULE hModule = LoadLibrary(_T("DllDemo.dll")); typedef int(*TYPE_fnDllDemo) (int);//定义函数指针 typedef int(*TYPE_fnExternCDllDemo) (int);//定义函数指针 //创建类对象 CDllDemo* pCDllDemo = (CDllDemo*)malloc(sizeof(CDllDemo)); TYPE_fnDllDemo fnDllDemo = (TYPE_fnDllDemo)GetProcAddress(hModule, "?fnDllDemo@@YAHH@Z"); int *nDllDemo = (int *)GetProcAddress(hModule, "nDllDemo"); TYPE_fnExternCDllDemo fnExternCDllDemo = (TYPE_fnExternCDllDemo)GetProcAddress(hModule, "fnExternCDllDemo"); if (pCDllDemo != NULL) // printf("pCDllDemo->Max(32,42) = %d\n", pCDllDemo->Max(32, 42));//Dll导出类的调用太麻烦,因为DLL本来就是为C函数服务设计的。 if (fnDllDemo != NULL) printf("fnDllDemo(32) = %d\n", fnDllDemo(32)); if (nDllDemo != NULL) printf("*nDllDemo = %d\n", *nDllDemo); if (fnExternCDllDemo != NULL) printf("fnExternCDllDemo(22) = %d\n", fnExternCDllDemo(22)); _tsystem(_T("pause")); FreeLibrary(hModule); return 0; } |
COM主要是一套给C/C++用的接口,当然为了微软的野心,它也被推广到了VB、Delphi以及其他一大堆奇奇怪怪的平台上。它主要为了使用dll发布基于interface的接口。我们知道dll的接口是为了C设计的,它导出的基本都是C的函数,从原理上来说,将dll加载到内存之后,会告诉你一组函数的地址,你自己call进去就可以调用相应的函数[3]。
但是对于C++来说这个事情就头疼了,现在假设你有一个类,我们知道使用一个类的第一步是创建这个类:new MyClass()。这里直接就出问题了,new方法通过编译器计算MyClass的大小来分配相应的内存空间,但是如果库升级了,相应的类可能会增加新的成员,大小就变了,那么使用旧的定义分配出来的空间就不能在新的库当中使用。
要解决这问题,我们必须在dll当中导出一个CreateObject的方法,用来代替构造函数,然后返回一个接口。然而,接口的定义在不同版本当中也是有可能会变化的,为了兼容以前的版本同时也提供新功能,还需要让这个对象可以返回不同版本的接口。接口其实是一个只有纯虚函数的C++类,不过对它进行了一些改造来兼容C和其他一些编程语言。
在这样改造之后,出问题的还有析构过程~MyClass()或者说delete myClass,因为同一个对象可能返回了很多个接口,有些接口还在被使用,如果其中一个被人delete了,其他接口都会出错,所以又引入了引用计数,来让许多人可以共享同一个对象。
其实到此为止也并不算是很奇怪的技术,我们用C++有的时候也会使用Factory方法来代替构造函数实现某些特殊的多态,也会用引用计数等等。COM技术的奇怪地方在于微软实在是脑洞太大了,它们构造了一个操作系统级别的Factory,规定所有人的Interface都统一用UUID来标识,以后想要哪个Interface只要报出UUID来就行了。这样甚至连链接到特定的dll都省了。
这就好比一个COM程序员,只要他在Windows平台上,调用别的库就只要首先翻一下魔导书,查到了一个用奇怪文字写的“Excel = {xxx-xxx-xxxx…}”的记号,然后它只要对着空中喊一声:“召唤,Excel!CoCreateInstance, {xxx-xxx-xxxx…}”然后呼的从魔法阵里面窜出来了一个怪物,它长什么样我们完全看不清,因为这时候它的类型是IUnknow,这是脑洞奇大无比的微软为所有接口设计的一个基类。
我们需要进一步要求它变成我们能控制的接口形态,于是我们再喊下一条指令:“变身,Excel 2003形态!QueryInterface, {xxx-xxx-xxxx…}”QueryInterface使用的是另一个UUID,用来表示不同版本的接口。于是怪物就变成了我们需要的Excel 2003接口,虽然我们不知道它实际上是2003还是2007还是更高版本。等我们使唤完这只召唤兽,我们就会对它说“回去吧,召唤兽!Release!”但是它不一定听话,因为之前给它的命令也许还没有执行完,它会忠诚地等到执行完再回去,当然我们并不关心这些细节。
微软大概会觉得自己设计出了软件史上最完美的二进制接口,从今以后所有的第三方库都可以涵盖在这套接口之下。然而历史的车轮是无情的,它碾过那些自以为是的人的速度总是会比想象的更快。Java的直接基于类的接口被广泛应用,开发使用起来远远来的简单,即便偶尔出点问题大家也都想办法解决了,事实证明程序员并不愿意花10倍的编写代码的时间来解决二进制库的版本兼容问题,他们更愿意假装没看见。很快微软也抄了一个.NET托管dll的方案出来,于是纯的二进制接口COM就慢慢被抛弃了。
COM,OLE,ActiveX,OCX,VBScript,历史不会忘记你们的,如果历史忘了,我替历史记住你们。安息吧。
借鉴COM技术,这里直接给出DLL到处类的成熟方法,可有效避免DLL地狱问题。具体结构为:
导出类是一个派生类,派生自一个抽象类——都是纯虚函数。使用者需要知道这个抽象类的结构。DLL最少只需要提供一个用于获取类对象指针的接口。使用者跟DLL提供者共用一个抽象类的头文件,使用者依赖于DLL的东西很少,只需要知道抽象类的接口,以及获取对象指针的导出函数,对象内存空间的申请是在DLL模块中做的,释放也在DLL模块中完成,最后记得要调用释放对象的函数。
这种方式比较好,通用,产生的DLL没有特定环境限制。借助了C++类的虚函数。一般都是采用这种方式。除了对DLL导出类有好处外,采用接口跟实现分离,可以使得工程的结构更清晰,使用者只需要知道接口,而不需要知道实现。
具体代码如下:
DLL导出类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
//DLL导出类头文件MatureApproach.h,与DLL使用者共享 #ifdef MATUREAPPROACH_EXPORTS #define MATUREAPPROACH_API __declspec(dllexport) #else #define MATUREAPPROACH_API __declspec(dllimport) #endif class IExport { public: virtual void Hi() = 0; virtual void Test() = 0; virtual void Release() = 0; }; extern "C" MATUREAPPROACH_API IExport* _stdcall CreateExportObj(); extern "C" MATUREAPPROACH_API void _stdcall DestroyExportObj(IExport* pExport); //DLL导出接口函数的实现MatureApproach.cpp #include "stdafx.h" #include "MatureApproach.h" #include "ExportClassImpl.h" BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; } MATUREAPPROACH_API IExport* APIENTRY CreateExportObj() { return new ExportImpl; } //这里不能直接delete pExport,因为没有把IExport的析构函数定义为虚函数 MATUREAPPROACH_API void APIENTRY DestroyExportObj(IExport* pExport) { pExport->Release(); } |
DLL导出类的具体实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
//DLL导出类头文件ExportClassImpl.h #include "MatureApproach.h" class ExportImpl : public IExport { public: virtual void Hi(); virtual void Test(); virtual void Release(); ~ExportImpl(); private: }; //DLL导出类的实现ExportClassImpl.cpp #include "stdafx.h" #include "ExportClassImpl.h" void ExportImpl::Hi() { wcout << L"Hello World" << endl; } void ExportImpl::Test() { wcout << L"Hi cswuyg" << endl; } void ExportImpl::Release() { delete this; } ExportImpl::~ExportImpl() { cout << "Release OK" << endl; } |