在 C++/CLI 中包装非托管指针时堆损坏
本文关键字:指针 损坏 C++ CLI 包装 | 更新日期: 2023-09-27 17:56:00
我在使用本机 C 代码、C++/CLI 和 C# 的 .NET 应用程序中遇到了堆损坏问题。这是我第一次真正进入这里的杂草。
应用程序的结构是用于 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 对象有效范围之外的非托管内存的本机指针。
从理论上讲,这三个用例中的任何一个都可能导致堆损坏,这有什么问题吗?我是否在我的连续阵列实现中缺少一些关键的东西?我担心垃圾收集器可能变得有点过分热心,并在我真正完成托管对象之前清理它们。
用例 1:我是否保证在右大括号之前不会调用终结器?是否有可能 .NET 已确定该对象不再使用,并且在我仍然有指向其内部存储器的指针时对其进行清理?GC::KeepAlive 是否需要用于堆栈对象?
用例 2:我是否需要在末尾使用 GC::KeepAlive 来保证在第三次函数调用之前不会释放对象?如果我改为写,我还需要它吗: nativeFunction(unmanagedArray->p, unmanagedArray->GetLength());
用例 3:我在这里看不出有什么问题,但也许我错过了什么?
感谢写出我的问题(最好的老师)的魔力以及桑迪和汉斯的建议,我详细研究了垃圾收集器在处理非托管资源时的行为。这是我发现的:
我使用的设计模式是有缺陷的。如果垃圾回收器决定不再使用托管对象句柄 (^),即使该句柄仍在作用域中,也可以对其进行垃圾回收。正确(但较慢)的设计模式不允许访问非托管资源,除非通过其托管包装类的方法。如果允许对非托管资源的指针或引用从包装器中泄漏出来,则获取它们的代码需要非常小心,以确保拥有它们的包装器不会被收集/最终确定。出于这个原因,像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 垃圾回收器可能会立即决定不再需要非托管数组(取决于 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# 代码之外,C# 代码不需要知道这些实现细节。
我遵循了所有这些约定,我的堆损坏和访问冲突消失了。希望这对某人有所帮助。
首先,我假设ContiguousArray<T>
中的成员被称为size
而不是_size
只是一个错字。
在访问违规方面,我认为案例 3 没有任何问题。 在情况 2 中,在使用数组指针完成之前nativeFunction
数组绝对可以进行垃圾回收。 我不确定案例 1 是否有同样的问题。 如果您使用 GC::KeepAlive
,是否可以解决访问冲突?
堆损坏可能意味着内存在 !ContiguousArray<T>()
中释放时已经释放。本机方法是否曾经释放阵列,或者ContiguousArrays
是否交换拥有的数组?
附言,最好检查calloc
是否没有返回nullptr
.