为什么Cdecl调用在“标准”P / Invoke惯例中常常不匹配?
我正在研究一个相当大的代码库,其中C ++功能是从C#调用的。
我们的代码库中有很多调用,例如…
C ++:
extern "C" int __stdcall InvokedFunction(int);
与相应的C#:
[DllImport("CPlusPlus.dll", ExactSpelling = true, SetLastError = true, CallingConvention = CallingConvention.Cdecl)] private static extern int InvokedFunction(IntPtr intArg);
我已经搜寻了网络(就我的能力而言),为什么存在这种明显的不匹配。 例如,为什么C#中有一个Cdecl,C ++中有__stdcall? 显然,这会导致堆栈被清除两次,但是,在这两种情况下,变量都以相同的相反顺序被压入堆栈,这样我就不会看到任何错误,尽管返回信息在在调试过程中尝试跟踪?
从MSDN: http : //msdn.microsoft.com/en-us/library/2x8kf7zx%28v=vs.100%29.aspx
// explicit DLLImport needed here to use P/Invoke marshalling [DllImport("msvcrt.dll", EntryPoint = "printf", CallingConvention = CallingConvention::Cdecl, CharSet = CharSet::Ansi)] // Implicit DLLImport specifying calling convention extern "C" int __stdcall MessageBeep(int);
再一次,C ++代码中有extern "C"
,而C#中有CallingConvention.Cdecl
。 为什么不叫CallingConvention.Stdcall
? 或者,为什么在C ++中有__stdcall
?
提前致谢!
这个问题反复出现,我会试着把它变成一个(长)参考答案。 长期以来,32位的代码是不兼容的调用约定。 如何做一个很有意义的函数调用的选择,但在今天的后端大多是一个巨大的痛苦。 64位代码只有一个调用约定,任何人要添加另一个将被送到南大西洋的小岛。
除了维基百科文章中的内容外,我会试着去注释它们的历史和相关性。 起点是如何进行函数调用的选择是传递参数的顺序,在哪里存储参数以及在调用之后如何清理。
-
__stdcall
通过16位Windows和OS / 2中使用的16位pascal调用约定进入Windows编程。 这是所有Windows API函数以及COM使用的惯例。 由于大多数pinvoke都是为了进行操作系统调用,如果不在[DllImport]属性中明确指定,则Stdcall是默认值。 它存在的唯一原因是它指定了被调用者清理。 这产生了更紧凑的代码,在他们不得不在640千字节的RAM中挤压GUI操作系统的时候非常重要。 它最大的缺点是危险 。 调用者所假定的是函数的参数与被调用者实现的内容之间的不匹配,导致堆栈失去平衡。 这反过来可能会导致非常难以诊断崩溃。 -
__cdecl
是用C语言编写的代码的标准调用约定。 它存在的主要原因是它支持用不定数量的参数进行函数调用。 在C代码中通常具有printf()和scanf()等函数。 有副作用,因为它是知道有多少参数实际传递的调用者,它是清理的调用者。 忘记CallingConvention = CallingConvention.Cdecl在[DllImport]声明是一个非常普遍的错误。 -
__fastcall
是一个定义相当差的调用约定,具有相互不兼容的选择。 在Borland编译器中,这是一个曾经在编译器技术上非常有影响力的公司,在它们解体之前,这种情况很常见。 也是许多微软员工的前雇主,包括C#的Anders Hejlsberg。 它是通过传递一些通过CPU寄存器而不是堆栈来使参数传递更便宜的。 托管代码中不支持标准化。 -
__thiscall
是为C ++代码发明的调用约定。 与__cdecl非常相似,但是它也指定了如何将类的隐藏指针传递给类的实例方法。 C ++之外的一个额外的细节。虽然看起来很简单,但是.NET的pinvoke编组不支持。 一个主要的原因,你不能捏C ++代码。 并发症并不是调用约定,它是这个指针的正确值。 由于C ++支持多重继承,这会变得非常复杂。 只有C ++编译器才能知道究竟需要传递什么。 只有完全相同的C ++编译器为C ++类生成代码,不同的编译器在如何实现MI以及如何优化它们方面做出了不同的选择。 -
__clrcall
是托管代码的调用约定。 这是其他的混合, 这个指针传递像__thiscall,优化的参数传递像__fastcall,参数顺序像__cdecl和调用清理像__stdcall。 托管代码的巨大优势是内置在抖动中的验证程序 。 这确保了调用者和被调用者之间永远不会有不兼容。 因此,让设计师能够利用所有这些惯例的优点,但却没有麻烦。 尽管代码安全的开销,托管代码如何保持与本机代码的竞争力的一个例子。
你提到extern "C"
,理解这个意义是重要的,也是为了生存interop。 语言编译器通常用额外的字符来修饰导出函数的名字。 也叫“名字捣毁”。 这是一个相当蹩脚的伎俩,永远不会造成麻烦。 您需要了解它以确定[DllImport]属性的CharSet,EntryPoint和ExactSpelling属性的正确值。 有许多约定:
-
Windows api装饰。 Windows最初是一个非Unicode操作系统,对字符串使用8位编码。 Windows NT是第一个成为Unicode核心的。 这导致了一个相当大的兼容性问题,旧的代码将无法在新的操作系统上运行,因为它将8位编码的字符串传递给需要utf-16编码的Unicode字符串的winapi函数。 他们通过编写每个winapi函数的两个版本来解决这个问题。 一个需要8位字符串,另一个需要Unicode字符串。 并且通过在传统版本(A = Ansi)的名称末尾加上字母A和在新版本(W =宽)的末尾加上一个W来区分两者。 如果函数不带字符串,则不会添加任何内容。 pinvoke marshaller在没有您的帮助的情况下自动处理这个问题,它只会尝试找到所有3个可能的版本。 但是,您应该始终指定CharSet.Auto(或Unicode),将Ansi字符串转换为Unicode的传统函数的开销是不必要的和有损的。
-
__stdcall函数的标准修饰是_foo @ 4。 领先的下划线和@n后缀,表示参数的组合大小。 这个后缀的目的是帮助解决讨厌的堆栈不平衡问题,如果调用者和被调用者不同意参数的数目。 虽然错误信息不是很好,但是编译器会告诉你找不到入口点。 值得注意的是,Windows在使用__stdcall时不会使用这种装饰。 这是故意的,给程序员一个GetProcAddress()参数的权利。 pinvoke编组也自动照顾这个,首先试图找到与@n后缀的入口点,然后尝试没有。
-
__cdecl函数的标准修饰是_foo。 一个领先的下划线。 编辑编组人员自动排序。 可悲的是,__stdcall的可选@n后缀不允许它告诉你,你的CallingConvention属性是错误的,很大的损失。
-
C ++编译器使用名称修饰,生成真正奇怪的外观名称,例如“?? 2 @ YAPAXI @ Z”,导出的名称为“operator new”。 由于支持函数重载,这是一个必要的罪恶。 它最初被设计成一个使用传统C语言工具来构建程序的预处理器。 这使得有必要通过给它们不同的名字来区分,比如
void foo(char)
和void foo(int)
超载。 这是extern "C"
语法起作用的地方,它告诉C ++编译器不要将名称改为应用名称。 大多数编写interop代码的程序员故意用它来使其他语言的声明更容易编写。 这实际上是一个错误,装饰是非常有用的,以防止错配。 您可以使用链接器的.map文件或Dumpbin.exe / exports实用程序来查看装饰名称。 undname.exe SDK实用程序非常方便将重名的名称转换回其原始的C ++声明。
所以这应该清除属性。 您使用EntryPoint来给出导出的函数的确切名称,可能不是您想要在自己的代码中调用它的好匹配项,特别是对于C ++错位的名称。 而你使用ExactSpelling告诉pinvoke编组不要试图找到替代名字,因为你已经给了正确的名字。
我现在要把我的书写痉挛了。 您的问题标题的答案应该清楚,Stdcall是默认的,但是用C或C ++编写的代码不匹配。 而你的[DllImport]声明不兼容。 这应该在调试器中从PInvokeStackImbalance管理调试器助手中产生一个警告,这是一个调试器扩展,用于检测错误的声明。 而且可以相当随机地崩溃你的代码,特别是在发布构建。 确保你没有关闭MDA。
cdecl
和stdcall
在C ++和.NET之间都是有效和可用的,但是它们应该在两个不受管理的和被管理的世界之间保持一致。 所以你的InvokedFunction的C#声明是无效的。 应该是stdcall。 MSDN示例只提供了两个不同的示例,一个使用stdcall(MessageBeep),另一个使用cdecl(printf)。 他们是无关的。