51
votes

I'm trying to write some C# code that calls a method from an unmanaged DLL. The prototype for the function in the dll is:

extern "C" __declspec(dllexport) char *foo(void);

In C#, I first used:

[DllImport(_dllLocation)]
public static extern string foo();

It seems to work on the surface, but I'm getting memory corruption errors during runtime. I think I'm pointing to memory that happens to be correct, but has already been freed.

I tried using a PInvoke code gen utility called "P/Invoke Interop Assistant". It gave me the output:

[System.Runtime.InteropServices.DLLImportAttribute(_dllLocation, EntryPoint = "foo")]
public static extern System.IntPtr foo();

Is this correct? If so, how do I convert this IntPtr to a string in C#?

3

3 Answers

83
votes

You must return this as an IntPtr. Returning a System.String type from a PInvoke function requires great care. The CLR must transfer the memory from the native representation into the managed one. This is an easy and predictable operation.

The problem though comes with what to do with the native memory that was returned from foo(). The CLR assumes the following two items about a PInvoke function which directly returns the string type

  1. The native memory needs to be freed
  2. The native memory was allocated with CoTaskMemAlloc

Therefore it will marshal the string and then call CoTaskMemFree(...) on the native memory blob. Unless you actually allocated this memory with CoTaskMemAlloc this will at best cause a crash in your application.

In order to get the correct semantics here you must return an IntPtr directly. Then use Marshal.PtrToString* in order to get to a managed String value. You may still need to free the native memory but that will dependent upon the implementation of foo.

30
votes

You can use the Marshal.PtrToStringAuto method.

IntPtr ptr = foo();
string str = Marshal.PtrToStringAuto(ptr);
1
votes

Current answers are not complete so I just wanted to post full solution in one place. Returning IntPtr instead of string doesn't solve any problem at all as you still have to free native memory allocated in C script. The best possible solution is to allocate bytes buffer on managed side and pass the memory to C script that will write string into that buffer not allocating memory at all.

Returning a string from C is exactly like this:

//extern "C" __declspec(dllexport) uint32_t foo(/*[out]*/ char* lpBuffer, /*[in]*/ uint32_t uSize)
uint32_t __stdcall foo(/*[out]*/ char* lpBuffer, /*[in]*/ uint32_t uSize)
{
  const char szReturnString[] = "Hello World";
  const uint32_t uiStringLength = strlen(szReturnString);

  if (uSize >= (uiStringLength + 1))
  {
    strcpy(lpBuffer, szReturnString);
    // Return the number of characters copied.
    return uiStringLength;
  }
  else
  {
    // Return the required size
    // (including the terminating NULL character).
    return uiStringLength + 1;
  }
}

C# code:

[DllImport(_dllLocation, CallingConvention=CallingConvention.StdCall, CharSet=CharSet.Ansi)]
private static extern uint foo(IntPtr lpBuffer, uint uiSize);

private static string foo()
{
    // First allocate a buffer of 1 byte.
    IntPtr lpBuffer = Marshal.AllocHGlobal(1);
    // Call the API. If the size of the buffer
    // is insufficient, the return value in
    // uiRequiredSize will indicate the required
    // size.
    uint uiRequiredSize = foo(lpBuffer, 1);

    if (uiRequiredSize > 1)
    {
        // The buffer pointed to by lpBuffer needs to be of a
        // greater size than the current capacity.
        // This required size is the returned value in "uiRequiredSize"
        // (including the terminating NULL character).
        lpBuffer = Marshal.ReAllocHGlobal(lpBuffer, (IntPtr)uiRequiredSize);
        // Call the API again.
        foo(lpBuffer, uiRequiredSize);
    }

    // Convert the characters inside the buffer
    // into a managed string.
    string str = Marshal.PtrToStringAnsi(lpBuffer);

    // Free the buffer.
    Marshal.FreeHGlobal(lpBuffer);
    lpBuffer = IntPtr.Zero;

    // Display the string.
    Console.WriteLine("GetString return string : [" + str + "]");

    return str;
}

There is simpler way to manage memory allocation/deallocation on C# side using StringBuilder:

[DllImport("TestDLL.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
private static extern uint foo(StringBuilder lpBuffer, UInt32 uiSize);

private static string foo()
{
    StringBuilder sbBuffer = new StringBuilder(1);
    uint uiRequiredSize = foo(sbBuffer, (uint)sbBuffer.Capacity);

    if (uiRequiredSize > sbBuffer.Capacity)
    {
        // sbBuffer needs to be of a greater size than current capacity.
        // This required size is the returned value in "uiRequiredSize"
        // (including the terminating NULL character).
        sbBuffer.Capacity = (int)uiRequiredSize;
        // Call the API again.
        foo(sbBuffer, (uint)sbBuffer.Capacity);
    }

    return sbBuffer.ToString();
}

There's a good topic explaining different ways of returning string from C/C++ code.