DLL导出与调用约定
一般来说,从DLL导出函数有两种方法。一种是使用.def文件;另一种是使用__declspec(dllexport)。
使用上面两种方法各有优缺点。使用.def文件就是需要额外维护,当导出函数更改名字或者追加导出函数。而使用__declspec(dllexport)则需要注意使用的调用约定,在使用C++编译器时。
调用约定
在使用C++或者在常见的windows API的声明头文件常见的WINAPI,而WINAPA就是__stdcall。在C++还有__cdecl。而__pascal和__thiscall都已忽略了。宏PASCAL也用的是__stdcall。
一般来说,调用约定只关乎参数的压栈和最后的清理工作。还有就是名字修饰。比如:
void func(char, short, int, double)
使用__cdecl调用:
使用C编译器编译后的修饰名字为:_func。清理栈的工作由调用方做。
使用__stdcall和__thiscall调用:
使用C编译器编译后的修饰名字为_func@20。清理栈的工作由被调方做。
使用__fastcall调用:
使用C编译器编译后的修饰名字为@func@20。清理栈的工作由被调方做。
名字修饰
从上面的图也可看见,压栈都是从右到左,而名字修饰的方法却不一样。C编译器的修饰方法相对来说比较简单。以_func@20为例,_是追加的,func就是函数的名字,@是追加的,20为参数的字节数。C++的修饰方法相对来说比较复杂。下图是使用C++编译器产生的名字,使用__declspec(dllexport)和__stdcall。
下图是使用__cdecl编译:
大致一看都差不多,只是在名字后的有个字母不一样,__stdcall得是YG,而__cdecl的是YA。而这也正好指明了C++的名字修饰的规则,简单来说就是:
・名字(可以包含命名空间)
・调用约定
・返回值
・参数列表
调用约定修饰规则:
代号 | 调用约定 |
@@YG | __stdcall |
@@YA | __cdecl |
@@YI | __fastcall |
类型修饰规则:
代号 | 含义 |
X | void |
D | char |
E | unsigned char |
F | short |
H | int |
I | unsigned int |
J | long |
K | unsigned long(DWORD) |
M | float |
N | double |
_N | bool |
U | struct |
PA | pointer |
PB | const pointer |
参数表后以”@Z”标示整个名字的结束,如果该函数无参数,则以”Z”标识结束。
最后一点:如果我们直接使用__declspec(dllexport)导出函数的话,最好使用C编译器。如果直接用C++编译器,导出的函数名就像上面一样。在动态使用DLL时调用GetProcAddress使用原始函数名称一般来说会失败。例如:
GetProcAddress(handle, "func");
所以一般使用C编译器,比如这样:
#ifdef __cplusplus // If used by C++ code extern "C" { // we need to export the C interface #endif __declspec(dllexport) VOID __cdecl func(char, short, int, double) { ... } #ifdef __cplusplus } #endif
这时,修饰后的名字就是这样:
一般导入函数时也建议这样:
#ifdef __cplusplus extern "C" { #endif // _declspec(dllimport) VOID func(char, short, int, double); // or VOID func(char, short, int, double); #ifdef __cplusplus } #endif
第一种写法在效率上来说会更好点。
参考:http://hi.baidu.com/luosiyong/item/c8664533f56b66c31b9696e3