Why Are Cdecl Calls Often Mismatched in the "Standard" P/Invoke Convention

Why are Cdecl calls often mismatched in the standard P/Invoke Convention?

This comes up repeatedly in SO questions, I'll try to turn this into a (long) reference answer. 32-bit code is saddled with a long history of incompatible calling conventions. Choices on how to make a function call that made sense a long time ago but are mostly a giant pain in the rear end today. 64-bit code has only one calling convention, whomever is going to add another one is going to get sent to small island in the South Atlantic.

I'll try to annotate that history and relevance of them beyond what's in the Wikipedia article. Starting point is that the choices to be made in how to make a function call are the order in which to pass the arguments, where to store the arguments and how to cleanup after the call.

  • __stdcall found its way into Windows programming through the olden 16-bit pascal calling convention, used in 16-bit Windows and OS/2. It is the convention used by all Windows api functions as well as COM. Since most pinvoke was intended to make OS calls, Stdcall is the default if you don't specify it explicitly in the [DllImport] attribute. Its one and only reason for existence is that it specifies that the callee cleans up. Which produces more compact code, very important back in the days when they had to squeeze a GUI operating system in 640 kilobytes of RAM. Its biggest disadvantage is that it is dangerous. A mismatch between what the caller assumes are the arguments for a function and what the callee implemented causes the stack to get imbalanced. Which in turn can cause extremely hard to diagnose crashes.

  • __cdecl is the standard calling convention for code written in the C language. Its prime reason for existence is that it supports making function calls with a variable number of arguments. Common in C code with functions like printf() and scanf(). With the side effect that since it is the caller that knows how many arguments were actually passed, it is the caller that cleans up. Forgetting CallingConvention = CallingConvention.Cdecl in the [DllImport] declaration is a very common bug.

  • __fastcall is a fairly poorly defined calling convention with mutually incompatible choices. It was common in Borland compilers, a company once very influential in compiler technology until they disintegrated. Also the former employer of many Microsoft employees, including Anders Hejlsberg of C# fame. It was invented to make argument passing cheaper by passing some of them through CPU registers instead of the stack. It is not supported in managed code due to the poor standardization.

  • __thiscall is a calling convention invented for C++ code. Very similar to __cdecl but it also specifies how the hidden this pointer for a class object is passed to instance methods of a class. An extra detail in C++ beyond C. While it looks simple to implement, the .NET pinvoke marshaller does not support it. A major reason that you cannot pinvoke C++ code. The complication is not the calling convention, it is the proper value of the this pointer. Which can get very convoluted due to C++'s support for multiple inheritance. Only a C++ compiler can ever figure out what exactly needs to be passed. And only the exact same C++ compiler that generated the code for the C++ class, different compilers have made different choices on how to implement MI and how to optimize it.

  • __clrcall is the calling convention for managed code. It is a blend of the other ones, this pointer passing like __thiscall, optimized argument passing like __fastcall, argument order like __cdecl and caller cleanup like __stdcall. The great advantage of managed code is the verifier built into the jitter. Which makes sure that there can never be an incompatibility between caller and callee. Thus allowing the designers to take the advantages of all of these conventions but without the baggage of trouble. An example of how managed code could stay competitive with native code in spite of the overhead of making code safe.

You mention extern "C", understanding the significance of that is important as well to survive interop. Language compilers often decorate the names of exported function with extra characters. Also called "name mangling". It is a pretty crappy trick that never stops causing trouble. And you need to understand it to determine the proper values of the CharSet, EntryPoint and ExactSpelling properties of a [DllImport] attribute. There are many conventions:

  • Windows api decoration. Windows was originally a non-Unicode operating system, using 8-bit encoding for strings. Windows NT was the first one that became Unicode at its core. That caused a rather major compatibility problem, old code would not have been able to run on new operating systems since it would pass 8-bit encoded strings to winapi functions that expect a utf-16 encoded Unicode string. They solved this by writing two versions of every winapi function. One that takes 8-bit strings, another that takes Unicode strings. And distinguished between the two by gluing the letter A at the end of the name of the legacy version (A = Ansi) and a W at the end of the new version (W = wide). Nothing is added if the function doesn't take a string. The pinvoke marshaller handles this automatically without your help, it will simply try to find all 3 possible versions. You should however always specify CharSet.Auto (or Unicode), the overhead of the legacy function translating the string from Ansi to Unicode is unnecessary and lossy.

  • The standard decoration for __stdcall functions is _foo@4. Leading underscore and a @n postfix that indicates the combined size of the arguments. This postfix was designed to help solve the nasty stack imbalance problem if the caller and callee don't agree about the number of arguments. Works well, although the error message isn't great, the pinvoke marshaller will tell you that it cannot find the entrypoint. Notable is that Windows, while using __stdcall, does not use this decoration. That was intentional, giving programmers a shot at getting the GetProcAddress() argument right. The pinvoke marshaller also takes care of this automatically, first trying to find the entrypoint with the @n postfix, next trying the one without.

  • The standard decoration for __cdecl function is _foo. A single leading underscore. The pinvoke marshaller sorts this out automatically. Sadly, the optional @n postfix for __stdcall does not allow it to tell you that your CallingConvention property is wrong, great loss.

  • C++ compilers use name mangling, producing truly bizarre looking names like "??2@YAPAXI@Z", the exported name for "operator new". This was a necessary evil due to its support for function overloading. And it originally having been designed as a preprocessor that used legacy C language tooling to get the program built. Which made it necessary to distinguish between, say, a void foo(char) and a void foo(int) overload by giving them different names. This is where the extern "C" syntax comes into play, it tells the C++ compiler to not apply the name mangling to the function name. Most programmer that write interop code intentionally use it to make the declaration in the other language easier to write. Which is actually a mistake, the decoration is very useful to catch mismatches. You'd use the linker's .map file or the Dumpbin.exe /exports utility to see the decorated names. The undname.exe SDK utility is very handy to convert a mangled name back to its original C++ declaration.

So this should clear up the properties. You use EntryPoint to give the exact name of the exported function, one that might not be a good match for what you want to call it in your own code, especially for C++ mangled names. And you use ExactSpelling to tell the pinvoke marshaller to not try to find the alternative names because you already gave the correct name.

I'll nurse my writing cramp for a while now. The answer to your question title should be clear, Stdcall is the default but is a mismatch for code written in C or C++. And your [DllImport] declaration is not compatible. This should produce a warning in the debugger from the PInvokeStackImbalance Managed Debugger Assistant, a debugger extension that was designed to detect bad declarations. And can rather randomly crash your code, particularly in the Release build. Make sure you didn't turn the MDA off.

When using PInvoke, why use __stdcall?

There is only one calling convention on x64 and so it does not matter which calling convention you specify. It is always ignored on x64.

On x86 it is important to make sure calling conventions match on both sides of the interface. So if you ever anticipate running your code on x86 it would be prudent to get that right now.

Pinvoke native method with pascal callingconvention

pascal was a 16-bit calling convention, in 32-bit code it was replaced by __stdcall. The identifier was retained for source compatibility. CallingConvention.StdCall is the default for pinvoke so nothing special needed.

You can find out more about DllImport.CallingConvention in this post, it mentions pascal.

Why does go's compiler gc use a different calling convention than C?

Because there's no advantage in having the same calling convention. Go code and C code cannot call each other directly even when the calling convention would be the same because Go uses split stacks.

OTOH, it makes sense in gccgo, as gcc supports C split stacks for some architectures. And, IIRC, there the calling convention is because of that compatible. (More details here.)

Disclaimer: I didn't ever actually used gccgo.

In the CDECL calling convention, can I reuse the arguments I pushed onto the stack?

In a word: No.

Consider this code:

__cdecl int foo(int a, int b)
{
a = 5;
b = 6;
return a + b;
}

int main()
{
return foo(1, 2);
}

This produced this asm output (compiled with -O0):

movl    $5, 8(%ebp)
movl $6, 12(%ebp)
movl 8(%ebp), %edx
movl 12(%ebp), %eax
addl %edx, %eax
popl %ebp
ret

So it is quite possible for a __cdecl function to stomp on the stack values.

That's not even counting the possibility of inlining or other optimization magic where things may not end up on the stack in the first place.

Memory corruption calling C++ dll from VB.NET

The default calling convention for the DllImportAttribte is listed as:

The default value for the CallingConvention field is Winapi, which in turn defaults to StdCall convention.

However the default for C++ programs is __cdecl

Your C++ code appears to use this default value by not specifying an alternative, so the VB signature should be:

<DllImport("D:\...\called_c.dll", EntryPoint:="add", ExactSpelling:=False, CallingConvention:=CallingConvention.Cdecl)>
Public Function add(ByRef a As Int32, ByRef b As Int32) As Int32
End Function

Alternatively, you may want to add the InAttribute to the arguments to prevent any copying back of the values by the interop marshaler.

<DllImport("D:\...\called_c.dll", EntryPoint:="add", ExactSpelling:=False, CallingConvention:=CallingConvention.Cdecl)>
Public Function add(<[In]()> ByRef a As Int32, <[In]()> ByRef b As Int32) As Int32
End Function


Related Topics



Leave a reply



Submit