写在前面
这里主要介绍下在编写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()
{
//实现
}
这里
不推荐直接把调用约定放在宏定义中
,因为这样在类声明中就没有用宏了,而且
直接把调用约定放在声明或实现中
能
更清晰
的告知调用者该函数是何种调用约定:
//.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, 如下:
这样在导出函数声明实现中不指定调用约定时,默认的就是__stdcall调用约定了.
在导出函数声明实现中显示指定调用约定
这里先将DLL1项目的默认调用约定切换回之前的__cdecl, 如下:
然后修改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命令查看会发生同样名字改编:
各调用约定的命名规则
这里以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
.