views:

802

answers:

6

Hi!

A .NET application calls C dll. The C code allocates memory for a char array and returns this array as result. The .NET applications gets this result as a string.

The C code:

extern "C" __declspec(dllexport) char* __cdecl Run()
{
    char* result = (char*)malloc(100 * sizeof(char));
    // fill the array with data
    return result;
}

The C# code:

[DllImport("Unmanaged.dll")]
private static extern string Run();

...
string result = Run();
// do something useful with the result and than leave it out of scope

Some tests of it show that the garbage collector does not free the memory allocated by the C code.

Any help will be appreciated. :)

+2  A: 

You cannot free unmanaged memory from managed code. You need to write a routine in C that calls free on the pointer returned by the Run function and P/Invoke it from .NET.

Another option is to allocate unmanaged memory in .NET, pass the pointer to the C function which will fill it with data and finally free this pointer:

IntPtr ptr = Marshal.AllocHGlobal(100 * sizeof(char));
SomeUnmanagedFunc(ptr);
Marshal.FreeHGlobal(ptr);
Darin Dimitrov
Could you provide a simple example, please.
Alex
The allocation of memory in the .NET application assumes that we know the size of the array in advance. In the real solution (the problem one) it is total mystery :).
Alex
A: 

.NET memory MUST be allocated within the CLR to be cleared by the GC. You need to add a function to free the block within the C DLL.

Remember to free the memory within the same instance of the C DLL which created the memory. You cant mix and match.

Andrew Keith
That is incorrect. The CLR knows how to free certain types of memory blocks allocated from unmanaged code, for example memory allocated using SysAllocString or CoTaskMemAlloc.
Franci Penov
+1  A: 

Another way to do it would be to pass a managed string (a StringBuilder instance) through P/Invoke (as a parameter to your Run function).

That way no allocations are made on the unmanaged side.

In other words, you would have something like:

extern "C" __declspec(dllexport) void __cdecl Run(char* data)
{
    // fill the array with data
    // no return value (void)
}

and call it like this:

[DllImport("Unmanaged.dll", CharSet = CharSet.Ansi)]
static extern void Run(StringBuilder result);

StringBuilder result = new StringBuilder(100);
Run(result);
Groo
+3  A: 

Managed string is not the same as char*. What happens undercover is that the marshaling code in the interop layer makes a copy of the unmanaged string in order to convert it to a managed string, but it can't free that memory as it does not know how it was allocated.

However, you could try allocating and returning a BSTR instead of a char*. The interop layer deals way better with automation datatypes than classic unmanaged data types.

The reason why that matters is the way char* and BSTRs are allocated in the memory.

The char* buffers are allocated on the heap of the C++ runtime using private allocation/deallocation routines that the CLR knows nothing about, so there's no way it can delete that memory. And to make things even worse, the buffer that char* points can be allocated by an internal heap implementation of the dll code, or it might even point to a member variable in a private class.

The BSTRs on the other hand are allocated using the WIndows API SysAllocString and are freed by SyFreeStirng and since the CLR interop layer knows about these Windows APIs, it knows how to free a BSTR it got from unmanaged code.

Franci Penov
Thank you. Works fine.
Alex
+2  A: 

The P/Invoke marshaller will assume that memory for the return type was allocated with CoTaskMemAlloc() and will call CoTaskMemFree() to release it. If this was not done, the program will fail with an exception on Vista and Win7 but silently leak memory on XP. Using SysAllocString() can be made to work but you have to annotate the return type in the [DllImport] attribute. Not doing so will still cause a leak, without a diagnostic on Win7. A BSTR is not a pointer to a memory block allocated by CoTaskMemAlloc, there are 4 bytes in front of the pointed-to address that store the string size.

Either of the following combinations will work properly:

extern "C" __declspec(dllexport)
BSTR __stdcall ReturnsAString() {
  return SysAllocString(L"Hello world");
}

[DllImport(@"c:\projects\cpptemp1\debug\cpptemp1.dll")]
[return: MarshalAs(UnmanagedType.BStr)]   // NOTE: required!
private static extern string ReturnsAString();

Or:

extern "C" __declspec(dllexport)
const wchar_t* __stdcall ReturnsAString() {
  const wchar_t* str = L"Hello world";
  wchar_t* retval = (wchar_t*)CoTaskMemAlloc((wcslen(str)+1) * sizeof(wchar_t));
  wcscpy(retval, str);
  return retval;
}

[DllImport(@"c:\projects\cpptemp1\debug\cpptemp1.dll", CharSet=CharSet.Auto)]
private static extern string ReturnsAString();

You should consider allowing the client code to pass a buffer so there are no memory management issues. That ought to look similar to this:

extern "C" __declspec(dllexport)
void __stdcall ReturnsAString(wchar_t* buffer, size_t buflen) {
  wcscpy_s(buffer, buflen, L"Hello world");
}

[DllImport(@"c:\projects\cpptemp1\debug\cpptemp1.dll", CharSet=CharSet.Auto)]
private static extern void ReturnsAString(StringBuilder buffer, int buflen);
...
    StringBuilder sb = new StringBuilder(256);
    ReturnsAString(sb, sb.Capacity);
    string s = sb.ToString();
Hans Passant
A: 

I was reading some questions about PInvoke and i stoped here. I don't know if the problem is still relevant to you but i decided to post my answer to future readers.

It's about your last comment to Darin Dimitrov's answer. When the size of allocated memory is not known, the tipical solution is to call the unmanaged function with a null pointer and receive the size in an out parameter. Then, we alloc the needed space and call again the unmanaged function.

Exemple below:

//MANAGED SIDE  
IntPtr ptr = IntPtr.Zero;  
int size = 0;  
myFunc(ptr, out size);  
ptr = Marshal.AllocHGlobal(size);  
myFunc(ptr, out size);  
//Do your work..  
Marshal.FreeHGlobal(ptr);  



//UNMANEGED SIDE  
int myFunc(void* dest, size_t& size){  
   if(dest==NULL)  
       //calculate de size..  
       size = 'resul'  
       return 0;  
    }  
    // create the array and copy all elements   
    memcopy(dest, ... , size);  
    //free all allocated space in unmanaged and return success  
    return 0;  
}  
Zé Carlos