托管代码中的免费非托管内存分配

时间:2009-12-19 08:33:13

标签: .net memory-management unmanaged dllimport

.NET应用程序调用C dll。 C代码为char数组分配内存并返回此数组作为结果。 .NET应用程序将此结果作为字符串获取。

C代码:

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

C#代码:

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

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

它的一些测试表明垃圾收集器没有释放C代码分配的内存。

任何帮助将不胜感激。 :)

7 个答案:

答案 0 :(得分:7)

托管字符串与char *不同。底层发生的事情是,互操作层中的封送代码会生成非托管字符串的副本,以便将其转换为托管字符串,但它无法释放该内存,因为它不知道它是如何分配的。

但是,您可以尝试分配并返回BSTR而不是char *。与传统的非托管数据类型相比,互操作层可以更好地处理自动化数据类型。

重要的原因是char *和BSTR在内存中的分配方式。

char *缓冲区使用CLR一无所知的私有分配/解除分配例程在C ++运行时的堆上分配,因此无法删除该内存。更糟糕的是,char *指向的缓冲区可以由dll代码的内部堆实现分配,或者甚至可以指向私有类中的成员变量。

另一方面,BSTR使用WIndows API SysAllocString进行分配,并由SyFreeStirng释放,并且由于CLR互操作层知道这些Windows API,因此它知道如何从非托管代码中释放BSTR。

答案 1 :(得分:7)

P / Invoke marshaller将假设返回类型的内存是使用CoTaskMemAlloc()分配的,并将调用CoTaskMemFree()来释放它。如果没有这样做,程序将失败,Vista和Win7上有例外,但在XP上默默泄漏内存。使用SysAllocString()可以使用,但您必须在[DllImport]属性中注释返回类型。如果不对Win7进行诊断,不这样做仍会导致泄漏。 BSTR 不是指向由CoTaskMemAlloc分配的内存块的指针,指向地址前面有4个字节存储字符串大小。

以下任一组合都可以正常使用:

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();

或者:

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();

您应该考虑允许客户端代码传递缓冲区,以免出现内存管理问题。这看起来应该与此相似:

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();

答案 2 :(得分:6)

您无法从托管代码中释放非托管内存。您需要在C中编写一个例程,该例程在free函数返回的指针上调用Run并从.NET调用它。

另一个选择是在.NET中分配非托管内存,将指针传递给C函数,该函数将用数据填充它,最后释放这个指针:

IntPtr ptr = Marshal.AllocHGlobal(100 * sizeof(char));
SomeUnmanagedFunc(ptr);
Marshal.FreeHGlobal(ptr);

答案 3 :(得分:3)

另一种方法是通过P / Invoke传递托管字符串(StringBuilder实例)(作为Run函数的参数)。

这样就不会在非管理方面进行分配。

换句话说,你会有类似的东西:

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

并将其称为:

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

StringBuilder result = new StringBuilder(100);
Run(result);

答案 4 :(得分:2)

我正在阅读有关PInvoke的一些问题,我在这里停留。我不知道问题是否仍然与您有关,但我决定将我的答案发布给未来的读者。

这是你对Darin Dimitrov的回答的最后评论。当分配的内存大小未知时,典型的解决方案是使用空指针调用非托管函数并在out参数中接收大小。然后,我们分配所需的空间并再次调用非托管函数。

以下示例:

//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;  
}  

答案 5 :(得分: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);
    }

}

答案 6 :(得分:-1)

.NET内存必须在CLR中分配,以便GC清除。您需要添加一个函数来释放C DLL中的块。

请记住在创建内存的C DLL的同一实例中释放内存。你不能混搭。