• 动态链接库(扩展)--调用约定


    写在前面

    这里主要介绍下在编写DLL项目时, 如何指定导出函数的调用约定, 注意事项, 以及各调用约定之前的区别.

    指定导出函数的调用约定时的注意事项

    这里以WinAPI的__stdcall调用约定为例,在函数名前返回类型后加调用约定声明:

    //声明实现分离
    //.h头文件,可以这样
    int __stdcall add(int a, int b);
    void __stdcall test();
    
    //类
    class Tmp
    {
    	void __stdcall tmpTest();
    };
    
    //.cpp源文件实现,需这样
    int __stdcall add(int a, int b)
    {
    	return a + b;
    }
    
    void __stdcall test()
    {
    	//实现
    }
    
    //类成员函数声明时指定了调用约定,实现可省略调用约定
    void Tmp::tmpTest()
    {
    	//实现
    }
    
    //即等价于这样,注意只有类的成员函数才能省略
    void __stdcall Tmp::tmpTest()
    {
    	//实现
    }
    
    //也可以这样,没有声明,直接实现时指定调用约定
    int __stdcall add(int a, int b)
    {
    	return a + b;
    }
    
    void __stdcall test()
    {
    	//实现
    }
    
    class Tmp
    {
    	void __stdcall tmpTest()
    	{
    		//实现
    	}
    };
    
    //不可以这样
    //即调用约定必须在函数名前,返回类型后
    __stdcall int add(int a, int b);
    
    class Tmp
    {
    	__stdcall void tmpTest();	
    }
    
    
    • 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

    实现中的调用约定必须和声明时一致(类的成员函数除外,类的成员函数声明时指定了调用约定,实现时可省略),不然编译时报错提示不同的修饰类型:

    //声明实现调用约定不一致时,编译会报错
    //.h 头文件, 默认C调用约定__cdecl
    int __stdcall add(int a, int b);
    
    class Tmp
    {
    	//这里是默认的C调用约定__cdecl
    	void tmpTest();	//等价于 void __cdecl tmpTest();
    };
    
    
    //.cpp源文件
    int add(int a, int b)		//默认C调用约定
    {
    	return a + b;	
    }
    
    void __stdcall Tmp::tmpTest()
    {
    	//实现
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    1

    这里不推荐直接把调用约定放在宏定义中,因为这样在类声明中就没有用宏了,而且直接把调用约定放在声明或实现中更清晰的告知调用者该函数是何种调用约定:

    //.h 头文件
    #ifdef DLL1_API
    #else
    #define DLL1_API __declspec(dllimport) __stdcall
    #endif
    
    int DLL1_API add(int a, int b);
    
    class DLL1_API Tmp		//这里会报错
    {
    	void tmpTest();
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    如何指定导出函数的调用约定

    这里有两种方式指定导出函数的调用约定:
    (1) 修改项目属性中默认的调用约定
    (2) 在函数声明实现中指定调用约定

    修改项目属性中默认的调用约定

    修改DLL1项目的默认的调用约定,项目属性 –》C/C++ -》高级 –》调用约定选择__stdcall, 如下:
    2
    这样在导出函数声明实现中不指定调用约定时,默认的就是__stdcall调用约定了.

    在导出函数声明实现中显示指定调用约定

    这里先将DLL1项目的默认调用约定切换回之前的__cdecl, 如下:
    3

    然后修改DLL1项目代码如下:

    //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
    
    //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;
    }
    
    • 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

    虽然DLL项目的默认调用约定是__cdecl, 但通过显式的在 add 和 subtract 的函数声明和实现中添加__stdcall调用约定声明, 来将这两个函数的调用约定指定为__stdcall.

    使用dumpbin命令查看会发生同样名字改编:
    4

    各调用约定的命名规则

    这里以C 和 C++ 编译环境下, 不同调用约定的情况举例说明:

    C编译时名字改编规则

    • __stdcall 调用约定: 在导出函数名前加一个下划线前缀, 在导出函数名后加一个@, @后接其所有参数的总字节数, 格式为_functionName@number.
    • __cdecl 调用约定: 仅在导出函数名前加一个下划线()前缀, 格式为_functionName.
    • __fastcall调用约定: 在导出函数名前加一个@, 函数名后也加一个@, 然后第二个@后接其所有参数的总字节数, 格式为@functionName@number.

    C++编译时名字改编规则

    • __stdcall 调用约定: 以 ? 标识函数名的开始, 后跟函数名, 函数名后以 @@YG 标识参数表的开始, 后跟参数类型, 参数类型以以下代号
      表示:
      X: void
      D: char
      E: unsigned char
      F: short
      H: int
      I: unsigned int
      J: long
      K: unsigned log
      M: float
      N: double
      _N: bool
      PA: 表示指针, PA后会带代号, 表明指针的类型. 如果相同类型的指针连续出现, 以 0 代替重复, 一个 0 表示一次重复, 注意只针对指针类型.

       参数表的第一项为该函数的返回值类型, 其后依次为参数的类型.
       
       参数表后以 **@Z** 标识整个名字的结束, 如果该函数无参数, 则以 Z 标识结束.
       
       例:
       void __declspec(dllexport) __stdcall test(int a, int b);
       改编后的函数名为: ?test@@YGXHH@Z
       说明: 以?标识函数名开始, @@YG标识参数表开始, 参数表中第一项为返回类型void(对应X), 后跟第一个参数类型int(对应H), 第二个参数类型同一个参数类型一致, 但不是指针类型, 因此这里H标识int, 最后以@Z 标识整个名字的结束
       
       int __declspec(dllexport) __stdcall add(int* a, int* b, int* c, char d);
       改编后的函数名为: ?add@@YGHPAH00D@Z
       说明: 依旧以?标识函数名开始, @@YG标识参数表开始, 参数表中第一项为返回类型int(对应H), 后跟第一个参数类型int*(对应PA-指针, H指针类型), 因为第二个参数类型也为int*, 因此这里以一个 0 表示重复, 第三个参数类型依旧是int*, 因此后再加一个 0 表示重复指针类型, 最后一个参数类型为char(对应D), 也依旧以 @Z 结束整个命名.
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
    • __cdecl 调用约定: 规则同上面的__stdcall调用约定, 只是参数表的开始标识由上面的 @@YG 变为 @@YA.

    • __fastcall 调用约定: 规则同上面的__stdcall调用约定, 只是参数表的开始标识由上面的 @@YG 变为 @@YI.

  • 相关阅读:
    数据分析,从了解你的数据开始,数据探索性分析工具包pandas-profiling
    【C++】特殊类设计
    【安全】正则回溯绕过练习简单案例
    网络安全等级保护2.0详解
    jvm之jvm基础
    vim编译器的使用
    【Lodash】 Filter 与Map 的结合使用
    检测nodejs内存泄露
    2022-Aug-28
    二硫化钼-石墨烯纳米复合材料(MoS2-rGO)|PEI修饰MoS2二硫化钼纳米材料|金纳米颗粒功能化二硫化钼(AuNPs@MoS2)
  • 原文地址:https://blog.csdn.net/SNAKEpc12138/article/details/126274119