Search code examples
c#c++.netpinvoke

StringBuilder marshalling leads to PInvokeStackImbalance exception


I'm trying to understand .Net marshalling and now i'm working with strings. I write an app but it doesn't work as expected. What am I doing wrong here?

C++

EXPORT void GetString(wchar_t **pBuff)
{
    std::wcout << "Initial string was: " << *pBuff << std::endl << "Changing its value..." << std::endl;
    *pBuff = L"Hello from C++";
}

C#:

const string DLLNAME = "CppLib.dll";
static void Main(string[] args)
{
    var sb = new StringBuilder(256).Append("Hello from C#");
    GetString(ref sb);
    Console.WriteLine("String from Dll is {0}", sb);
}

[DllImport(DLLNAME, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode)]
private static extern void GetString(ref StringBuilder pBuff);

but when I run it I get PInvokeStackImbalance exception.

Actual output is:

Initial string was: Hello from C#

Changing its value...

BANG - Exception is thrown here

how to fix it? I tried to change CallingConvention - but it didn't help, of course, because I use StdCall here. But I have no more ideas.

In pure C++ this code works fine:

#include <iostream>
using namespace std;

void GetString(wchar_t **pBuff)
{
    std::wcout << "Initial string was: " << *pBuff << std::endl << "Changing its value..." << std::endl;
    *pBuff = L"Hello from C++!";
}

int main() {
    wchar_t *pBuff = L"blablablablablablabla";
    GetString(&pBuff);
    std::wcout << pBuff;
    return 0;
}

Solution

  • You have 3 problems here. The MDA probably just stepped in on your last attempt, the one that made you give up. And sure, you already know why, CallingConvention.StdCall is wrong.

    You cannot use StringBuilder, it has to be passed without ref and is intended to allow the callee to copy the string contents in the buffer. You need an extra argument, bufferLength, that ensures that the native code cannot destroy the GC heap. Pass the Capacity value. Use wcscpy_s() to copy the string content.

    But you are returning a pointer. That doesn't make the pinvoke marshaller very happy, it is a troublesome memory management issue. It assumes that somebody has to clean up the string buffer. When you let the marshaller do it then it will call CoTaskMemFree(), that rarely comes to a good end.

    You'll have to fool it and declare the argument as ref IntPtr instead. Then use Marshal.PtrToStringUni() in your C# code to retrieve the string. Otherwise a nasty failure mode if you don't return a pointer to a string literal but allocate on the heap or dangle a wchar[] pointer. Copying is the safe way.