Free unmanaged memory allocation from managed code
Asked Answered
I

7

8

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. :)

Impatience answered 19/12, 2009 at 8:33 Comment(0)
K
8

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.

Kythera answered 19/12, 2009 at 8:59 Comment(0)
R
8

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();
Repertory answered 19/12, 2009 at 13:47 Comment(0)
T
6

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);
Tripping answered 19/12, 2009 at 8:37 Comment(3)
Could you provide a simple example, please.Impatience
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 :).Impatience
@Darin - How do you think will the first method work reliably? Are there any tricks or something that must be implemented. Should I wrap call to unmanaged memory free operation in IDisposable method for my class that handles unmanaged memory?Leucocytosis
M
3

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);
Marcille answered 19/12, 2009 at 8:42 Comment(1)
As Darin's answer this one would work if the size of necessary memory is known.Leucocytosis
J
2

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;  
}  
Jannet answered 26/2, 2010 at 14:17 Comment(1)
Quite interesting approach but it assumes that the execution time of managed code is very fast so we can call it twice. In practice managed code could a) execute slowly b) return slightly different result sets so the allocated memory won't be enoughLeucocytosis
M
0
public class Memory
{
    [DllImport("kernel32.dll")]
    private static extern bool SetProcessWorkingSetSize(IntPtr proc, int min, int max);

    //[DllImport("kernel64.dll")]
    //private static extern bool SetProcessWorkingSetSize(IntPtr proc, int min, int max);

    public String Errores = "";
    public void Limpiar()
    {
        try
        {
            Process Mem;
            Mem = Process.GetCurrentProcess();
            SetProcessWorkingSetSize(Mem.Handle, -1, -1);
            Mem = null;
        }
        catch (Exception ex)
        {
            Errores = ex.ToString() + " " + ex.StackTrace.ToString();
        }

    }
}
public class LimpiadodeMemoria
{

    private Boolean Monitorear;
    private Boolean Salida;
    private String ElMensajeBitacoras;
    private String Error;

    public delegate void Errores(string Elerror);
    public event Errores OnErrores;

    public delegate void Bitacora(string LaBitacora);
    public event Bitacora OnBitacora;

    public void Iniciar()
    {
        Monitorear = true;
        Salida = false;
        ElMensajeBitacoras = "LimpiadodeMemoria - " + DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss") + " Iniciada";
        OnBitacora(ElMensajeBitacoras);
        while (Monitorear == true)
        {
            Salida = false;
            Memory _Memori = new Memory();
            _Memori.Limpiar();
            Error = _Memori.Errores;
            _Memori = null;
            if (Error != "")
            {
                OnErrores(Error);
            }
            Salida = true;
            System.Threading.Thread.Sleep(1000);
        }
    }

    public void Detener()
    {
        Monitorear = false;
        while (Salida == false)
        {
            System.Threading.Thread.Sleep(100);
        }
        ElMensajeBitacoras = "LimpiadodeMemoria - " + DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss") + " Detenida";
        OnBitacora(ElMensajeBitacoras);
    }

}
Maidel answered 5/3, 2021 at 20:49 Comment(0)
O
-1

.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.

Overunder answered 19/12, 2009 at 8:38 Comment(1)
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.Kythera

© 2022 - 2024 — McMap. All rights reserved.