我在.NET应用程序中使用本机C代码,C ++ / CLI和C#时遇到堆损坏问题。这是我第一次真正进入这里的杂草。
应用程序的结构是用于GUI和整体控制流的C#,用于包装本机C函数的C ++ / CLI,以及用于处理数据的本机C函数。这些本机C函数通常接受作为输入的本机指针(例如:int *)和维度。 C ++ / CLI将这些低级函数包装到更高级别的组合处理函数中,C#调用高级函数。
有时,我确实需要在C#级别分配非托管内存,然后将同一块内存传递给几个不同的C ++ / CLI函数。
为了通过我的C#和C ++ / CLI层自由传递这些数组,我创建了一个围绕托管指针的瘦包装类。这个名为ContiguousArray的包装器在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) {}
};
我在几个方面使用这个包装类。
用例1(C ++ / CLI):
{
// Create an array for the low level code
ContiguousArray<float> unmanagedArray(1024);
// Call some native functions
someNativeCFunction(unmanagedArray.p, unmanagedArray.GetLength());
float* unmanagedArrayPointer = unmanagedArray.p;
anotherNativeCFunction(unmanagedArrayPointer, unmanagedArray.GetLength());
int returnCode = theLastNativeCFunction(unmanagedArray.p, unmanagedArray.GetLength());
return returnCode;
} // unmanagedArray goes out of scope, freeing the memory
用例2(C ++ / CLI):
{
// 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());
return returnCode;
} // unmanagedArray goes out of scope, the garbage collector will take care of it at some point
用例3(C#):
{
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
我认为通过使用这个包装类来处理非托管内存非常小心,但我在应用程序中一直看到堆损坏和访问冲突问题。我永远不会保留指向ContiguousArray对象有效范围之外的非托管内存的本机指针。
这三个用例中是否存在任何错误,理论上可能导致堆损坏?我在ContiguousArray实现中遗漏了一些关键字吗?我担心垃圾收集器可能会变得有点过分热心,并在我完成它之前清理我的托管对象。
用例1:我保证在结束括号之前不会调用终结器吗?是否有可能.NET已经确定该对象不再使用,并且在我仍然有一个指向其内部存储器的指针时它会被清理干净? GC :: KeepAlive是否需要用于堆栈对象?
用例2:我是否需要GC :: KeepAlive以保证在第三次函数调用之前不处理对象?如果我改为写,我还需要它吗? nativeFunction(unmanagedArray-&gt; p,unmanagedArray-&gt; GetLength());
用例3:我在这里看不出任何错误,但也许我错过了什么?
答案 0 :(得分:1)
感谢写出我的问题(最好的老师)以及tsandy和Hans的建议的魔力,我已经研究了垃圾收集器在处理非托管资源时的行为。这是我发现的:
我使用的设计模式存在缺陷。如果垃圾收集器决定不再使用托管对象句柄(^),即使句柄仍在范围内,也可以进行垃圾回收。正确(但较慢)的设计模式不允许访问非托管资源,除非通过其托管包装类的方法。如果允许指向非托管资源的指针或引用从包装器中泄漏出来,那么获取它们的代码需要非常小心,以确保不会收集/最终确定拥有它们的包装器。因此,设计为ContiguousArray的包装类并不是一个好主意。
那说,这个模式很快!所以这里是如何逐案挽救事物的。
用例1实际上没问题!在包装器超出范围时,在C ++ / CLI中使用堆栈语义可确保确定性完成。在包装器超出范围后保持指针仍然是一个错误,但在所有这些都是安全的。我更改了大量的C ++ / CLI代码以强烈支持堆栈语义,包括尽可能使用句柄引用(%)作为仅由我的C ++ / CLI代码调用的函数的参数。
用例2很危险,需要修复。有时您无法避免使用句柄,因此您需要使用GC::KeepAlive(unmanagedArray)强制垃圾收集器保持对象直到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;
}
用例3在技术上不安全。在调用finalCLIFunction之后,.NET垃圾收集器可能会立即决定它不再需要unmanagedArray(取决于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;
}
那就是它。尽可能使用堆栈语义。如果不能,请在需要对象存活的最后一行之后使用GC :: KeepAlive()。请记住也要调用C ++ / CLI函数的参数。将所有这些垃圾收集纠缠在您的C#代码之外,不应该知道这些实现细节。
我遵循了所有这些约定,我的堆损坏和访问冲突都消失了。希望这对某人有所帮助。
答案 1 :(得分:0)
首先,我假设ContiguousArray<T>
中的成员被称为size
而不是_size
只是一个错字。
就访问违规而言,我在案例3中没有看到任何错误。在案例2中,在使用其指针完成nativeFunction
之前,数组肯定可以进行垃圾回收。我不确定案例1是否有同样的问题。如果您使用GC::KeepAlive
,是否会修复访问冲突?
堆损坏可能意味着在!ContiguousArray<T>()
中释放内存时已经释放了内存。本机方法是否可以释放数组或执行ContiguousArrays
交换拥有的数组?
P.S。,最好检查calloc
是否还没有nullptr
。