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

  • Post author:
  • Post category:其他




写在前面

这里主要介绍下在编写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();	
}



实现中的调用约定必须和声明时一致

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

//声明实现调用约定不一致时,编译会报错
//.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

这里

不推荐直接把调用约定放在宏定义中

,因为这样在类声明中就没有用宏了,而且

直接把调用约定放在声明或实现中



更清晰

的告知调用者该函数是何种调用约定:

//.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) 在函数声明实现中指定调用约定



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

修改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;
}

虽然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 结束整个命名.
    
  • __cdecl 调用约定: 规则同上面的__stdcall调用约定, 只是参数表的开始标识由上面的 @@YG 变为

    @@YA

    .

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

    @@YI

    .



版权声明:本文为SNAKEpc12138原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。