为什么Cdecl调用在“标准”P / Invoke公约中经常不匹配?

时间:2013-03-27 13:58:57

标签: c# c++ pinvoke stdcall cdecl

我正在开发一个相当大的代码库,其中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

提前致谢!

2 个答案:

答案 0 :(得分:142)

在SO问题中反复出现这种情况,我会尝试将其转化为(长期)参考答案。 32位代码背负着不兼容的调用约定的悠久历史。选择如何进行一个很久以前有意义的功能调用,但今天主要是后端的巨大痛苦。 64位代码只有一个调用约定,而另外一个将被发送到南大西洋的小岛上。

我会尝试在Wikipedia article之外注释它们的历史和相关性。起点是,如何进行函数调用的选择是传递参数的顺序,存储参数的位置以及调用后如何清理。

  • __stdcall通过16位Windows和OS / 2中使用的旧的16位pascal调用约定进入Windows编程。它是所有Windows api函数以及COM使用的约定。由于大多数pinvoke旨在进行OS调用,因此如果您未在[DllImport]属性中明确指定,则Stdcall是默认值。它存在的唯一原因是它指定被调用者清理。这产生了更紧凑的代码,非常重要,因为他们不得不在640千字节的RAM中挤压GUI操作系统。它最大的缺点是危险。调用者假设的是函数的参数和被调用者实现的内容导致堆栈失衡的不匹配。这反过来会导致极难诊断崩溃。

  • __cdecl是用C语言编写的代码的标准调用约定。它存在的主要原因是它支持使用可变数量的参数进行函数调用。 C代码中常见的函数有printf()和scanf()。副作用是因为调用者知道实际传递了多少个参数,所以调用者可以清理。忘记CallingConvention = [DllImport]声明中的CallingConvention.Cdecl是非常常见错误。

  • __fastcall是一个定义相当差的调用约定,具有互不兼容的选择。这在Borland编译器中很常见,这家公司曾经在编译器技术方面非常有影响力,直到他们解体。也是许多微软员工的前雇主,包括C#成名的Anders Hejlsberg。它的发明是为了让参数传递更便宜,通过CPU寄存器而不是堆栈传递它们的一些。由于标准化不佳,托管代码不支持它。

  • __thiscall是为C ++代码发明的调用约定。与__cdecl非常相似,但它还指定了如何将类对象的隐藏 this 指针传递给类的实例方法。 C ++中除C之外的额外细节虽然看起来很简单,但是.NET pinvoke marshaller 支持它。您无法解释C ++代码的主要原因。复杂性不是调用约定,它是 this 指针的正确值。由于C ++对多重继承的支持,这可能会变得非常复杂。只有C ++编译器才能确定究竟需要传递的内容。并且只有生成C ++类代码的完全相同的C ++编译器,不同的编译器在如何实现MI以及如何优化它方面做出了不同的选择。

  • __clrcall是托管代码的调用约定。它是其他的混合,这个指针传递像__thiscall,优化的参数传递像__fastcall,参数顺序像__cdecl和调用者清理像__stdcall。托管代码的巨大优势是内置于抖动中的验证程序。这确保了调用者和被调用者之间永远不会存在不兼容性。因此,允许设计师利用所有这些惯例的优势,但没有麻烦的包袱。尽管使代码安全的开销,托管代码如何与本机代码保持竞争的示例。

你提到extern "C",了解其重要性以及互操作的重要性。语言编译器通常使用额外字符修饰导出函数的名称。也被称为"名称缩减"。这是一个非常糟糕的技巧,永远不会停止造成麻烦。您需要了解它以确定[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后缀,表示参数的组合大小。如果调用者和被调用者不同意参数的数量,则此后缀旨在帮助解决令人讨厌的堆栈不平衡问题。效果很好,虽然错误信息不是很好,但是pinvoke marshaller会告诉你它无法找到入口点。值得注意的是,Windows在使用__stdcall时,使用此装饰。这是故意的,让程序员有机会获得正确的GetProcAddress()参数。 pinvoke marshaller也会自动处理这个问题,首先尝试使用@n后缀找到入口点,然后尝试使用@n后缀。

  • __cdecl函数的标准修饰是_foo。一个领先的下划线。 pinvoke marshaller自动对此进行排序。遗憾的是,__stdcall的可选@n后缀不允许它告诉你你的CallingConvention属性是错误的,很有损失。

  • C ++编译器使用名称修改,生成真正奇怪的名称,例如" ?? 2 @ YAPAXI @ Z","运算符new"的导出名称。由于它支持函数重载,这是一个必要的恶魔。它最初被设计为预处理器,使用传统的C语言工具来构建程序。这使得有必要通过赋予它们不同的名称来区分void foo(char)void foo(int)重载。这是extern "C"语法发挥作用的地方,它告诉C ++编译器将名称修改应用于函数名称。编写互操作代码的大多数程序员故意使用它来使用另一种语言的声明更容易编写。这实际上是一个错误,装饰对于捕捉不匹配非常有用。您可以使用链接器的.map文件或Dumpbin.exe / exports实用程序来查看修饰的名称。 undname.exe SDK实用程序非常便于将受损的名称转换回其原始的C ++声明。

所以这应该清理属性。您使用EntryPoint来提供导出函数的确切名称,这可能与您在自己的代码中调用它的名称不匹配,尤其是对于C ++受损名称。并且您使用ExactSpelling告诉pinvoke marshaller不要尝试查找替代名称,因为您已经给出了正确的名称。

我现在为我的写作抽筋做了一段时间。你的问题标题的答案应该是清楚的,Stdcall是默认的,但是用C或C ++编写的代码是不匹配的。并且您的[DllImport]声明兼容。这应该在调试器中从PInvokeStackImbalance Managed Debugger Assistant产生一个警告,这是一个用于检测错误声明的调试器扩展。而且可能会随机崩溃您的代码,尤其是在发布版本中。确保你没有关闭MDA。

答案 1 :(得分:7)

cdeclstdcall在C ++和.NET之间都是有效且可用的,但它们应该在两个非托管和托管世界之间保持一致。因此,InvokedFunction的C#声明无效。应该是stdcall。 MSDN示例只提供了两个不同的示例,一个使用stdcall(MessageBeep),另一个使用cdecl(printf)。他们是无关的。