从托管C#释放非托管内存及其指针

时间:2012-09-05 03:04:47

标签: c# c++ memory-leaks buffer pinvoke

简短的问题是: 如何释放从本机DLL返回的内存作为托管代码中的ItrPtr?

详情: 假设我们有简单的函数将两个参数作为OUTPUT,第一个是参考指针到字节数组,第二个是参考Int。 该函数将根据一些规则分配字节数,并返回内存指针和字节大小以及返回值(1表示成功,0表示失败)。

下面的代码工作正常,我可以正确获取字节数组和字节数以及返回值,但是当我尝试使用指针(IntPtr)释放内存时,我得到异常:

  

Windows在TestCppDllCall.exe中触发了一个断点。

     

这可能是由于堆的损坏,这表示TestCppDllCall.exe或其加载的任何DLL中存在错误。

     

这也可能是由于用户在TestCppDllCall.exe具有焦点时按下F12。

     

输出窗口可能包含更多诊断信息。

说清楚:

  1. 下一个C#代码与其他DLL函数正常工作具有相同的签名,释放内存工作没有任何问题。

  2. 如果您需要更改分配内存方法或添加任何其他代码,则接受(C)代码中的任何修改。

  3. 我需要的所有功能都是Native DLL函数接受两个参数引用(Byte数组和int,在c#[IntPtr of byte array and int]中)根据一些规则填充一些值并返回函数结果(成功或失败)。


  4. CppDll.h

    #ifdef CPPDLL_EXPORTS
    #define CPPDLL_API __declspec(dllexport)
    #else
    #define CPPDLL_API __declspec(dllimport)
    #endif
    
    extern "C" CPPDLL_API int writeToBuffer(unsigned char *&myBuffer, int& mySize);
    

    CppDll.cpp

    #include "stdafx.h"
    #include "CppDll.h"
    
    extern "C" CPPDLL_API int writeToBuffer(unsigned char*& myBuffer, int& mySize)
    {
        mySize = 26;
    
        unsigned char* pTemp = new unsigned char[26];
        for(int i = 0; i < 26; i++)
        {
            pTemp[i] = 65 + i;
        }
        myBuffer = pTemp; 
        return 1;
    }
    

    C#代码:

    using System;
    using System.Text;
    using System.Runtime.InteropServices;
    
    namespace TestCppDllCall
    {
        class Program
        {
            const string KERNEL32 = @"kernel32.dll";
            const string _dllLocation = @"D:\CppDll\Bin\CppDll.dll";
            const string funEntryPoint = @"writeToBuffer";
    
            [DllImport(KERNEL32, SetLastError = true)]
            public static extern IntPtr GetProcessHeap();
            [DllImport(KERNEL32, SetLastError = true)]
            public static extern bool HeapFree(IntPtr hHeap, uint dwFlags, IntPtr lpMem);
            [DllImport(_dllLocation, EntryPoint = funEntryPoint, CallingConvention = CallingConvention.Cdecl)]
            public static extern int writeToBuffer(out IntPtr myBuffer, out int mySize);
    
            static void Main(string[] args)
            {
                IntPtr byteArrayPointer = IntPtr.Zero;
                int arraySize;
                try
                {
                    int retValue = writeToBuffer(out byteArrayPointer, out arraySize);
                    if (retValue == 1 && byteArrayPointer != IntPtr.Zero)
                    {
                        byte[] byteArrayBuffer = new byte[arraySize];
                        Marshal.Copy(byteArrayPointer, byteArrayBuffer, 0, byteArrayBuffer.Length);
                        string strMyBuffer = Encoding.Default.GetString(byteArrayBuffer);
                        Console.WriteLine("Return Value : {0}\r\nArray Size : {1}\r\nReturn String : {2}",
                            retValue, arraySize, strMyBuffer);
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Error calling DLL \r\n {0}", ex.Message);
                }
                finally
                {
                    if (byteArrayPointer != IntPtr.Zero)
                        HeapFree(GetProcessHeap(), 0, byteArrayPointer);
                }
                Console.ReadKey();
            }
        }
    }
    

    当我调试此代码时,我在行中设置断点(返回1),缓冲区的值为:

    myBuffer = 0x031b4fc0 "ABCDEFGHIJKLMNOPQRSTUVWXYZ‎‎‎‎««««««««î‏"
    

    当函数调用返回时,我在C#代码中得到相同的值,值为:

    52121536
    

    结果我得到了正确的内存指针,我能够获得字节数组值,如何在C#中用这个指针释放这些内存块?

    如果有任何不清楚或有任何错字,请告诉我,我不是母语为英语的人。

3 个答案:

答案 0 :(得分:13)

简短回答:您应该在DLL中添加一个单独的方法,为您释放内存。

答案很长:在DLL实现中可以使用不同的方式分配内存。释放内存的方式必须与分配内存的方式相匹配。例如,分配有new[](带方括号)的内存需要使用delete[](而不是deletefree)进行释放。 C#没有为您提供机制;你需要将指针发送回C ++。

extern "C" CPPDLL_API void freeBuffer(unsigned char* myBuffer) {
    delete[] myBuffer;
}

答案 1 :(得分:7)

如果要在本机代码中分配自己的内存,请使用CoTaskMemAlloc,然后可以使用Marshal.FreeCoTaskMem释放托管代码中的指针。 CoTaskMemAlloc被描述为“在基于COM的应用程序中共享内存的唯一方法”(参见http://msdn.microsoft.com/en-us/library/windows/desktop/aa366533(v=vs.85).aspx

如果您需要将使用CoTaskMemAlloc分配的内存与本机C ++对象一起使用,则可以使用 placement new 来初始化内存,就像使用new运算符一样。例如:

void * p = CoTaskMemAlloc(sizeof(MyType));
MyType * pMyType = new (p) MyType;

这不会分配内存new只调用预分配内存上的构造函数。

调用Marshal.FreeCoTaskMem不会调用该类型的析构函数(如果只需要释放内存,则不需要);如果你需要通过调用析构函数来做更多的空闲内存,你必须提供一个本机方法来执行该操作并进行P / Invoke。无论如何都不支持将本机类实例传递给托管代码。

如果您需要使用其他API分配内存,则需要通过P / Invoke在托管代码中公开内存,以便在托管代码中释放它。

答案 2 :(得分:3)

  HeapFree(GetProcessHeap(), 0, byteArrayPointer);

不,那不行。堆处理错误,CRT使用HeapCreate()创建自己的堆。它隐藏在CRT数据中,你无法实现它。从技术上讲,您可以从GetProcessHeaps()找回句柄,除非您不知道它是哪一个。

指针也可能是错误的,CRT可能会从HeapAlloc()返回的指针中添加一些额外的信息来存储调试数据。

您需要导出一个调用delete []的函数来释放缓冲区。或者编写C ++ / CLI包装器,以便在包装器中使用delete []。额外要求C ++代码和包装器使用完全相同版本的CRT DLL(需要/ MD)。这几乎总是要求您可以重新编译C ++代码。