1. extern "C"的作用
extern "C"的作用是声明以c语言的格式编译当前代码:
- c语言没有函数重载
- 编译后的函数名若有参数以"xxx@数字"结尾,“数字"为所有参数占用的内存大小(4位对齐);若无参数则结尾不含”@数字"
- 编译后的开头字符与调用约定__cdecl(无开头字符)、__stdcall(以‘_’开头)、__fastcall(以‘@’开头)有关
上代码,两个函数,分别以c和c++格式编译,看看效果是什么:
//ApiExport.h
// extern "C" 与 默认c++ 方式的区别
extern "C" __declspec(dllexport) void func1_c();
__declspec(dllexport) void func1_cpp();
//ApiExport.cpp
void func1_c(){}
void func1_cpp(){}
很简单的两个函数,没有输入也没有输出。编译,查看生产的dll导出的函数名称:
可以看到声明extern "C"编译的函数名和我们定义的函数名是一致的,而以c++方式编译的函数名加了前缀和后缀。
再分析一下编译后的“func1_c”,既没有前缀又没有后缀,说明调用约定是__cdecl(默认调用约定)。
2. __cdecl、 __stdcall、__fastcall
调用约定,约定的什么?
在表面看来,约定了编译后的函数名称;实际约定的是由调用者还是被调用者清理堆栈上的参数。
有两派:
__cdecl :调用者清理
__stdcall :被调用者清理
__fastcall比较特殊,没有被标准化,各个编译器实现不一样。
但是,但是!!!在Windows x64环境下编译代码时,只有一种调用约定,也就是说,32位下的各种约定在64位下统一成一种了。(X86调用约定 - 维基百科)
再深入就涉及到汇编了,到此为止;下面讲一下编译后的函数名称的区别。
还是上图上代码:
这里只写出声明代码,实际是没有定义的函数是不会编译的。
//ApiExport.h
#ifdef DLLAPI
#define DLLAPI __declspec(dllexport)
#else
#define DLLAPI __declspec(dllimport)
#endif
#ifndef DLLAPI_EXTERN_C
#define DLLAPI_EXTERN_C extern "C" DLLAPI
#endif
// 以c++方式编译
DLLAPI_EXTERN_C void __stdcall fun_extern_c_stdcall();
DLLAPI_EXTERN_C int __stdcall fun_extern_c_stdcall0();
DLLAPI_EXTERN_C int __stdcall fun_extern_c_stdcall1(int param);
DLLAPI_EXTERN_C int __stdcall fun_extern_c_stdcall2(int param, char param2);
DLLAPI_EXTERN_C int __stdcall fun_extern_c_stdcall3(int param, char param2,long param3);
DLLAPI_EXTERN_C int __cdecl fun_extern_c_cdecl(int param);
DLLAPI_EXTERN_C int __fastcall fun_extern_c_fastcall(int param);
// 以c方式编译
DLLAPI void __stdcall fun_cpp_stdcall();
DLLAPI int __stdcall fun_cpp_stdcall0();
DLLAPI int __stdcall fun_cpp_stdcall1(int param);
DLLAPI int __stdcall fun_cpp_stdcall2(int param, char param2);
DLLAPI int __stdcall fun_cpp_stdcall3(int param, char param2, long param3);
DLLAPI int __cdecl fun_cpp_cdecl(int param);
DLLAPI int __fastcall fun_cpp_fastcall(int param);
上图:
C++编译时函数名修饰约定规则:
__stdcall调用约定:
1)、以"?“标识函数名的开始,后跟函数名;
2)、函数名后面以”@@YG"标识参数表的开始,后跟参数表;
3)、参数表以代号表示:
X–void ,
D–char,
E–unsigned char,
F–short,
H–int,
I–unsigned int,
J–long,
K–unsigned long,
M–float,
N–double,
_N–bool,
PA(32位)/PEA(64位)–表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以"0"代替,一个"0"代表一次重复;
4)、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;
5)、参数表后以"@Z"标识整个名字的结束,如果该函数无参数,则以"Z"标识结束。
其格式为"?functionname@@YG***@Z"或"?functionname@@YG*XZ",例如:
int __stdcall func_cpp_stacall3(int param,char param2,long param3)
编译后函数名:“?func_cpp_stacall3@@YGHHDJ@Z”
“?” 以c++方式编译
“@@” 后面为参数定义
“YG” 调用约定为__stdcall
“H” 返回值为 int 型
“H” 第一个参数为 int 型
“D” 第二个参数为 char 型
“J” 第三个参数为 long 型
“@Z” 有参函数名后缀
void __stdcall func_cpp_stdcall()
编译后函数名:“?func_cpp_stdcall@@YGXXZ”
“?” 以c++方式编译
“@@” 后接参数定义
“X” 返回值为 void
“XZ” 无参函数后缀
__cdecl调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的"@@YG"变为"@@YA"。
__fastcall调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的"@@YG"变为"@@YI"。
C编译时函数名修饰约定规则:
**__stdcall调用约定:**
以“_”开头,以“@数字”结尾,数字为参数占用的内存字节数。
**__cdecl调用约定:**
不改变函数名称。
**__fastcall调用约定:**
以“@”开头,以“@数字”结尾,数字为参数占用的内存字节数。
总结:
假设函数为:int func(int arg1,char arg2,long arg3);
以下列方式编译后的函数名为:
cdecl | stdcall | fastcall | |
---|---|---|---|
以C方式编译 | func | _func@12 | @func@12 |
以C++方式编译 | ?func@@YAHHDJ@Z | ?func@@YGHHDJ@Z | ?func@@YIHHDJ@Z |
为什么是@12?int占4byte,char占1byte,long占4byte,4+1+4=9?只想说一句:内存4位对齐!。注意:指针在32位程序中是4字节,64位程序是8字节
3. 用.def文件定义导出函数
从上一节可以看出,只有以C方式编译且调用约定为“__cdecl”(extern “C” __declspec(dllimport) int function)时函数名不变,其他时候函数名都会变,在大部分时候都没有影响(c/c++提供.lib文件即可、c#需要声明调用约定的特性),但是在delphi调用c/c++ dll时不能直接使用定义的函数名,只能使用编译后的函数名。为了统一编译后的函数名称,只能使用模块定义文件(def文件)定义函数名称,使用模块定义文件可以不用__declspec(dllimport)声明函数,最好都统一在.def文件中声明,方便管理。
看一下.def文件格式:
LIBRARY dllExportTest
EXPORTS
fun_cpp_stdcall @1
- 文件第一条LIBRARY语句不是必须的,但若有LIBRARY语句则后面必须是生成的dll的文件名;
- EXPORTS语句之后每一行列出一个已定义的函数名称(未定义的名称也可以,但没什么用)
- 函数名称后接“@函数序号”,函数序号取值范围为 1 - N(N为导出函数的总数),且不能重复
在Visual Studio中使用.def文件:
- 为项目添加模块定义文件
- 在文件中添加导出函数
- 设置到项目环境(添加模块定义文件时会自动设置)