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

Kad*_*ura 53 c# c++ pinvoke cdecl stdcall

我正在开发一个相当大的代码库,其中C++函数是从C#调用P /.

我们的代码库中有很多调用,比如......

C++:

extern "C" int __stdcall InvokedFunction(int);
Run Code Online (Sandbox Code Playgroud)

使用相应的C#:

[DllImport("CPlusPlus.dll", ExactSpelling = true, SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
    private static extern int InvokedFunction(IntPtr intArg);
Run Code Online (Sandbox Code Playgroud)

我已经搜索了网(在我有能力的范围内),因为为什么存在这种明显的不匹配.例如,为什么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);
Run Code Online (Sandbox Code Playgroud)

再一次,extern "C"C++代码和CallingConvention.CdeclC#中都有.为什么不CallingConvention.Stdcall呢?或者,此外,为什么__stdcallC++中存在?

提前致谢!

Han*_*ant 146

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

我将尝试注释它们的历史和相关性,超出维基百科文章中的内容.起点是,如何进行函数调用的选择是传递参数的顺序,存储参数的位置以及调用后如何清理.

  • __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编组并没有支持它.您无法解释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后缀找到入口点,然后再尝试没有.

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

  • C++编译器使用名称修改,产生真正奇怪的名字,如"?? 2 @ YAPAXI @ Z","operator new"的导出名称.由于它支持函数重载,这是一个必要的恶魔.它最初被设计为预处理器,使用传统的C语言工具来构建程序.这使得有必要通过赋予它们不同的名称来区分,例如a 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.

  • 谢谢你的教训.我对P/Invoke有了新的尊重.我更欣赏__clrcall和C#中缺少多重继承. (4认同)
  • +1我有一些温和的悬念评论.你谈到`__fastcall`但这实际上是一个MS调用约定.通常,它可以被视为fastcall约定族的一部分.MS fastcall,Borland fastcall等.MS版本`__fastcall`只使用两个x86寄存器:ECX,EDX.Borland版本今天作为"寄存器"约定存在于Delphi世界中,它使用三个寄存器.EAX被添加到混合中. (4认同)
  • @Hans:**谁将要添加另一个将被送到南大西洋的小岛**=>你现在可以发送微软,因为他们添加了一个新的`__vectorcall`! (3认同)

Sim*_*ier 7

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