在C ++ / CLI中包装非托管指针时堆损坏

时间:2016-07-07 21:20:37

标签: c# .net garbage-collection c++-cli mixed-mode

我在.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:我在这里看不出任何错误,但也许我错过了什么?

2 个答案:

答案 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