在 C#/C++ 之间编组复杂结构

Posted

技术标签:

【中文标题】在 C#/C++ 之间编组复杂结构【英文标题】:Marshalling Complex structures between C#/C++ 【发布时间】:2014-02-17 07:14:50 【问题描述】:

我正在尝试从 C++ 填充结构数组并将结果传递回 C#。

我想也许创建一个具有结构数组的结构可能是前进的方向,因为我遇到的大多数示例都使用结构(但传递基本类型)。我已经尝试了以下方法,但到目前为止没有运气。

在以下位置找到了一个示例:http://limbioliong.wordpress.com/2011/08/20/passing-a-pointer-to-a-structure-from-c-to-c-part-2/?relatedposts_exclude=542

我在 C# 中有以下内容

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
namespace CSharpDLLCall

  class Program
  

[StructLayout(LayoutKind.Sequential,Pack=8)]
public struct Struct1

    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
    public double[] d1;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
    public double[] d2;


[StructLayout(LayoutKind.Sequential)]
public struct TestStructOuter

     public int length;
     public IntPtr embedded;


 static void Main(string[] args)
 
    Program program = new Program();
    program.demoArrayOfStructs();
 

public void demoArrayOfStructs() 

    TestStructOuter outer = new TestStructOuter();
    testStructOuter.embedded = new Struct1[10];
    outer.length = 10;
    outer.embedded = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Struct1)) * 10);
    Marshal.StructureToPtr(outer, outer.embedded, false);
    testAPI2(ref outer);
    outer = (TestStructOuter)(Marshal.PtrToStructure(outer.embedded, typeof(TestStructOuter)));
    Marshal.FreeHGlobal(outer.embedded);
 

[DllImport(@"C:\CPP_Projects\DLL\DLLSample\Release\DLLSample.dll")]
static extern void testAPI2(IntPtr pTestStructOuter);


在 C++ 中的标题中我有

#ifdef DLLSAMPLE_EXPORTS
#define DLLSAMPLE_API __declspec(dllexport)
#else
#define DLLSAMPLE_API __declspec(dllimport)
#endif

#include <iostream>
using namespace std;

#pragma pack(1)
struct struct1

    double d1[];
    double d2[];
;

struct TestStructOuter

    struct1* embedded;
;

extern "C"
   
    DLLSAMPLE_API void __stdcall testAPI2(TestStructOuter* pTestStructOuter);

在我的 cpp 中:

#include "stdafx.h"
#include "DLLSample.h"

__declspec(dllexport) void __stdcall testAPI2(TestStructOuter* pTestStructOuter)

    // not sure that this is necessary
    // for (int i = 0; i < 10 ; ++i)
    //     
    //     pTestStructOuter->embedded = new struct1;        
    // 

    for (int i = 0; i < 10 ; ++i)
    
            struct1 s1;

            for (int idx = 0; idx < 10; ++idx)
            
                    s1.d1[i] = i+0.5;
                    s1.d2[i] = i+0.5;
            

            pTestStructOuter->embedded[0] = s1;
    

这似乎不起作用我得到的错误: 参数不正确。(HRESULT 异常:0x80070057 (E_INVALIDARG))

这可能意味着它无法识别结构数组。任何想法我怎么能做到这一点?谢谢。

【问题讨论】:

您的 C++ 代码不正确,您声明了一个不占用 任何 空间的结构。数组是空的。然后,您的 testAPI2() 函数通过访问数组来调用 UB,因为它们的长度为 10 个元素。 C++ 是一种装载武器,你将它对准你的脚并扣动扳机。当你跳来跳去时接下来会发生什么是完全无法猜测的。 【参考方案1】:

好的,我有一个工作样本。我将此作为另一个答案发布,因为这是一种非常不同的方法。

所以,在 C++ 端,我有这个头文件:

struct Struct1

  int d1[10];
  int d2[10];
;

extern "C" __declspec(dllexport) void __stdcall 
  TestApi2(int* pLength, Struct1 **pStructures);

还有如下代码:

__declspec(dllexport) void __stdcall 
  TestApi2(int* pLength, Struct1 **pStructures)

  int len = 10;

  *pLength = len;
  *pStructures = (Struct1*)LocalAlloc(0, len * sizeof(Struct1));

  Struct1 *pCur = *pStructures;

  for (int i = 0; i < len; i++)
  
    for (int idx = 0; idx < 10; ++idx)
    
      pCur->d1[idx] = i + idx;
      pCur->d2[idx] = i + idx;
    

    pCur ++;
  

现在,重要的一点是LocalAlloc。这实际上是我遇到所有问题的地方,因为我分配了错误的内存。 LocalAlloc 是 .NET 在执行 Marshal.AllocHGlobal 时调用的方法,这非常方便,因为这意味着我们可以使用调用者中的内存并根据需要进行处理。

现在,此方法允许您返回任意长度的结构数组。相同的方法可用于例如。返回结构数组的结构,你只是更深入。关键是 LocalAlloc - 这是您可以使用 Marshal 类轻松访问的内存,它是不会被丢弃的内存。

您还必须返回数组长度的原因是因为没有办法知道您“返回”了多少数据。这是非托管代码中的一个常见“问题”,如果你曾经做过任何 P/Invoking,你就会知道这一切。

现在,C# 方面。我已经努力以一种很好的方式做到这一点,但问题是结构数组根本不是 blittable,这意味着你不能简单地使用MarshalAs(UnmanagedType.LPArray, ...)。所以,我们必须走IntPtr的方式。

定义如下所示:

[StructLayout(LayoutKind.Sequential)]
public class Struct1

  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
  public int[] d1;

  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
  public int[] d2;
        

[DllImport(@"Win32Project.dll", CallingConvention = CallingConvention.StdCall)]
static extern void TestApi2(out int length, out IntPtr structs);

基本上,我们得到一个指向“数组”长度的指针,以及指向数组第一个元素的指针。这就是我们读取数据所需的全部内容。

代码如下:

int length;
IntPtr pStructs;

TestApi2(out length, out pStructs);

// Prepare the C#-side array to copy the data to
Struct1[] structs = new Struct1[length];

IntPtr current = pStructs;
for (int i = 0; i < length; i++)

  // Create a new struct and copy the unmanaged one to it
  structs[i] = new Struct1();
  Marshal.PtrToStructure(current, structs[i]);

  // Clean-up
  Marshal.DestroyStructure(current, typeof(Struct1));

  // And move to the next structure in the array
  current = (IntPtr)((long)current + Marshal.SizeOf(structs[i]));


// And finally, dispose of the whole block of unmanaged memory.
Marshal.FreeHGlobal(pStructs);

如果您想真正返回结构数组的结构,唯一需要改变的是方法参数移动到“包装”结构中。方便的是.NET可以自动处理包装器的编组,不太方便的是它不能处理内部数组,所以你再次必须使用length + IntPtr来手动管理。

【讨论】:

我会试试这个,然后回来找你。这也是很多工作。我很乐意为您安排转账,支付 - 朋友,西联汇款,您选择的任何东西(这又是很多工作!) @user3316669 别担心,这很有趣 :) 在这一行:Marshal.PtrToStructure(current,structs[i])。我得到一个错误'结构不能是一个值类。参数名称:结构'。所以我改变了那行和 structs[i] = new Struct1();到 structs[idx] = (Struct1)Marshal.PtrToStructure(current, typeof(Struct1));这似乎成功了!非常感谢,非常感谢您的帮助! 我还发现了 2 篇非常有趣的文章。您可能会喜欢,实现 ICustomMarsharler 的使用。 link1link2 @user3316669 这很有趣,对我来说效果很好。也许您使用的是不同的 .NET 版本?【参考方案2】:

首先,不要使用未知长度的数组。您正在扼杀托管代码和非托管代码之间可能使用的数组 - 您无法确定所需的大小(Marshal.SizeOf 没用),您必须手动传递数组长度等。如果它不是固定长度数组,请使用IntPtr:

[StructLayout(LayoutKind.Sequential,Pack=8)]
public struct Struct1

    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
    public double[] d1;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
    public double[] d2;


[StructLayout(LayoutKind.Sequential)]
public struct TestStructOuter

     public int length;
     public IntPtr embedded;

(如果您的embedded 数组是固定长度的,请随意忽略这一点,但确实包含ByValArray, SizeConst 位。您还必须将ArraySubType=System.Runtime.InteropServices.UnmanagedType.Struct 添加到属性中)。

您可能已经注意到,TestStructOuter 在这种情况下几乎没有用,它只会增加复杂性(请注意,与本地语言相比,编组是最昂贵的操作之一,所以如果您打电话通常情况下,保持简单可能是个好主意)。

现在,您只是为外部结构分配内存。即使在您的代码中,Marshal.SizeOf 也不知道结构应该有多大;对于IntPtr,情况更是如此(或者,更准确地说,您只请求一个 IntPtr,即 4-8 个字节左右 + 长度)。大多数情况下,您希望直接传递 IntPtr,而不是进行这种包装(在 C++/CLI 中使用相同的东西完全是另一回事,尽管对于您的情况来说这是不必要的)。

您的方法的签名将是这样的:

[DllImport(@"C:\CPP_Projects\DLL\DLLSample\Release\DLLSample.dll", 
 CallingConvention=System.Runtime.InteropServices.CallingConvention.StdCall)]
public static extern  void testAPI2(ref TestStructOuter pTestStructOuter) ;

现在,如果您使用IntPtr 方法,您希望继续手动分配内存,只需正确执行即可,例如:

TestStructOuter outer = new TestStructOuter();
testStructOuter.length = 1;
testStructOuter.embedded = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Struct1)));

Marshal.StructureToPtr(embedded, testStructOuter.embedded, false);

最后,调用方法:

// The marshalling is done automatically
testAPI2(ref outer);

不要忘记释放内存,最好是在 tryfinally 子句中,围绕自内存分配以来的所有内容:

Marshal.FreeHGlobal(outer.embedded);

现在,这过于复杂,而且并非完全最优。省略TestStructOuter 是一种可能性,然后您可以简单地将长度和指针直接传递给嵌入式数组,避免不必要的编组。另一种选择是在TestStructOuter 中使用固定大小的数组(如果它需要是一个数组:)),这将解决您的许多问题并消除对手动编组的任何需要。换句话说,如果TestStructOuter 被定义为我之前提到的:

[StructLayout(LayoutKind.Sequential)]
public struct TestStructOuter

     [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10, 
      ArraySubType=UnmanagedType.Struct)]
     public Struct1[] embedded;

突然间,你的整个通话和一切都变得如此简单

testAPI2(ref outer);

整个编组是自动完成的,无需手动分配或转换。

希望这会有所帮助:)

【讨论】:

嗨 Luaan,我觉得你的解决方案在隧道尽头有光明 :-)。非常渴望得到它,但是我已经尝试过TestStructOuter outer = new TestStructOuter(); outer.length = 10; outer.embedded = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Struct1)) * 10); Marshal.StructureToPtr(outer.embedded, outer.embedded, false); testAPI2(ref outer); outer = (TestStructOuter)(Marshal.PtrToStructure(outer.embedded, typeof(TestStructOuter))); Marshal.FreeHGlobal(outer.embedded); 但是程序失败了。 @user3316669 是的,您的Marshal.StructureToPtr 呼叫是错误的。您必须将准备好的结构作为第一个参数传递,而不是指向非托管结构的指针。此外,无论如何,您只发布一个实例,而不是您想要传递的 10 个。一个接一个地传递多个结构有点棘手,基本上,您需要在一个循环中执行StructureToPtr,同时将IntPtr增加结构大小。您能否在某处发布您的测试 API dll,以便我可以尝试使其工作?那会让一切都快一点:) 我真的很想在某处发布完整的解决方案(它非常小)。这是我第一次使用***,我不确定在哪里。并且 cmets 仅限于 ~500 个字符左右。 @user3316669 如果您不担心与未知公司共享数据,请尝试speedyshare.com 之类的方法(注意:我不以任何方式认可这个特定的文件共享网站,事实上它是第一个是我从 Google 上下来的,但我相信它对你的样本来说已经足够好了,除非你受到一些 NDA 或类似的约束)。 @user3316669 别担心,当我们弄清楚实际问题是什么时,我们可以根据需要更新问题和答案:))【参考方案3】:

提示:Leadn C++/CLI。在我的生活中,我不得不处理两次复杂的互操作——一次是使用 ISDN(AVM 开发套件让它变得更容易——遗憾的是 C++,我需要 C#),现在使用 Nanex(伟大的实时复杂和完整的市场数据源,sadl 复杂的 C++ , 我需要 C#)

在这两种情况下,我都在 C++/CLI 中制作了自己的包装器,与 C++ 对话并在 C# 中公开真实的对象模型。让我可以把事情做得更好,并处理很多我们友好的 Marshaller 无法有效处理的边缘情况。

【讨论】:

以上是关于在 C#/C++ 之间编组复杂结构的主要内容,如果未能解决你的问题,请参考以下文章

创建要在 C# 中编组的 C++ Dll 的最佳实践 [关闭]

使用结构的c中的复杂产品?

编组包含 c 字符串的结构

如何编组从 C 头文件中的宏定义的结构?

如何将 C 指针编组到 C# 结构数组

P/Invoke 编组和解组 C# 和非托管 DLL 之间的二维数组、结构和指针