一种优雅的动态链接库DLL的使用
1. 什么是DLL(动态链接库)?
动态链接库(DLL)是从C语言函数库和Pascal库单元的概念发展而来的。所有的C语言标准库函数都存放在某一函数库中。在链接应用程序的过程中,链接器从库文件中拷贝程序调用的函数代码,并把这些函数代码添加到可执行文件中。这种方法同只把函数储存在已编译的OBJ文件中相比更有利于代码的重用。但随着Windows这样的多任务环境的出现,函数库的方法显得过于累赘。如果为了完成屏幕输出、消息处理、内存管理、对话框等操作,每个程序都不得不拥有自己的函数,那么Windows程序将变得非常庞大。Windows的发展要求允许同时运行的几个程序共享一组函数的单一拷贝。动态链接库就是在这种情况下出现的。动态链接库不用重复编译或链接,一旦装入内存,DLL函数可以被系统中的任何正在运行的应用程序软件所使用,而不必再将DLL函数的另一拷贝装入内存。
DLL是一个包含可由多个程序同时使用的代码和数据的库。例如:在Windows操作系统中,Comdlg32 DLL执行与对话框有关的常见函数。
因此,每个程序都可以使用该DLL中包含的功能来实现“打开”对话框。这有助于促进代码重用和内存的有效使用。这篇文章的目的就是让你一次性就能了解和掌握DLL。
2. 为什么要使用DLL(动态链接库)?
代码复用是提高软件开发效率的重要途径。
一般而言,只要某部分代码具有通用性,就可以将它构造成相对独立的功能模块并在之后的项目中重复使用。
比较常见的例子是各种应用程序框架,它们都以源代码的形式发布。由于这种复用是源代码级别的,源代码完全暴露给了程序员,因而称之为“白盒复用”。白盒复用有以下三个缺点:
1.暴露源代码,多份拷贝,造成存储浪费;
2.容易与程序员的本地代码发生命名冲突;
3.更新模块功能比较困难,不利于问题的模块化实现;
为了弥补这些不足,就提出了“二进制级别”的代码复用了。使用二进制级别的代码复用一定程度上隐藏了源代码,对于“黑盒复用”的途径不只DLL一种,静态链接库,甚至更高级的COM组件都是。
使用DLL主要有以下优点:
1.使用较少的资源;当多个程序使用同一函数库时,DLL可以减少在磁盘和物理内存中加载的代码的重复量。
这不仅可以大大影响在前台运行的程序,而且可以大大影响其它在Windows操作系统上运行的程序;
2.推广模块式体系结构;
3.简化部署与安装。
优雅创建DLL,与优雅使用DLL库 步骤
3.1 创建DLL
用VS2013新建工程,
、
点击确定,新建工程完毕
这个时候VS2013自动帮我们添加了dllmain文件,
Windows在加载DLL时,需要一个入口函数,就像控制台程序需要main函数一样。有的时候,DLL并没有提供DllMain函数,应用程序也能成功引用DLL,这是因为Windows在找不到DllMain的时候,系统会从其它运行库中引入一个不做任何操作的默认DllMain函数版本,并不意味着DLL可以抛弃DllMain函数。(因此我们可以不使用该dllmain文件,或者删除即可)根据编写规范,Windows必须查找并执行DLL里的DllMain函数作为加载DLL的依据,它使得DLL得以保留在内存里。这个函数并不属于导出函数,而是DLL的内部函数,这就说明不能在客户端直接调用DllMain函数,DllMain函数是自动被调用的。
DllMain函数在DLL被加载和卸载时被调用,在单个线程启动和终止时,DllMain函数也被调用。参数ul_reason_for_call指明了调用DllMain的原因,有以下四种情况:
DLL_PROCESS_ATTACH:当一个DLL被首次载入进程地址空间时,系统会调用该DLL的DllMain函数,传递的ul_reason_for_call参数值为DLL_PROCESS_ATTACH。这种情况只有首次映射DLL时才发生;
DLL_THREAD_ATTACH:该通知告诉所有的DLL执行线程的初始化。当进程创建一个新的线程时,系统会查看进程地址空间中所有的DLL文件映射,之后用DLL_THREAD_ATTACH来调用DLL中的DllMain函数。要注意的是,系统不会为进程的主线程使用值DLL_THREAD_ATTACH来调用DLL中的DllMain函数;
DLL_PROCESS_DETACH:当DLL从进程的地址空间解除映射时,参数ul_reason_for_call参数值为DLL_PROCESS_DETACH。当DLL处理DLL_PROCESS_DETACH时,DLL应该处理与进程相关的清理操作。如果进程的终结是因为系统中有某个线程调用了TerminateProcess来终结的,那么系统就不会用DLL_PROCESS_DETACH来调用DLL中的DllMain函数来执行进程的清理工作。这样就会造成数据丢失;
DLL_THREAD_DETACH:该通知告诉所有的DLL执行线程的清理工作。注意的是如果线程的终结是使用TerminateThread来完成的,那么系统将不会使用值DLL_THREAD_DETACH来执行线程的清理工作,这也就是说可能会造成数据丢失,所以不要使用TerminateThread来终结线程。
3.2 编写DLL的函数
编写DLL时的函数与一般的函数方法基本一样。但要对库中的函数进行必要的声明,以说明哪些函数是可以导出的,哪些函数是不可以导出的。
把DLL中的函数声明为导出函数的方法有两种:
一是使用关键字_declspec(dllexport)来声明。
二是在.def文件中声明。
一:使用关键字来声明创建
(1)普通函数
1 #ifndef _MYCODE_H_ 2 #define _MYCODE_H_ 3 #ifdef DLLDEMO1_EXPORTS 4 #define EXPORTS_DEMO _declspec( dllexport ) 5 #else 6 #define EXPORTS_DEMO _declspec(dllimport) 7 #endif 8 EXPORTS_DEMO int Add (int a , int b); 9 #endif 10 11 或者: 12 13 #ifndef _MYCODE_H_ 14 #define _MYCODE_H_ 15 #ifdef DLLDEMO1_EXPORTS 16 #define EXPORTS_DEMO _declspec( dllexport ) 17 #else 18 #define EXPORTS_DEMO _declspec(dllimport) 19 #endif 20 extern "C" EXPORTS_DEMO int Add (int a , int b); 21 #endif
【1】这里采用宏定义的方法进行声明,这样就可以把头文件作为与 生成的DLL、lib 三者一起进行打包发送给使用者。使用者根据宏定义的使用,即可直接包含
【2】extern C
为了使一个用C++语言编写的DLL函数可以在C语言编写的应用程序中使用,在关键字_declspec(dllexport) 之前要附加另一个关键字:extern “C”,以通知编译器采用C链接方式。
为什么要使用extern “C”呢?C++之父在设计C++时,考虑到当时已经存在了大量的C代码,为了支持原来的C代码和已经写好的C库,需要在C++中尽可能的支持C,而extern “C”就是其中的一个策略。在声明函数时,注意到我也使用了extern “C”,这里要详细的说说extern “C”。
extern “C”包含两层含义,首先是它修饰的目标是”extern”的;其次,被它修饰的目标才是”C”的。先来说说extern;在C/C++中,extern用来表明函数和变量作用范围(可见性)的关键字,这个关键字告诉编译器,它申明的函数和变量可以在本模块或其它模块中使用。extern的作用总结起来就是以下几点:
1.在一个文件内:
如果外部变量不在文件的开头定义,其有效范围只限定在从定义开始到文件的结束处。如果在定义前需要引用该变量,则要在引用之前用关键字”extern”对该变量做”外部变量声明”,表示该变量是一个已经定义的外部变量。有了这个声明,就可以从声明处起合理地使用该变量了。
#include <iostream> using namespace std; int main(int argc, char *argv[]) { extern int a; cout<<a<<endl; } int a = 100;2.在多文件的程序中:
如果多个文件都要使用同一个外部变量,不能在各个文件中各定义一个外部变量,否则会出现“重复定义”的错误。
正确的做法是在任意一个文件中定义外部变量,其它文件用extern对变量做“外部变量声明”。
在编译和链接时,系统会知道该变量是一个已经在别处定义的外部变量,并把另一文件中外部变量的作用域扩展到本文件,这样在本文件就可以合法地使用该外部变量了。写过MFC程序的人都知道,在在CXXXApp类的头文件中,就使用extern声明了一个该类的变量,而该变量的实际定义是在CXXXApp类的实现文件中完成的;
3. 外部函数:
在定义函数时,如果在最左端加关键字extern,表示此函数是外部函数。C语言规定,如果在定义时省略extern,则隐含为外部函数。而内部函数必须在前面加static关键字。在需要调用此函数的文件中,用extern对函数作声明,表明该函数是在其它文件中定义的外部函数。
综上,因为是对C++ 的类的dll导出,则可以采用下述形式,且可以用添加extern c 的形式:
1 #ifndef AMF_H 2 #define AMF_H 3 4 #if defined(_WIN32) || defined(_WIN64) /*Windows*/ 5 #ifdef DLL_EXPORT 6 #define LIBSPEC __declspec(dllexport) 7 #elif defined(DLL_IMPORT) 8 #define LIBSPEC __declspec(dllimport) 9 #else 10 #define LIBSPEC //什么都没有定义,直接使用源码 11 #endif 12 #else /*non-windows*/ 13 #define LIBSPEC /*TODO: Allow this header file to generate (and be distributed) with non-windows shared objects*/ 14 #endif 15 16 17 class LIBSPEC Amf /*LIBSPEC macro allows windows users the option of using this class in DLL form */ 18 { 19 public: 20 Amf(); 21 ~Amf(); 22 Amf(const Amf& In); 23 Amf& operator=(const Amf& In); 24 25 26 //Amf I/O 27 bool Save(std::string AmfFilePath, bool Compressed = true); 28 bool Load(std::string AmfFilePath, bool StrictLoad = true); 29 } 30 31 #undef LIBSPEC 32 #endif //AMF_WIN_H
把上述作为 CXXXXX.h 的头文件内容,因为该文件最终要副带给使用者因此,不合适在临面进行函数实现。
而函数或者类的实现作为CXXXXX.cpp 源文件中。
1 extern "C" __declspec(dllexport) void SayHello() 2 3 { 4 5 ::MessageBoxW(NULL, L"Hello", L"fangyukuan", MB_OK); 6 7 }
或者
普通的类的实现源文件相同。
#include “CXXXX.h” //类的实现,且不需要额外修饰 CXXX::CXXX() { } CXXX::XXXX() { }
编译即可生成相相应的DLL:
一般生成四个文件,其中最重要的额DLL是动态链接库, lib是函数接口的映射,以及 包含导出生命的头文件.h
4 使用DLL
基本上要把三个文件进行使用:
(1)XXX.dll 文件,放在程序生成exe的目录下面;
(2)XXX.lib 文件,可以像在项目中进行加载,也可以放在程序使用的代码中加载。
放在程序中加载:告诉连接器,lib文件的位置,以及需要连接加载的lib名称。
放在代码中加载:
1 #include <windows.h> 2 #include <iostream> 3 //(1)添加dll的头文件 4 #include "..\\DLLDemo1\\CXXX.h" 5 using namespace std; 6 //(2)加载lib文件 7 #pragma comment(lib, "..\\debug\\DLLDemo1.lib") 8 // (3)dll放在 exe目录即可 9 int main(int argc, char *argv[]) 10 { 11 cout<<Add(2, 3)<<endl; 12 return 0; 13 }
在使用上,一般没什么差异,直接利用include的头文件中的类型进行使用即可。
5. 使用VS6中的工具进行检测
depend.exe 可以用来打开dll文件,并查看dll的依赖文件,以及dll中的导出的函数接口。
注意:如果没有使用extern c声明的导出接口,其depends开发的效果如下:
即名字为 ?[email protected]@[email protected]
如果用extern C声明, 可函数名称就正常了。
不过,一般如果不采用直接对单个导出函数进行单独使用的方式,上述两种差异不大。
为了简洁,且一般开发的C++ 的dll ,如果包含类,则没必要使用 extern c 因为,C是没办法调用C++ 的dll 的。
endl;