动态链接库

1. 动态链接库的分类

  Visual C++支持三种DLL,它们分别是Non-MFC DLL(非MFC动态库)、 MFC Regular DLL(MFC规则DLL)、 MFC Extension DLL(MFC扩展DLL)。

  (1) 非MFC动态库:不采用MFC类库结构,其导出函数为标准的C接口,能被非MFC或MFC编写的应用程序所调用。

  (2) MFC规则DLL:包含一个继承自CWinApp的类,但其无消息循环。

  (3) MFC扩展DLL:采用MFC的动态链接版本创建,它只能被用MFC类库所编写的应用程序所调用。

2. DLL中导出函数的声明方式

  一种方式是:在函数声明中加上__declspec(dllexport)
  另外一种方式是:采用模块定义(.def)文件声明,(.def)文件为链接器提供了有关被链接程序的导出、属性及其他方面的信息。

  (1) 方式一:在函数声明中加上__declspec(dllexport)

1 #pragma once
2 
3 extern "C" __declspec(dllexport) int add(int x, int y);
.h
1 #include "stdafx.h"
2 #include "dllTest.h"
3 
4 int add(int x, int y)
5 {
6     return (x + y);
7 }
.cpp

   (2) 方式二:采用模块定义(.def)文件声明

    ①首先创建 一个DLL程序(DllTestDef)

    ②在*.cpp中添加需要导出的接口函数

 1 #include "stdafx.h"
 2 
 3 int __stdcall Add(int a, int b)
 4 {
 5     return (a + b);
 6 }
 7 
 8 int _stdcall Sub(int a, int b)
 9 {
10     return (a - b);
11 }
.cpp

     ③然后创建一个.def的文件

1 LIBRARY dllTest
2 EXPORTS
3 Add @ 1
4 Sub @ 2
.def

3. DLL的调用方式

  (1)动态调用:

    "LoadLibrary-GetProcAddress-FreeLibrary"系统API提供的三位一体"DLL加载-DLL函数地址获取-DLL释放"方式,这种调用方式称为DLL的动态调用。

 1 #include "stdafx.h"
 2 #include <iostream>
 3 using namespace std;
 4 #include <windows.h>
 5 
 6 typedef int(*lpAdd)(int, int);
 7 typedef int(*lpSub)(int, int);
 8 
 9 int main()
10 {
11     HMODULE hDll = NULL;
12     lpAdd pAdd = NULL;
13     lpSub pSub = NULL;
14 
15     hDll = LoadLibrary(TEXT("dllTest.dll"));
16     if (hDll != NULL)
17     {
18         pAdd = (lpAdd)GetProcAddress(hDll, "Add");
19         pSub = (lpSub)GetProcAddress(hDll, "Sub");
20 
21         if (pAdd != NULL)
22         {
23             cout << "3 + 5 = " << pAdd(3, 5) << endl;
24         }
25 
26         if (pSub != NULL)
27         {
28             cout << "2 + 4 = " << pSub(2, 4) << endl;
29         }
30     }
31 
32     getchar();
33     return 0;
34 }
View Code

   (2)静态调用:

    静态调用,也称为隐式调用,由编译系统完成对DLL的加载和应用程序结束时DLL卸载的编码(Windows系统负责对DLL调用次数的计数),调用方式简单,能够满足通常的要求。通常采用的调用方式是把产生动态连接库时产生的.LIB文件加入到应用程序的工程中,想使用DLL中的函数时,只须在源文件中声明一下。

 1 #include "stdafx.h"
 2 #include <iostream>
 3 using namespace std;
 4 
 5 #pragma comment(lib, "..\\x64\\Release\\dllTest.lib")
 6 extern "C" __declspec(dllimport) int add(int x, int y);
 7 
 8 int main()
 9 {
10     cout << "4 + 7 = " << add(4, 7) << endl;
11 
12     getchar();
13     return 0;
14 }
View Code

 4. DllMain函数

    Windows在加载DLL的时候,需要一个入口函数,如同控制台或 DOS 程序需要 main 函数、WIN32程序需要 WinMain 函数一样。

    在前面的例子中,DLL并没有提供 DllMain函数, 应用工程也能成功引用 DLL,这是因为Windows在找不到DllMain的时候, 系统会从其它运行库中引入一个不做任何操作的缺省 DllMain函数版本,并不意味着 DLL可以放弃 DllMain函数。

    根据编写规范, Windows必须查找并执行 DLL里的 DllMain函数作为加载 DLL的依据,它使得 DLL得以保留在内存里。这个函数并不属于导出函数,而是 DLL 的内部函数。这意味着不能直接在应用工程中引用 DllMain函数, DllMain是自动被调用的 。

 1 BOOL APIENTRY DllMain( HMODULE hModule,
 2                        DWORD  ul_reason_for_call,
 3                        LPVOID lpReserved
 4                      )
 5 {
 6     switch (ul_reason_for_call)
 7     {
 8     case DLL_PROCESS_ATTACH:
 9     case DLL_THREAD_ATTACH:
10     case DLL_THREAD_DETACH:
11     case DLL_PROCESS_DETACH:
12         break;
13     }
14     return TRUE;
15 }
DllMain

  5. 关于调用约定

  C/C++缺省的调用方式是__cdecl方式,Windows API使用__stdcall调用方式,在DLL导出函数中,为了跟Windows API保持一致,建议使用__stdcall调用方式。

        __cdecl方式与__stdcall对函数名最终生成符号的方式不同。若采用C编译方式(在C++中需将函数声明为extern "C")。

  __stdcall调用约定在输出函数名钱加下划线,后面加“@”符号和参数的字节数,形如_functionname@number;

  而__cdecl调用约定仅在输出函数名前加下划线,形如_functionname。

6. DLL导出变量

     DLL定义的全局变量可以被调用进程访问,DLL也可以访问调用进程的全局数据。

    (1) 在DLL中导出变量有两种方法:

   方法一:用模块定义文件(.def)进行导出声明  

 1 // dllmain.cpp : 定义 DLL 应用程序的入口点。
 2 #include "stdafx.h"
 3 
 4 int dllGlobalVar;
 5 
 6 BOOL APIENTRY DllMain( HMODULE hModule,
 7                        DWORD  ul_reason_for_call,
 8                        LPVOID lpReserved
 9                      )
10 {
11     switch (ul_reason_for_call)
12     {
13     case DLL_PROCESS_ATTACH:
14         dllGlobalVar = 123;
15         break;
16     case DLL_THREAD_ATTACH:
17     case DLL_THREAD_DETACH:
18     case DLL_PROCESS_DETACH:
19         break;
20     }
21     return TRUE;
22 }
23 
24 
25 
26 LIBRARY dllExportVariable_def
27 EXPORTS
28 dllGlobalVar DATA
View Code

  特别要注意的是用extern int dllGlobalVar声明所导入的并不是DLL中全局变量本身,而是其地址,应用程序必须通过强制指针转换来使用DLL中的全局变量。这一点,从*(int*)dllGlobalVar可以看出。因此在采用这种方式引用DLL全局变量时,千万不要进行这样的赋值操作:

                dllGlobalVar = 1;
  其结果是dllGlobalVar指针的内容发生变化,程序中以后再也引用不到DLL中的全局变量了。

  而通过_declspec(dllimport)方式导入的就是DLL中全局变量本身而不再是其地址了,笔者建议在一切可能的情况下都使用这种方式。

  方法二:用__declspec进行导出声明

1 __declspec(dllexport) extern int dllGlobalVar = 88;
View Code

   (2) 调用DLL中导出的变量:

  同样,应用程序调用DLL中的变量也有两种方法。 

  第一种是隐式链接: 

 1 #include <iostream>
 2 using namespace std;
 3 
 4 #pragma comment(lib, "..\\x64\\Debug\\dllExportVariable_declspec.lib")
 5 extern _declspec(dllimport) int dllGlobalVar;
 6 
 7 int main()
 8 {
 9     cout << "dllGlobalVar = " << dllGlobalVar << endl;
10 
11     dllGlobalVar = 88;
12     cout << "dllGlobalVar = " << dllGlobalVar << endl;
13 
14     getchar();
15     return 0;
16 }
View Code

  第二种是显式链接: 

 1 #include <iostream>
 2 using namespace std;
 3 
 4 #include <windows.h>
 5 
 6 int main()
 7 {
 8     int my_int;
 9     HINSTANCE hInstLibrary = LoadLibrary(TEXT("dllExportVariable_def.dll"));
10 
11     if (hInstLibrary != NULL)
12     {
13         my_int = *(int*)GetProcAddress(hInstLibrary, "dllGlobalVar");
14         cout << "my_int = " << my_int << endl;
15     }
16     FreeLibrary(hInstLibrary);
17 
18     getchar();
19     return 0;
20 }
View Code

  Note:一般不建议从DLL中导出全局变量,对于希望从DLL获取资源以实现资源共享的情景,最好是通过导出一个Get函数获得,这样操作起来更方便而且更安全。

7. DLL导出类

  一、导出类的简单方式

    这种方式是比较简单的,同时也是不建议采用的不合适方式。 

    只需要在导出类加上__declspec(dllexport),就可以实现导出类。对象空间还是在使用者的模块里,dll只提供类中的函数代码。

    不足的地方是:使用者需要知道整个类的实现,包括基类、类中成员对象,也就是说所有跟导出类相关的东西,使用者都要知道。通过Dependency Walker可以看到,这时候的dll导出的是跟类相关的函数:如构造函数、赋值操作符、析构函数、其它函数,这些都是使用者可能会用到的函数。

    这种导出类的方式,除了导出的东西太多、使用者对类的实现依赖太多之外,还有其它问题:必须保证使用同一种编译器。导出类的本质是导出类里的函数,因为语法上直接导出了类,没有对函数的调用方式、重命名进行设置,导致了产生的dll并不通用。

 

    简单方式导出类的DLL示例:

 1 #pragma once
 2 
 3 //相关的类都必须导出
 4 class _declspec(dllexport) CBase
 5 {
 6 public:
 7     void Test1();
 8 private:
 9     int m_var1;
10 };
11 
12 //相关的类都必须导出
13 class _declspec(dllexport) CData
14 {
15 public:
16     void Test2();
17 private:
18     int m_var2;
19 };
20 
21 //要导出的类
22 class _declspec(dllexport) CExportClass : public CBase
23 {
24 public:
25     CExportClass(int i = 0);
26 
27     void TestFun();
28     CData GetDataObj() { return m_DataObj; }
29 
30 private:
31     int m_i;
32     CData m_DataObj;
33 };
.h
 1 CExportClass::CExportClass(int i) : m_i(i)
 2 {
 3 }
 4 
 5 void CExportClass::TestFun()
 6 {
 7     cout << "This is TestFun() from CExportClass class!" << endl;
 8 }
 9 
10 void CBase::Test1()
11 {
12     cout << "This is Test1() from CBase class!" << endl;
13 }
14 
15 void CData::Test2()
16 {
17     cout << "This is Test2() from CData class!" << endl;
18 }
.cpp

   调用示例:

 1 #include "stdafx.h"
 2 #include <Windows.h>
 3 #include "..\dllTest_ExportClass(NotRecommend)\dllTest_ExportClass(NotRecommend).h"
 4 
 5 #pragma comment(lib, "..\\x64\\Debug\\dllTest_ExportClass(NotRecommend).lib")
 6 
 7 int main()
 8 {
 9     CExportClass obj(55);
10     obj.Test1();
11     obj.TestFun();
12 
13     CData DataObj = obj.GetDataObj();
14     DataObj.Test2();
15 
16     system("pause");
17     return 0;
18 }
View Code

   二、导出类的较好方式

    这种方式和COM类似,它的结构是这样的:导出类是一个派生类,派生自一个抽象类(都是纯虚函数)。使用者只需知道这个抽象类的结构。

    DLL最少需要提供一个用于获取类对象指针的接口。使用者和DLL提供者共用一个抽象类的头文件。使用者依赖于DLL的东西很少,只需要知道抽象类的接口,以及获取对象指针的导出函数,对象内存空间的申请是在DLL模块中做的,释放也在DLL模块中完成(需要在最后调用释放对象的函数)。

    这种方式比较好,通用,产生的DLL没有特定的环境限制。除了对DLL导出类有好处外,它采用接口和实现分离,也可以使得工程结构更清晰,使用者只需要知道接口,不需要知道实现。

    

    测试示例代码下载地址: https://files.cnblogs.com/files/YQ2014/dllTest_ExportClass%28NotRecommend%29.zip   

  

  参考资料:

  http://www.cnblogs.com/cswuyg/archive/2011/10/06/DLL2.html

  http://www.codeproject.com/KB/cpp/howto_export_cpp_classes.aspx

  

  导出类的DLL要小心DLL Hell问题。

       详细的可以参考:DLL导出类避免地狱问题的完美解决方案

 

posted on 2018-10-17 14:59  FlyingPig007  阅读(458)  评论(0编辑  收藏  举报

导航