• 动态链接库(八)--二次开发dll


    写在前面

    此前已接触了开发和使用动态链接库时的诸如声明时的导入导出符号位置、调用约定位置; 不同编译器调用约定的名字改编规则以及如何解决不同编译器调用约定导致的名字改编问题等。

    在经历上述问题并了解如何解决过后,相信后续对动态链接库的开发使用都会更从容一些。

    例在实际的工作中,难免会用到其他开发人员开发的dll。

    在使用其他开发人员开发的dll时,从该dll导入的函数都有发生名字改编,且提供的.h头文件中也没有指明编译环境和调用约定,导致在自己项目中调用.h头文件中的函数时,会有一堆 无法解析的外部符号 错误提示。

    这里我们就会知道,该错误是因为这些dll的导出函数的 编译环境和调用约定 可能和我们目前使用该dll的项目不一样,导致的名字改编问题,因此使用原始函数名调用时导致编译报错。

    知道原因后,我们就可以同之前一样,使用dumpbin工具查看该dll的导出函数发生改编后的名字,依次推断下这些导出函数的编译环境和调用约定

    然后在dll提供的.h头文件中添加相应处理:不同编译器添加extern “C”, 调用约定则在声明中显示添加即可。

    但也会有其他开发同事使用该dll时会碰到上述问题,假设这里就有同事不会处理过来问你如何解决的,这时你就可以神秘地给他这几段代码:
    调用约定
    解决不同编译器导致的名字改编问题
    解决不同调用约定导致的名字改编问题

    告知他:全是你想要的😏。

    当然上面链接都是咱们之前解决不同编译器和调用约定导致的名字改编问题的实例,其他需求的话,不会还有人不知道全球最大的hub吧。


    虽然知道如何解决了,但为了方便后续使用,你的leader让你封装下这个dll,封装成后续使用时不用在人为的修改包含的.h头文件就能直接使用原始函数名调用。

    因为这个dll是其他开发人员的,当然最理想的方式就是找到他,让他修改该dll项目,解决名字改编问题。

    不过人家大概率是不会搭理你的,如果他帮你改了,那你就可以考虑入手了,毕竟过日子还是傻点的好。




    如果他不改,你也可以问他要代码,自己改,不过人家大概率也是不会给你的。

    那么这是你可能就会考虑,二次开发这个dll,再封装成dll。即自己新建一个dll,在自己这个dll中使用发生名字改编的dll,然后在自己的这个dll中解决名字改编问题,后续就提供自己封装的这个dll,依旧能够使用之前dll的函数,且不再有名字改编导致的调用问题。

    下面,我们就此需求出发,详细学习下,如何二次开发dll。这里会具体分成:二次开发C dll和二次开发C++ dll 两部分。

    二次开发C dll

    既然要二次开发dll,总得需要有个dll让我们二次开发吧。因此这里提供一个C编译生成的dll,代替上述没有源码,且发生名字改编的dll。

    //CDll.h
    #pragma once
    #ifdef DLL_API
    #else
    #define  DLL_API __declspec(dllimport)
    #endif
    
    int DLL_API __stdcall add_c(int a, int b);
    int DLL_API __stdcall sub_c(int a, int b);
    
    void DLL_API __cdecl fun();
    
    //CDll.c
    #define DLL_API __declspec(dllexport)
    #include "CDll.h"
    
    #include 
    #include 
    
    int __stdcall add_c(int a, int b)
    {
    	return a + b;
    }
    
    int __stdcall sub_c(int a, int b)
    {
    	return a - b;
    }
    
    
    void DLL_API __cdecl fun()
    {
    	OutputDebugString(_T("\n-----fun-----\n"));
    }
    
    
    • 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

    因为要有名字改编,因此这里不提供模块定义文件。

    编译生成,使用dumpbin查看:
    1

    这里通过改编名,我们可以推断_add_c@8是C编译__stdcall 调用约定,同理_sub_c@也是,而fun则是C编译__cdecl调用约定。

    为了模拟上述使用原始名调用失败问题,这里将生产的dll更新到我们C++项目,使用原始名调用:
    2

    简单解决就是手动在cplusTest项目包含CDll.h中添加extern “C” 限定符即可:
    3

    但这里像新创建一个dll项目,在这个dll项目中封装CDll,并解决名字改编问题。

    这里新建了一个Dll2的dll项目,并有将CDll更新到该项目中:
    4

    然后如何解决名字改编呢?
    先修改CDll.h,添加条件编译的extern “C” 限定符, 再在模块定义文件中添加CDll中导出函数的符号,然后在Dll2.h 头文件中包含CDll.h即可。

    5

    编译生成,对外提供Dll2.h、Dll2.lib、Dll2.dll以及在Dll2中引用的CDll的相关文件: CDll.h、CDll.lib、CDll.dll。

    使用dumpbin查看CDll.dll 和 二次封装的Dll2.dll 中的导出函数:
    6
    7
    可以看到在CDll.dll 中三个导出函数都是有发生名字改编的,改编名分别是**_add_c@8、_sub_c@8和_fun**, 在Dll2中解决名字改编(即在模块定义文件中指定导出符号)后,从Dll2.dll导出的三个函数就没有发生名字改编了:add_c、sub_c和fun。

    这里注意函数相对地址的联系,以add_c函数为例,在Dll2.dll中add_c的函数地址为Dll2.dll的相对地址(当被加载的使用进程后会被替换成Dll2.dll在该进程中的实际地址) + 一个偏移量(70), 这里对应的是小括号后的_add_c@8。

    因为Dll2.dll中没有该函数的实现,因此会去Dll2.dll中引用的其他模块—CDll.dll中查找,在CDll.dll中,_add_c@8的函数地址为CDll.dll的相对地址(当被加载至Dll2时会被替换成在Dll2中的实际地址) + 偏移量(125).

    因此,当使用Dll2.dll的项目在链接时,会将所有相对地址赋实际值。当调用add时,会去Dll2.dll中找_add_c@8, 然后再对应到CDll.dll中的 实际地址 + 125 偏移量的函数地址。

    更新到cplusTest项目中,虽然多了Dll2.h、Dll2.lib、Dll2.dll,实际使用时只需包含Dll2.h即可。
    当然也需要在项目属性中导入库文件Dll2.lib 和 CDll.lib。
    8

    只需包含Dll2.h, 即可直接使用原始函数名调用。

    注意事项

    注意二次封装的Dll2中,不能有和CDll中的导出函数有同名的存在,这样编译就不会通过。

    9

    二次开发C++ dll

    同上,这里也提供一个C++的Dll1,然后在Dll2中二次开发。

    c++dll项目Dll1如下:

    //Dll1.h
    #pragma once
    #ifdef DLL1_API
    #else
    #define DLL1_API __declspec(dllimport) 
    #endif
    
    //C编译
    #ifdef __cplusplus
    extern "C"
    {
    #endif
    
    	int DLL1_API __stdcall add(int a, int b);
    	int DLL1_API __stdcall subtract(int a, int b);
    
    #ifdef __cplusplus
    };
    #endif
    
    
    //C++编译
    void DLL1_API __cdecl test();
    
    //重载函数
    void DLL1_API __stdcall output(int a, int b);
    void DLL1_API __stdcall output(int a, char b);
    void DLL1_API __stdcall output(int a, int b, char c);
    
    
    class Dll1
    {
    public:
    	//构造和析构必须指定导出符号, 否则无法创建销毁Dll1实例对象
    	DLL1_API Dll1();
    	virtual DLL1_API ~Dll1();
    
    	void DLL1_API publicFun();
    
    public:
    	//无法为类成员变量声明导出符号
    	int /*DLL1_API*/ m_nVal;
    
    private:
    	void DLL1_API privateFun();
    	//无法为类成员变量声明导出符号
    	int /*DLL1_API*/ m_nVal2;
    	
    };
    
    • 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

    Dll1.cpp如下:

    //Dll.cpp
    #define DLL1_API __declspec(dllexport)
    #include "Dll1.h"
    #include 
    #include 
    
    int __stdcall add(int a, int b)
    {
    	return a + b;
    }
    
    int  __stdcall subtract(int a, int b)
    {
    	return a - b;
    }
    
    
    //C++编译
    void DLL1_API __cdecl test()
    {
    	OutputDebugString(_T("\n-----test-----\n\n"));
    }
    
    void DLL1_API __stdcall output(int a, int b)
    {
    	TCHAR buf[100] = { 0 };
    	_stprintf_s(buf, _T("\noutput重载一:int a = %d,int b = %d\n\n"), a, b);
    	OutputDebugString(buf);
    }
    
    void DLL1_API __stdcall output(int a, char b)
    {
    	TCHAR buf[100] = { 0 };
    	_stprintf_s(buf, _T("\noutput重载二:int a = %d,char b = %c\n\n"), a, b);
    	OutputDebugString(buf);
    }
    
    void DLL1_API __stdcall output(int a, int b, char c)
    {
    	TCHAR buf[100] = { 0 };
    	_stprintf_s(buf, _T("\noutput重载三:int a = %d,int b = %d, char c = %c\n\n"), a, b, c);
    	OutputDebugString(buf);
    }
    
    Dll1::Dll1() : m_nVal(0), m_nVal2(0)
    {
    	TCHAR buf[100] = { 0 };
    	_stprintf_s(buf, _T("\nDll1默认构造--m_nVal = %d\n"), m_nVal);
    	OutputDebugString(buf);
    }
    
    Dll1::~Dll1()
    {
    	OutputDebugString(_T("\nDll1析构\n"));
    }
    
    void Dll1::publicFun()
    {
    	OutputDebugString(_T("\nDll1--publicFun\n"));
    }
    
    void Dll1::privateFun()
    {
    	OutputDebugString(_T("\nDll1--privateFun\n"));
    }
    
    • 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
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    因为希望Dll1各导出函数发生名字改编,因此这里不提供模块定义文件解决名字改编问题。

    编译生成后,使用dumpbin命令查看:
    10

    然后在Dll2项目中解决Dll1中能够解决的名字改编问题,不能解决的有:
    ①重载函数无法通过模块定义文件解决名字改编问题
    ②类的析构函数

    更新Dll2项目如下:

    #pragma once
    
    #ifdef DLL2_API
    #else
    #define DLL2_API _declspec(dllimport)
    #endif
    
    #include "CDll.h"
    
    #include "Dll1.h"
    
    //Dll2中也可以在这声明自己的导出函数
    void DLL2_API __stdcall dll2_test();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    #define DLL2_API _declspec(dllexport)
    #include "Dll2.h"
    
    #include 
    #include 
    
    
    void __stdcall dll2_test()
    {
    	OutputDebugString(_T("\n-----dll2_test-----\n\n"));
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    编译生成,使用dumpbin命令查看:
    11
    可以看到Dll1.dll 和 CDll.dll 中的导出函数都没有发生名字改编了。

    然后更新到C++项目cplusTest中使用,所需文件:
    CDll: CDll.h、CDll.lib、CDll.dll
    Dll1:Dll1.h、Dll1.lib、Dll1.dll
    Dll2: Dll2.h、Dll2.lib、Dll2.dll

    dll 文件直接放在输出exe文件的Debug目录下:
    12

    可以在项目根目录下创建一个Lib文件夹用来存放dll对应lib 和 .h文件:
    13
    Lib文件夹内再创建一个include文件夹用来存放.h文件:
    14
    15
    这样子的话方便管理,不过为了能在项目中直接include “Dll2.h”, 需要配置下项目属性:
    16

    选择刚刚创建的include文件夹目录即可:
    17

    导入lib的时候也许注意,添加相对main.cpp的lib存放路径:
    18

    然后就可以正常使用了, 使用时只需包含Dll2.h即可

    //cplusTest中的main.cpp
    #include 
    using namespace std;
    #include "Dll2.h"
    
    int main()
    {
    	//CDll.dll 导出函数测试
    	cout << "add_c: " << add_c(3, 3) << endl;
    	cout << "sub_c: " << sub_c(3, 3) << endl;
    	fun();
    
    	//Dll1.dll 导出函数测试
    	cout << "add: " << add(4, 4) << endl;
    	cout << "subtract: " << subtract(3, 3) << endl;
    
    	test();
    	output(1, 2);
    	output(1, 'C');
    	output(1, 2, 'D');
    
    	Dll1 dll1;
    	dll1.publicFun();
    	//dll1.privateFun();	即使是导出函数,也无法通过实例调用类的私有成员函数
    
    	//Dll2.dll 导出函数测试
    	dll2_test();
    
    
    	system("pause");
    	return 0;
    }
    
    • 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

    重新编译运行,均能正常调用:
    19

    总结

    无论是C编译生成的dll, 还是C++编译生成的dll,若其没有解决 不同编译器 或 不同调用约定 导致的名字改编问题,都可以通过在封装一层的方式去解决原始dll中没有解决的名字改编问题。

  • 相关阅读:
    python小说爬虫源代码
    使用yum install和reposync下载rpm安装包以及wget和curl下载文件
    JAVA计算机毕业设计摄影摄区源码+系统+mysql数据库+lw文档
    LeetCode42:接雨水
    跨区互联组网怎么做?SD-WAN专线可以实现吗?
    代码随想录 | Day53
    MySQL系列——集群复制方式及原理
    软件测试/测试开发丨接口自动化测试学习笔记,整体结构响应断言
    内核实战教程第五期 _ SQL 执行引擎的设计与实现
    【译】介绍 MSTest Runner – CLI、Visual Studio 等
  • 原文地址:https://blog.csdn.net/SNAKEpc12138/article/details/126314764