使用指针释放托管C#中的非托管内存

11

简单说,问题是: 如何在托管代码中释放从原生DLL返回的ItrPtr内存?

详细说明: 假设我们有一个简单的函数,接受两个参数作为输出。第一个参数是引用指向字节数组的指针,第二个参数是引用整数。 根据一些规则,该函数将分配一定量的字节并返回内存指针、字节大小和返回值(成功为1,失败为0)。

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

Windows has triggered a breakpoint in TestCppDllCall.exe.

This may be due to a corruption of the heap, which indicates a bug in TestCppDllCall.exe or any of the DLLs it has loaded.

This may also be due to the user pressing F12 while TestCppDllCall.exe has focus.

The output window may have more diagnostic information.

为了澄清事情:

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

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

  3. 我需要的所有功能都是本地DLL函数,接受两个参数(Byte数组和int,在C#中为IntPtr和int),根据某些规则填充它们,并返回函数结果(成功或失败)。


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();
        }
    }
}
当我调试这段代码时,在(return 1)这一行设置了断点,缓冲区的值为:
myBuffer = 0x031b4fc0 "ABCDEFGHIJKLMNOPQRSTUVWXYZ‎‎‎‎««««««««î‏"

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

52121536

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

如果有什么不清楚或打错字,请告诉我,我不是以英语为母语的。

3个回答

13

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

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

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

感谢您的快速回复。 我希望有一种直接在C#中释放非托管内存的方法。 - khaled
1
我同意dasblinkenlight的观点:最干净的方法可能是向你的.dll添加一个“free”方法。但Peter Ritchie也是正确的:C#肯定可以通过互操作调用相应的“free”(例如FreeCoTaskMem())来释放.dll分配的内存(例如CoTaskMemAlloc())。从C#中无法做到的一件事是“删除”,如果.dll执行了C++“new”。malloc/free:可以。CoTaskMemAlloc/FreeCoTaskMem:也可以。New/free:绝对不行。 - paulsm4
@paulsm4 请解释如何在C#中调用malloc/free? - awattar
@user1142979:Peter Ritchie在他下面的回复中解释了如何使用CoTaskMemAlloc()和Marshal.FreeCoTaskMem()。您可以在这里阅读更多信息:使用平台调用进行数据编组。请注意评论中的“必须使用CoTaskMemAlloc而不是new运算符,因为托管端的代码将调用Marshal.FreeCoTaskMem来释放此内存”。 - paulsm4
@paulsm4 我明白,但是当dll使用malloc分配内存时,应该不使用Marshal.FreeCoTaskMem来释放内存。我有这样的印象,你已经找到了在这种情况下调用相应的“free”的方法。 - awattar

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 在托管代码中公开它,以便在托管代码中释放它。

+1 - CoTaskMemAlloc是在Windows中匹配托管/非托管(或任何其他跨语言)分配的官方方式。 - Alexei Levenkov

3
  HeapFree(GetProcessHeap(), 0, byteArrayPointer);

不行,那样行不通。堆句柄是错误的,CRT使用HeapCreate()创建了自己的堆。它被隐藏在CRT数据中,您无法访问它。您可以从GetProcessHeaps()找回句柄,但不知道哪一个是正确的。
指针也可能不正确,CRT可能已经从HeapAlloc()返回的指针中添加了一些额外的信息来存储调试数据。
您需要导出一个函数来调用delete[]来释放缓冲区。或者编写一个C++/CLI包装器,以便在包装器中使用delete[]。还需要C++代码和包装器使用完全相同版本的CRT DLL(/MD必需)。这几乎总是要求您重新编译C++代码。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接