将引用类型对象数组从 C# 编组到 C++

Kha*_*led -1 c# c++ arrays pinvoke marshalling

我能够通过装饰它成功地将引用类型对象从 C# 传递到 C++StructLayout(LayoutKind.Sequential)

C# 中的类:

StructLayout(LayoutKind.Sequential)
public class Point
{
        [DataMember]
        public double X { get; set; }
        [DataMember]
        public double Y { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

C++ 中的等价物:

struct Point
{
public:
    double x;
    double y;
};
Run Code Online (Sandbox Code Playgroud)

C++中的函数体:

extern "C" __declspec(dllexport) void Translate(Point* pt, double dx, double dy)
{
    pt->x += dx;
    pt->y += dy;
}
Run Code Online (Sandbox Code Playgroud)

以及 C# 中的等效项:

[DllImport("myDll.dll", CallingConvention = CallingConvention.Cdecl)]
public extern static void Translate(Point pt, double dx, double dy); 
Run Code Online (Sandbox Code Playgroud)

到目前为止我没有遇到任何问题,当我在接下来的两个函数之间传递这些点的数组时,问题就会发生:

extern "C" __declspec(dllexport) double GetArea(Point** vertices,int count, bool isClosed)
    {
        double area = 0;
        if (!isClosed || count < 3) return 0;
        int j;
        for (int i = 0; i < count; i++)
        {
            // j = i+1 except when i = count then j = 0
            j = (i + 1) % count;

            area += vertices[i]->x * vertices[j]->y;
            area -= vertices[i]->y * vertices[j]->x;
        }
        area /= 2;
        return fabs(area);
    }
Run Code Online (Sandbox Code Playgroud)

和 C# 版本:

[DllImport("myDll.dll", CallingConvention = CallingConvention.Cdecl)]
public extern static double GetArea(Point[] vertices, int count, bool isClosed);
Run Code Online (Sandbox Code Playgroud)

有没有一种特定的方法来编组这个数组?但保持 Point 类不变?

调用方法是这样的:

public static double GetArea()
        {
            Point[] vertices = { new Point(100, 100), new Point(0, 0), new Point(0, 100) };
            bool isClosed = true;
            return NativeMethods.GetArea(vertices, vertices.Length, isClosed); //this is the imported C++ method
}
Run Code Online (Sandbox Code Playgroud)

Lua*_*aan 6

您根本不应该真正互操作类(除了那些明确支持的类,例如string数组......)。.NET 的本机互操作是为 C 风格的函数和结构设计的,而不是为 C++ 设计的。因此,要么将本机代码更改为 C 风格,要么使用 C++/CLI 之类的代码为 C++ 对象和函数提供托管接口。

处理互操作的最简单方法是使用编组器,并将其更改Point为值类型,而不是引用类型:

struct Point
{
  public double x;
  public double y;
}

[DllImport("myDll.dll", CallingConvention = CallingConvention.Cdecl)]
public extern unsafe static double GetArea(Point[] vertices, int count, bool isClosed);
Run Code Online (Sandbox Code Playgroud)

这当然需要Point是一个 blittable 类型 - 只需将其struct改为aclass就足够了。当vertices是指向值数组的指针时,这就是您所要做的。同样,您需要将其设为 C++ 端的值,而不是指针(并且函数的签名为GetArea(Point *vertices, ...),而不是使用指向指针的指针)。

手动编组数组有两个主要选项 - 不安全代码和IntPtr. 如果您需要整理一些更复杂的东西,这可能很有用

不安全签名很简单:

[DllImport("myDll.dll", CallingConvention = CallingConvention.Cdecl)]
public extern unsafe static double GetArea(Point** vertices, int count, bool isClosed);
Run Code Online (Sandbox Code Playgroud)

创建和传递数组的方法与 C 中的相同 - 我无法真正提供任何具体信息,因为它完全取决于谁拥有什么内存以及该内存的结构。您可以轻松使用现有的 .NET 数组,只需固定它们:

public static unsafe double CallGetArea(Point[] vertices, bool isClosed)
{
  if (vertices.Length == 0) throw new ArgumentException("Vertices empty.", "vertices");

  fixed (Point* pVertices = &vertices[0])
  {
    return GetArea(pVertices, vertices.Length);
  }
}
Run Code Online (Sandbox Code Playgroud)

这假设您只想传递一个Point值数组(签名将具有Point*)。您不能真正将指针传递给这样的数组 -pVertices是只读的。再说一次,这仅在Point可 blittable 时才有效。

签名IntPtr会丢弃类型信息:

[DllImport("myDll.dll", CallingConvention = CallingConvention.Cdecl)]
public extern unsafe static double GetArea(IntPtr vertices, int count, bool isClosed);
Run Code Online (Sandbox Code Playgroud)

同样,该调用与 C 类似,您还必须执行大量烦人的编组器调用来进行读取和写入,而不是使用类似 C 的语言来执行此操作。

如果您确实想坚持创建一个指向 的指针数组的指针Point,那么事情会变得有点复杂。编组器不支持指针数组,因此您必须手动进行编组。当然,这涉及大量不必要的复制和分配 - 因此,如果您出于性能原因尝试这样做,请不要这样做。基本思想是这样的:

unsafe double CallGetAreaPointers(Point[] vertices, bool isClosed)
{
  var ipArray = Marshal.AllocHGlobal(vertices.Length * sizeof(Point));
  var ipItems = new Stack<IntPtr>();

  try
  {
    Point** pArray = (Point**)ipArray.ToPointer();

    for (var i = 0; i < vertices.Length; i++)
    {
      var ipItem = Marshal.AllocHGlobal(sizeof(Point));
      ipItems.Push(ipItem);

      Marshal.StructureToPtr(vertices[i], ipItem, false);

      pArray[i] = (Point*)ipItem.ToPointer();
    }

    GetArea(pArray, vertices.Length, isClosed);
  }
  finally
  {
    IntPtr ipItem;
    while ((ipItem = ipItems.Pop()) != IntPtr.Zero) Marshal.FreeHGlobal(ipItem);

    Marshal.FreeHGlobal(ipArray);
  }
}
Run Code Online (Sandbox Code Playgroud)

免责声明:这只是一个后台代码;如果您决定这样做,请确保您做得正确,这只是一个一般想法)

请注意,为了简单起见,我仍然使用Pointas struct- 它只允许您在 C++ 端使用指针。如果您想完全将其设为 C# 中的引用类型,最简单的方法是创建一个PointStruct类型(如果您愿意,可以是私有类型,也没关系)并将类字段复制到结构体中。当然,在任何一种情况下,单独分配每个Point实例都没有什么意义——一个简单的值数组将以完全相同的方式工作,同时作为连续的内存位,使用和清理更简单、更便宜。