C# 在C+;中包装非托管指针时发生堆损坏+/CLI
我在使用本机C代码、C++/CLI和C#的.NET应用程序中遇到堆损坏问题。这是我第一次真正进入这里的荒野 应用程序的结构是C#用于GUI和总体控制流,C++/CLI用于包装本机C函数,本机C函数用于处理数据。这些本机C函数通常接受指向数组(例如:int*)和维度的本机指针作为输入。C++/CLI将这些低级函数包装为高级组合处理函数,C#调用高级函数 有时,我确实需要在C#级别分配非托管内存,然后将相同的内存包传递给几个不同的C++/CLI函数 为了在我的C#和C++/CLI层中自由地传递这些数组,我围绕托管指针创建了一个瘦包装器类。在C++/CLI层定义的名为ContiguousArray的包装器如下所示:C# 在C+;中包装非托管指针时发生堆损坏+/CLI,c#,.net,garbage-collection,c++-cli,mixed-mode,C#,.net,Garbage Collection,C++ Cli,Mixed Mode,我在使用本机C代码、C++/CLI和C#的.NET应用程序中遇到堆损坏问题。这是我第一次真正进入这里的荒野 应用程序的结构是C#用于GUI和总体控制流,C++/CLI用于包装本机C函数,本机C函数用于处理数据。这些本机C函数通常接受指向数组(例如:int*)和维度的本机指针作为输入。C++/CLI将这些低级函数包装为高级组合处理函数,C#调用高级函数 有时,我确实需要在C#级别分配非托管内存,然后将相同的内存包传递给几个不同的C++/CLI函数 为了在我的C#和C++/CLI层中自由地传递这些数
template <typename T>
public ref class ContiguousArray
{
public:
ContiguousArray<T>(int size)
{
_size = size;
p = (T*) calloc(_size,sizeof(T));
}
T& operator[](int i)
{
return p[i];
}
int GetLength()
{
return _size;
}
~ContiguousArray<T>()
{
this->!ContiguousArray<T>();
}
!ContiguousArray<T>()
{
if (p != nullptr)
{
free(p);
p = nullptr;
}
}
T* p;
int _size;
};
// Some non-templated variants of ContiguousArray for passing out to other .NET languages
public ref class ContiguousArrayInt16 : public ContiguousArray<Int16>
{
ContiguousArrayInt16(int size) : ContiguousArray<Int16>(size) {}
};
我认为我在使用这个包装类处理非托管内存时非常小心,但我一直在应用程序中看到堆损坏和访问冲突问题。我从不将指向非托管内存的本机指针保留在ContiguousArray对象有效的作用域之外
从理论上讲,这三个用例中的任何一个都有可能导致堆损坏吗?我是否在我的ContinguousArray实现中丢失了一些关键内容?我担心垃圾收集器可能变得有点过于热心,在我真正处理完托管对象之前就清理了它们
用例1:我是否保证在结束大括号之前不会调用终结器?是否有可能.NET已经决定不再使用该对象,并且在我仍然有指向其内部内存的指针时将其清除?GC::KeepAlive是否需要用于堆栈对象
用例2:我是否需要在最后使用GC::KeepAlive来保证在第三次函数调用之前不释放对象?如果我改为写下:
nativeFunction(unmanagedArray->p,unmanagedArray->GetLength())
用例3:我看不出这里有什么错误,但也许我遗漏了什么?首先,我假设
连续数组中的成员被称为大小而不是\u大小
只是一个打字错误
在访问违规方面,我认为案例3没有任何错误。在案例2中,在使用其指针完成nativeFunction
之前,肯定可以对数组进行垃圾收集。我不确定案例1是否有同样的问题。如果使用GC::KeepAlive
,是否修复了访问冲突
堆损坏可能意味着在中释放内存时,内存已经被释放!ContiguousArray()
。本机方法是否释放过数组或ContiguousArrays
是否交换过拥有的数组
另外,检查calloc
是否没有返回nullptr
是个好主意。多亏了写下我的问题(最好的老师)的魔力和tsandy和Hans的建议,我已经详细研究了垃圾收集器在处理非托管资源时的行为。以下是我的发现:
我使用的设计模式有缺陷。如果垃圾收集器决定不再使用托管对象句柄(^),即使该句柄仍在作用域中,也可以对其进行垃圾收集。适当(但较慢)的设计模式不允许访问非托管资源,除非通过其托管包装器类的方法。如果允许指向非托管资源的指针或引用从包装器中泄漏出来,那么获取它们的代码需要非常小心,以确保拥有它们的包装器不会被收集/最终确定。因此,像ContiguousArray这样设计的包装器类不是一个好主意
也就是说,这种模式很快!下面是如何逐案抢救的方法
用例1实际上还可以!在C++/CLI中使用堆栈语义可确保在包装器超出范围时进行确定性终结。在包装器超出范围后保留指针仍然是一个错误,但总的来说是安全的。我对我的C++/CLI代码做了大量修改,以强烈支持堆栈语义,包括尽可能使用句柄引用(%)作为仅由我的C++/CLI代码调用的函数的参数
用例2是危险的,需要修复。有时,您无法避免使用句柄,因此需要使用来强制垃圾收集器保留对象,直到KeepAlive调用为止
{
// Create an array for the low level code
ContiguousArray<float>^ unmanagedArray = gcnew ContiguousArray<float>(1024);
cliFunction(unmanagedArray);
anotherCLIFunction(unmanagedArray);
float* unmanagedArrayPointer = unmanagedArray->p;
int returnCode = nativeFunction(unmanagedArrayPointer, unmanagedArray->GetLength());
GC::KeepAlive(unmanagedArray); // Force the wrapper to stay alive while native operations finish.
return returnCode;
}
{
//为低级代码创建一个数组
ContiguousArray^unmanagedArray=gcnew ContiguousArray(1024);
cliFunction(非托管Darray);
另一个CLI函数(非托管Darray);
float*unmanagedarraypoter=unmanagedArray->p;
int returnCode=nativeFunction(unmanagedarraypoter,unmanagedArray->GetLength());
GC::KeepAlive(unmanagedArray);//强制包装在本机操作完成时保持活动状态。
返回代码;
}
用例3在技术上是不安全的。在调用finalCLIFunction之后,.NET垃圾收集器可能会立即决定不再需要非托管Darray(取决于finalCLIFunction的实现)。但是,如果不需要的话,用诸如KeepAlive之类的实现细节来加重C代码的负担是没有意义的。相反,不要试图从C#代码访问任何非托管的内容,并确保所有C++/CLI函数的实现都为它们自己的参数调用KeepAlive(如果这些参数是句柄)
int finalCLIFunction(ContiguousArrayInt16^ unmanagedArray)
{
// Do a bunch of work with the unmanaged array
Int16* ptr = unmanagedArray->p;
for(int i=0; i < unmanagedArray->GetLength(); i++)
{
ptr[i]++;
}
// Call KeepAlive on the calling arguments to ensure they stay alive
GC::KeepAlive(unmanagedArray);
return 0;
}
int finalCLIFunction(连续数组int16^非托管数组)
{
//对非托管阵列执行大量工作
Int16*ptr=unmanagedArray->p;
对于(int i=0;iGetLength();i++)
{
ptr[i]++;
}
//对调用参数调用KeepAlive以确保它们保持活动状态
基帕利夫
{
ContiguousArrayInt16 unmanagedArray = new UnmanagedArray(1024);
cliFunction(unmanagedArray);
unmanagedArray = anotherCLIFunctionThatReplacesUnmanagedArray(unmanagedArray); // Unmanaged array is possibly replaced, original gets collected at some point
returnCode = finalCLIFunction(unmanagedArray);
// Do something with return code like show the user
} // Memory gets freed at some point
{
// Create an array for the low level code
ContiguousArray<float>^ unmanagedArray = gcnew ContiguousArray<float>(1024);
cliFunction(unmanagedArray);
anotherCLIFunction(unmanagedArray);
float* unmanagedArrayPointer = unmanagedArray->p;
int returnCode = nativeFunction(unmanagedArrayPointer, unmanagedArray->GetLength());
GC::KeepAlive(unmanagedArray); // Force the wrapper to stay alive while native operations finish.
return returnCode;
}
int finalCLIFunction(ContiguousArrayInt16^ unmanagedArray)
{
// Do a bunch of work with the unmanaged array
Int16* ptr = unmanagedArray->p;
for(int i=0; i < unmanagedArray->GetLength(); i++)
{
ptr[i]++;
}
// Call KeepAlive on the calling arguments to ensure they stay alive
GC::KeepAlive(unmanagedArray);
return 0;
}