• 动态链接库(扩展)--实际开发时的注意事项


    写在前面

    通过前面的动态链接库的相关知识学习后,这里以C和C++编译环境为例简单说下实际开发动态链接库时的一些注意事项.

    此前的Dll1项目为了能让C++编译生成的dll 在C项目中使用,因为编译器不同,因此会通过extern “C”解决不同编译器之前的名字改编问题.

    因为extern “C” 限定符是C++中为了兼容C而出现的,因此在extern “C”包括的代码段中, 不能出现C没有的东西,例:
    ① 函数重载。函数重载也是C++才出现的功能,在extern “C”代码段中声明重载函数,编译器会报错:
    1
    ② 类。C是面向过程的编程语言,而类是C++扩展的支持面向对象编程的内容。虽然在extern “C”中声明类时不会编译报错,但更新到C项目后编译会报错:
    2

    所以开发dll时得明确自己的开发环境(是C还是C++, 最好明确告知使用者),还需明确使用该dll的用户(是在C编译器下还是C++编译器下使用).

    给C项目使用的dll

    要是给C项目使用的,那么dll中就不能有C没有的内容(例函数重载,类等),不然在C项目使用时就会有编译报错.

    C编写dll给C项目使用

    最理想的情况就是用C编写dll然后给C使用,这样就不用考虑不同编译器导致的名字改编问题了,只需考虑不同调用约定导致的名字改编问题即可.

    当然 C编写的dll 也肯定是可以给 C++项目 使用的(只是这里需解决下不同编译器导致的名字改编问题), 因为C++本就是C的超集,后面有详细介绍.

    C++编写dll给C项目使用

    若是用 C++编写给C项目使用的dll,按实际需要解决:
    动态链接库(六)–解决不同编译器导致的名字改编问题
    动态链接库(七)–解决不同调用约定导致的名字改编问题

    然后再注意下extern “C” 限定符下不能出现C没有的内容即可.

    给C++项目使用的dll

    同理,最理想的情况就是用C++编写dll然后给C++项目使用,这样就不用考虑不同编译器导致的名字改编问题了,只需考虑不同调用约定导致的名字改编问题即可.

    这里 解决不同调用约定导致的名字改编问题时需注意的是C++语言新扩展的一些内容,例:如何解决重载函数导出时的名字改编问题?

    C编写dll给C++项目使用

    因为C++本就是C的扩展,因此用C编写给C++项目使用的dll倒没有多大语言方面的限制.

    同理按需解决下面两个问题即可:
    动态链接库(六)–解决不同编译器导致的名字改编问题
    动态链接库(七)–解决不同调用约定导致的名字改编问题

    这里详细说明下 用C编写dll给C++项目使用时 如何解决不同编译器导致的名字改编问题. 当然和 动态链接库(六)–解决不同编译器导致的名字改编问题 类似.

    因为 动态链接库(六)–解决不同编译器导致的名字改编问题 中的dll是使用C++编译器编译生成的,所以可以在Dll1.h中加extern “C”, 若在C编译器中,extern “C”限定符是不存在的,如下:

    新建一个Win32控制台项目,在向导中勾选DLL和空项目:
    3

    然后添加一个CDll.c 源文件,告诉编译器该文件以C方式编译:
    4

    使用默认的C调用约定__cdecl:
    5

    然后同前一样,为DLL添加CDll.h 头文件,并通过条件编译定义导出导入宏:
    新建的CDll.h头文件内容如下:

    //CDll.h
    #ifdef CDLL_API
    #else
    #define  CDLL_API __declspec(dllimport)
    #endif
    
    //因为这里在C编译环境下,因此不能直接使用C++扩展的extern “C” 限定符
    int CDLL_API add_c(int a, int b);
    int CDLL_API sub_c(int a, int b);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    修改CDll.c源文件如下:

    //CDll.c
    #define CDLL_API __declspec(dllexport)
    #include "CDll.h"
    
    int CDLL_API add_c(int a, int b)
    {
    	return a + b;
    }
    
    int CDLL_API sub_c(int a, int b)
    {
    	return a - b;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    编译生成dll, 使用dumpbin命令查看导出函数, 导出函数名符合C编译__cdecl调用约定的名字改编规则:
    6

    然后更新 CDll.dll 到C++项目cplusTest中,在cplusTest项目的main.cpp中添加如下调用:

    //cplusTest中的main.cpp
    #include 
    using namespace std;
    
    #include "Dll1.h"
    #include "CPlusDll.h"
    #include "CDll.h"
    int main()
    {
    	cout << "add: " << add(5, 3) << endl;
    	cout << "subtract: " << subtract(5, 3) << endl;
    
    	cout << "add_cplus: " << add_cplus(2, 2) << endl;
    	cout << "sub_cplus: " << sub_cplus(2, 2) << endl;
    
    	cout << "add_c: " << add_c(3, 3) << endl;
    	cout << "sub_c: " << sub_c(3, 3) << endl;
    
    	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

    编译运行可正常调用:
    7

    因为这里没有发生名字改编,因此可以直接通过CDll.h中的原始函数名调用.

    接着尝试在CDll中使用不同的调用约定,修改CDll项目:

    //CDll.h
    #ifdef CDLL_API
    #else
    #define  CDLL_API __declspec(dllimport)
    #endif
    
    int CDLL_API __stdcall add_c(int a, int b);
    int CDLL_API __stdcall sub_c(int a, int b);
    
    //CDll.c
    #define CDLL_API __declspec(dllexport)
    #include "CDll.h"
    
    int CDLL_API __stdcall add_c(int a, int b)
    {
    	return a + b;
    }
    
    int CDLL_API __stdcall sub_c(int a, int b)
    {
    	return a - b;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    重新编译生成,使用dumpbin命令查看,符合C编译__stdcall调用约定的名字改编规则:
    8

    再更新到C++项目cplusTest中,main.cpp代码无需变动, 重新编译会有以下报错提示:
    9
    这是因为在 C++项目中使用从 C编译生成的dll的导入函数add_c 和 sub_c 时,会不知道这两个函数的编译环境(因为在CDll项目的CDll.h中没有指定也没法指定extern “C”).

    第一次没有在声明实现中指定调用约定时,在C++项目中能正常调用,是因为没有发生名字改编.

    第二次在声明实现中有指定调用约定时,发生了名字改编,在C++项目中调用时,因为没有指定编译方式,这里应该会以C++编译__stdcall调用约定的改编名?add_c@@YGHHH@Z 和 ?sub_c@@YGHHH@Z 去dll中查找,然后找不到编译报错.

    因为没法在CDll项目的CDll.h头文件中加extern “C”限定符,但这里可以手动的在C++项目cplusTest项目包含的CDll.h加extern “C”(因为在C++项目中已经是C++编译环境了),这就告诉了C++编译器,add_c 和 sub_c两个函数是以C编译__stdcall调用约定生成的,这样在main.cpp中调用时就会以C编译__stdcall调用约定的改编名_add_c@8 和 _sub_c@8去dll中查找了,这时就能编译通过正常调用了:
    10

    其实这里也可以和 动态链接库(六)–解决不同编译器导致的名字改编问题 中一样通过条件编译的方式添加extern “C”, 因为C编译环境下是没有定义__cplusplus的,因此在编译生成DLL时extern “C”不会编译,放到C项目时,没有定义__cplusplus, extern “C” 也不会起作用, 而放到C++项目时,extern “C”才会参与编译:

    //CDll.h
    #ifdef CDLL_API
    #else
    #define  CDLL_API __declspec(dllimport)
    #endif
    
    //因为这里在C编译环境下,因此不能直接使用C++扩展的extern “C” 限定符
    //但可以通过条件编译方式添加extern “C”
    #ifdef __cplusplus
    extern "C"
    {
    #endif
    
    	int CDLL_API __stdcall add_c(int a, int b);
    	int CDLL_API __stdcall sub_c(int a, int b);
    
    #ifdef __cplusplus
    };
    #endif
    
    //CDll.c
    #define CDLL_API __declspec(dllexport)
    #include "CDll.h"
    
    int CDLL_API __stdcall add_c(int a, int b)
    {
    	return a + b;
    }
    
    int CDLL_API __stdcall sub_c(int a, int b)
    {
    	return a - b;
    }
    
    
    • 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

    更新到C++项目cplusTest中依旧可以正常编译运行:
    11

    这个示例证明无论是C编写dll跨编译器给C++使用,还是C++编写dll跨编译器给C使用,都能通过 动态链接库(六)–解决不同编译器导致的名字改编问题 中提到的使用条件编译的extern “C” 限定符解决不同编译器导致的名字改编问题.

    C++编写dll给C++项目使用

    同理,使用同一编译器(C++编译器)编写使用dll,就不用考虑 不同编译器导致的名字改编问题, 只需注意不同调用约定导致的名字改编问题即可.

    解决不同调用约定导致的名字改编问题, 同 动态链接库(七)–解决不同调用约定导致的名字改编问题一样, 这里不再赘述.

    这里需注意的是C++支持函数重载,重载函数导出时的名字改编问题应该如何解决?

    解决C++重载函数导出时的名字改编问题

    首先需了解下重载函数的概念:
    C++允许在同一命名空间内声明多个功能类似的同名函数,但是这些函数额形式参数—参数个数、类型或者顺序 必须不同. 这就是函数重载,即同名但形式参数不同就构成函数重载.

    函数重载常用来实现功能类似而所处理的数据类型不同的问题.

    **注意:**返回类型不同不构成重载.

    通过函数重载的定义,这里首先确定导入导出符号以及调用约定都不会构成函数重载:
    12
    13

    因为C中没有函数重载,因此这里不能在extern “C”代码段中声明重载函数:
    14

    所以这里在Dll1项目的Dll1.h中extern “C”外声明重载函数:

    //Dll1.h
    #ifdef DLL1_API
    #else
    #define DLL1_API __declspec(dllimport) 
    #endif
    
    #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
    
    
    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);
    
    
    • 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

    Dll1.cpp如下:

    //Dll.cpp
    #define DLL1_API __declspec(dllexport)
    #include "Dll1.h"
    
    int __stdcall add(int a, int b)
    {
    	return a + b;
    }
    
    int  __stdcall subtract(int a, int b)
    {
    	return a - b;
    }
    
    
    #include 
    #include 
    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);
    }
    
    
    • 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

    先不在模块定义文件中指定output导出符号:
    LIBRARY

    EXPORTS
    add
    subtract

    编译生成,使用dumpbin命令查看,output符合C++编译__stdcall调用约定的改编规则:
    15

    更新到C++项目cplusTest中,在cplusTest中的main.cpp中调用:

    //cplusTest中的main.cpp
    #include 
    using namespace std;
    
    #include "Dll1.h"
    #include "CPlusDll.h"
    #include "CDll.h"
    int main()
    {
    	//重载一调用
    	output(1, 2);
    	//重载二调用
    	output(1, 'C');
    	//重载三调用
    	output(1, 2, 'D');
    	system("pause");
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    可以看到输出窗口正常调用:
    16

    其实 C++编写的dllC++项目 用,两个项目编译环境一样且都是支持函数重载的,给C项目用的话也没有函数重载这一说法了.

    因此这里函数名虽然发生了不同调用约定导致的名字改编,但也能通过在使用Dll的项目包含的Dll.h头文件中显示的指定调用约定,使从dll导入的重载函数能够正常调用. 即无需解决因不同调用约定导致的重载函数导出时的名字改编问题.

    要想 不让导出的重载函数发生 不同调用约定导致的名字改编 ,参考 动态链接库(七)–解决不同调用约定导致的名字改编问题尝试使用模块定义文件指定ouput导出函数的符号,修改Dll1项目的Dll1.def如下:
    LIBRARY

    EXPORTS
    add
    subtract
    output

    重新编译,会有以下报错提示:
    17
    这里模块定义文件也不知道,这里指定的output要作为哪个重载版本的导出符号!即模块定义文件不支持“导出符号重载”.

    因为模块定义文件中只有一个符号,并没有像函数声明那样有函数名,返回类型,参数列表等,这里尝试在Dll1.def中添加多个output符号:
    LIBRARY

    EXPORTS
    add
    subtract
    output
    output
    output

    依旧编译报错提示:
    18

    所以这里貌似没法解决 不同调用约定的重载函数的名字改编问题.

    尝试在模块定义文件中给每个重载函数起别名:
    LIBRARY

    EXPORTS
    add
    subtract
    output1 = ?output@@YGXHH@Z ;重命名重载一改编后的名字
    output2 = ?output@@YGXHD@Z ;重命名重载二改编后的名字
    output3 = ?output@@YGXHHD@Z ;重命名重载三改编后的名字

    重新编译生成,使用dumpbin命令查看:
    19
    以output1为例,可以看到只是起了个别名,函数的相对地址都是@ILT + 155.

    而且这也不太符和函数重载的存在的意义了,相当于是另一个函数而不是同名函数了,即:

    void DLL1_API __stdcall output1(int a, int b);
    void DLL1_API __stdcall output2(int a, char b);
    void DLL1_API __stdcall output3(int a, int b, char c);
    
    void DLL1_API __stdcall output1(int a, int b)
    {
    	TCHAR buf[100] = { 0 };
    	_stprintf_s(buf, _T("\noutput重?载?一°?:êoint a = %d,ê?int b = %d\n\n"), a, b);
    	OutputDebugString(buf);
    }
    
    void DLL1_API __stdcall output2(int a, char b)
    {
    	TCHAR buf[100] = { 0 };
    	_stprintf_s(buf, _T("\noutput重?载?二t:êoint a = %d,ê?char b = %c\n\n"), a, b);
    	OutputDebugString(buf);
    }
    
    void DLL1_API __stdcall output3(int a, int b, char c)
    {
    	TCHAR buf[100] = { 0 };
    	_stprintf_s(buf, _T("\noutput重?载?三¨y:êoint a = %d,ê?int b = %d, char c = %c\n\n"), a, b, c);
    	OutputDebugString(buf);
    }
    
    
    
    • 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

    总结

    无论是C编写dll 给 C++项目用, 还是 C++编写dll 给 C项目 用, 解决不同编译器导致和不同调用约定导致的名字改编问题均可通过以下两步解决:
    动态链接库(六)–解决不同编译器导致的名字改编问题
    动态链接库(七)–解决不同调用约定导致的名字改编问题

    其中需要注意的是 exten “C” 限定符下不能有与C无关的代码出现.

    此外, 对于C++支持的重载函数的 不同调用约定导致的名字改编问题, 这里没法通过模块定义文件解决, 其实也无需解决, 因为支持重载函数的也必须是C++了, 因此这里只需在编写提供给调用者的Dll.h中显式的指定该导出函数的调用约定即可.

  • 相关阅读:
    linux xhost命令
    Telegram bot i南航打卡 部署 vercel 无服务器微服务
    发音测评 kaldi compute gop 保姆级实战指南
    Leetcode—53.最大子数组和【中等】
    企业清算有哪些类型?在哪里可以查看相关公告?
    基于YOLOv8模型的足球目标检测系统(PyTorch+Pyside6+YOLOv8模型)
    docker-compose部署mysql
    js - 原生的一些滚动属性和方法(scroll)
    java求两个数的百分比
    抓包整理外篇——————状态栏[ 四]
  • 原文地址:https://blog.csdn.net/SNAKEpc12138/article/details/126291576