【问题标题】:How do I marshal a struct that contains a variable-sized array to C#?如何将包含可变大小数组的结构编组到 C#?
【发布时间】:2011-08-19 14:45:33
【问题描述】:

如何编组这种 C++ 类型?

ABS_DATA 结构用于将任意长度的数据块与长度信息相关联。 Data数组的声明长度为1,但实际长度由Length成员给出。

typedef struct abs_data {
  ABS_DWORD Length;
  ABS_BYTE Data[ABS_VARLEN];
} ABS_DATA;

我尝试了以下代码,但它不起作用。数据变量始终为空,我确定其中有数据。

[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Ansi)]
    public struct abs_data
    {
        /// ABS_DWORD->unsigned int
        public uint Length;

        /// ABS_BYTE[1]
       [System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.ByValTStr, SizeConst = 1)]
        public string Data;
    }

【问题讨论】:

  • 所以数据是一个只有1个字节的字节数组?
  • 另外,你有我可以看到的定义 ABS_BYTE 和 ABS_VARLEN 的 typedef 吗?
  • 不,通常会有 1000 个字节。
  • 那你为什么只有 1 个字节长?
  • typedef unsigned char ABS_BYTE 无符号整数类型(1字节)

标签: c# .net c++ marshalling


【解决方案1】:

老问题,但我最近不得不自己做这个,所有现有的答案都很差,所以......

在结构中封送可变长度数组的最佳解决方案是使用custom marshaler。这使您可以控制运行时用于在托管数据和非托管数据之间转换的代码。不幸的是,自定义编组的文档很少,并且有一些奇怪的限制。我将快速介绍这些内容,然后再讨论解决方案。

令人讨厌的是,您不能对结构或类的数组元素使用自定义封送处理。这种限制没有记录或合乎逻辑的原因,编译器不会抱怨,但您会在运行时遇到异常。另外,还有一个自定义编组器必须实现的函数int GetNativeDataSize(),这显然是不可能准确实现的(它不会向您传递对象的实例来询问其大小,因此您只能关闭类型,即当然是可变大小!)幸运的是,这个函数无关紧要。我从未见过它被调用过,而且即使它返回一个虚假值(一个 MSDN 示例让它返回 -1),自定义封送拆收器也能正常工作。

首先,我认为您的本机原型可能如下所示(我在这里使用 P/Invoke,但它也适用于 COM):

// Unmanaged C/C++ code prototype (guess)
//void DoThing (ABS_DATA *pData);

// Guess at your managed call with the "marshal one-byte ByValArray" version
//[DllImport("libname.dll")] public extern void DoThing (ref abs_data pData);

这是您可能如何使用自定义编组器的简单版本(这确实应该有效)。稍后我会谈到 marshaler 本身......

[StructLayout(LayoutKind.Sequential)]
public struct abs_data
{
    // Don't need the length as a separate filed; managed arrays know it.
    [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef=typeof(ArrayMarshaler<byte>))]
    public byte[] Data;
}

// Now you can just pass the struct but it takes arbitrary sizes!
[DllImport("libname.dll")] public extern void DoThing (ref abs_data pData);

不幸的是,在运行时,除了SafeArrayByValArray 之外,您显然不能将数据结构中的数组编组为任何内容。 SafeArrays 被计算在内,但它们看起来与您在此处寻找的(非常常见的)格式完全不同。所以这行不通。当然,ByValArray 要求在编译时知道长度,所以这也不起作用(就像你遇到的那样)。奇怪的是,您可以在数组参数上使用自定义封送处理,这很烦人,因为您必须将MarshalAsAttribute 放在使用此类型的每个参数上,而不仅仅是将它放在一个字段上,并在您使用包含该字段的类型的任何地方应用它,但 c'est la vie。它看起来像这样:

[StructLayout(LayoutKind.Sequential)]
public struct abs_data
{
    // Don't need the length as a separate filed; managed arrays know it.
    // This isn't an array anymore; we pass an array of this instead.
    public byte Data;
}

// Now you pass an arbitrary-sized array of the struct
[DllImport("libname.dll")] public extern void DoThing (
    // Have to put this huge stupid attribute on every parameter of this type
    [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef=typeof(ArrayMarshaler<abs_data>))]
    // Don't need to use "ref" anymore; arrays are ref types and pass as pointer-to
    abs_data[] pData);

在那个例子中,我保留了abs_data 类型,以防你想用它做一些特殊的事情(构造函数、静态函数、属性、继承等等)。如果您的数组元素由复杂类型组成,您将修改结构以表示该复杂类型。然而,在这种情况下,abs_data 基本上只是一个重命名的字节——它甚至没有“包装”字节;就本机代码而言,它更像是 typedef - 所以你可以只传递一个字节数组并完全跳过结构:

// Actually, you can just pass an arbitrary-length byte array!
[DllImport("libname.dll")] public extern void DoThing (
    // Have to put this huge stupid attribute on every parameter of this type
    [MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef=typeof(ArrayMarshaler<byte>))]
    byte[] pData);

好的,现在您可以了解如何声明数组元素类型(如果需要),以及如何将数组传递给非托管函数。但是,我们仍然需要那个自定义封送器。您应该阅读“Implementing the ICustomMarshaler Interface”,但我将在此处使用内联 cmets 进行介绍。请注意,我使用了一些需要 .NET 4.5.1 或更高版本的速记约定(如 Marshal.SizeOf&lt;T&gt;())。

// The class that does the marshaling. Making it generic is not required, but
// will make it easier to use the same custom marshaler for multiple array types.
public class ArrayMarshaler<T> : ICustomMarshaler
{
    // All custom marshalers require a static factory method with this signature.
    public static ICustomMarshaler GetInstance (String cookie)
    {
        return new ArrayMarshaler<T>();
    }

    // This is the function that builds the managed type - in this case, the managed
    // array - from a pointer. You can just return null here if only sending the 
    // array as an in-parameter.
    public Object MarshalNativeToManaged (IntPtr pNativeData)
    {
        // First, sanity check...
        if (IntPtr.Zero == pNativeData) return null;
        // Start by reading the size of the array ("Length" from your ABS_DATA struct)
        int length = Marshal.ReadInt32(pNativeData);
        // Create the managed array that will be returned
        T[] array = new T[length];
        // For efficiency, only compute the element size once
        int elSiz = Marshal.SizeOf<T>();
        // Populate the array
        for (int i = 0; i < length; i++)
        {
            array[i] = Marshal.PtrToStructure<T>(pNativeData + sizeof(int) + (elSiz * i));
        }
        // Alternate method, for arrays of primitive types only:
        // Marshal.Copy(pNativeData + sizeof(int), array, 0, length);
        return array;
    }

    // This is the function that marshals your managed array to unmanaged memory.
    // If you only ever marshal the array out, not in, you can return IntPtr.Zero
    public IntPtr MarshalManagedToNative (Object ManagedObject)
    {
        if (null == ManagedObject) return IntPtr.Zero;
        T[] array = (T[])ManagedObj;
        int elSiz = Marshal.SizeOf<T>();
        // Get the total size of unmanaged memory that is needed (length + elements)
        int size = sizeof(int) + (elSiz * array.Length);
        // Allocate unmanaged space. For COM, use Marshal.AllocCoTaskMem instead.
        IntPtr ptr = Marshal.AllocHGlobal(size);
        // Write the "Length" field first
        Marshal.WriteInt32(ptr, array.Length);
        // Write the array data
        for (int i = 0; i < array.Length; i++)
        {   // Newly-allocated space has no existing object, so the last param is false
            Marshal.StructureToPtr<T>(array[i], ptr + sizeof(int) + (elSiz * i), false);
        }
        // If you're only using arrays of primitive types, you could use this instead:
        //Marshal.Copy(array, 0, ptr + sizeof(int), array.Length);
        return ptr;
    }

    // This function is called after completing the call that required marshaling to
    // unmanaged memory. You should use it to free any unmanaged memory you allocated.
    // If you never consume unmanaged memory or other resources, do nothing here.
    public void CleanUpNativeData (IntPtr pNativeData)
    {
        // Free the unmanaged memory. Use Marshal.FreeCoTaskMem if using COM.
        Marshal.FreeHGlobal(pNativeData);
    }

    // If, after marshaling from unmanaged to managed, you have anything that needs
    // to be taken care of when you're done with the object, put it here. Garbage 
    // collection will free the managed object, so I've left this function empty.
    public void CleanUpManagedData (Object ManagedObj)
    { }

    // This function is a lie. It looks like it should be impossible to get the right 
    // value - the whole problem is that the size of each array is variable! 
    // - but in practice the runtime doesn't rely on this and may not even call it.
    // The MSDN example returns -1; I'll try to be a little more realistic.
    public int GetNativeDataSize ()
    {
        return sizeof(int) + Marshal.SizeOf<T>();
    }
}

哇,好长啊!好吧,你有它。我希望人们看到这一点,因为那里有很多不好的答案和误解......

【讨论】:

  • 感谢精彩的总结!这有助于我理解 marshal 的内部结构......虽然我的情况稍微糟糕一点:“struct Foo {int a; int b; /* unrelated fields */, int size, int arr[0]; }”。由于附加字段 a 和 b,我不能像您所做的那样将其作为参数传递。有什么建议吗?
  • @wsxiaoys:我不明白问题出在哪里......您为Foo 编写了一个自定义封送器,它为ab 和您的“无关”分配了额外的字节字段”并填充它们,然后在适当的偏移量处写入数组大小和值。解组是一样的;从本机指针中读取 ab 的方式与我们读取上面大小的方式相同,但在适当的偏移量处,然后获取大小并分配托管数组,然后填充它。 Foo 的托管表示可能不需要 size 字段,但您可以根据需要包含它。
  • 谢谢,非常有用和有用,不幸的是它看起来在包含的“核心”分支 5.0 中不起作用 - 请参阅 github.com/dotnet/runtime/issues/8271 。无论如何 +1 以获得很好的答案!
  • @MosèBottacini 您不能在结构字段上使用自定义封送处理程序-正如我所说,编译器不会抱怨,但会产生运行时错误-这就是您链接的问题所尝试的。正如我所展示的,您仍然可以编组函数参数,或者至少您应该能够,如果不能,请在 GitHub 上打开一个新错误(您链接的错误是不同的)。
  • @CBHacking 也许我没有很好地表达自己,但我感谢您的出色回答,是的,我发布的链接 * 正是 * 我的情况;无论如何,我已经重新设计了代码,使其更加面向“.Net 5.0”,这在同样的方式上是坏的,也是好的,但它有它的“优点”,比如更多的控制,代价是编写更多的代码:现在我已经序列化和反序列化以预期方式填充和提取到/从 Span 的方法。
【解决方案2】:

不可能编组包含可变长度数组的结构(但可以编组可变长度数组作为函数参数)。您必须手动读取数据:

IntPtr nativeData = ... ;
var length = Marshal.ReadUInt32 (nativeData) ;
var bytes  = new byte[length] ;

Marshal.Copy (new IntPtr ((long)nativeData + 4), bytes, 0, length) ;

【讨论】:

  • Marshal.ReadUInt32() 在 .NET 4.0 框架上不存在,遗憾的是。谁能解释一下为什么?
  • 可能是因为无符号类型不符合 CLS。使用Marshal.ReadIntNn 函数并手动转换为无符号。
【解决方案3】:

如果要保存的数据不是字符串,则不必将其存储在字符串中。除非原始数据类型是char*,否则我通常不会编组为字符串。否则byte[] 应该可以。

试试:

[MarshalAs(UnmanagedType.ByValArray, SizeConst=[whatever your size is]]
byte[] Data;

如果您稍后需要将其转换为字符串,请使用:

System.Text.Encoding.UTF8.GetString(your byte array here). 

显然,您需要根据需要更改编码,尽管UTF-8 通常就足够了。

我现在看到了问题,您必须编组一个 VARIABLE 长度数组。 MarshalAs 不允许这样做,并且必须通过引用发送数组。

如果数组长度是可变的,你的byte[] 需要是一个 IntPtr,所以你会使用,

IntPtr Data;

代替

[MarshalAs(UnmanagedType.ByValArray, SizeConst=[whatever your size is]]
byte[] Data;

然后您可以使用 Marshal 类访问基础数据。

类似:

uint length = yourABSObject.Length;
byte[] buffer = new byte[length];

Marshal.Copy(buffer, 0, yourABSObject.Data, length);

您可能需要在完成后清理内存以避免泄漏,但我怀疑当您的 ABSObject 超出范围时 GC 会清理它。无论如何,这是清理代码:

Marshal.FreeHGlobal(yourABSObject.Data);

【讨论】:

  • 就像我在上面所做的那样,将我包含的块替换为您定义数据的部分。将 SizeConst= 更改为 ABS_VARLEN 宏的定义。
  • 问题是长度是基于public uint Length的。但我不能在那里填写。
  • 我认为你的答案会更容易理解,如果你只是删除不相关的部分而不是在它的末尾添加“更新”。无需记录您在问题中的回答的过去迭代;编辑历史已经有了,以防有人真的想知道你的原始答案。
  • @Jonathan:这里不需要Marshal.FreeHGlobal,因为没有非托管数组可以释放。该数组是问题中声明的非托管结构的一部分。如果需要释放整个结构,那是另一回事。无论如何,必须通过分配器释放非托管指针,该分配器首先用于分配它们,并且有很多非托管内存分配器。
  • @Anton 我只是看不出两个 c++ 声明的主要区别 ABS_BYTE* 数据与将 ABS_BYTE 数据 [] 引用为数据相同。因此,我不明白为什么它们的编组方式不同。我并不怀疑他们是,我只是不明白为什么。
【解决方案4】:

您正试图将 byte[ABS_VARLEN] 的内容编组为长度为 1 的 string。您需要弄清楚 ABS_VARLEN 常量是什么并将数组编组为:

[MarshalAs(UnmanagedType.LPArray, SizeConst = 1024)]
public byte[] Data;

(1024有一个占位符;不管ASB_VARLEN的实际值是什么都填。)

【讨论】:

  • ABS_VARLEN 表示它可能总是不同的长度。基于公共 uint 长度。
  • 我认为非 C 程序员似乎对这个结构有很多问题和误解。通常,该结构将 1 的数组定义为“在内存中跟随的是由 'Length' 成员指定的任意数量的字节,您需要遍历和解释这些字节。” 1 并不一定意味着 1,并且没有已知的数字可能是什么。它纯粹是在运行时根据具体情况确定的。
【解决方案5】:

在我看来,固定数组并获取其地址更简单、更有效。

假设您需要将abs_data 传递给myNativeFunction(abs_data*)

public struct abs_data
{
    public uint Length;
    public IntPtr Data;
}

[DllImport("myDll.dll")]
static extern void myNativeFunction(ref abs_data data);

void CallNativeFunc(byte[] data)
{
    GCHandle pin = GCHandle.Alloc(data, GCHandleType.Pinned);

    abs_data tmp;
    tmp.Length = data.Length;
    tmp.Data = pin.AddrOfPinnedObject();

    myNativeFunction(ref tmp);

    pin.Free();
}

【讨论】:

    猜你喜欢
    • 2014-03-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-10-26
    • 1970-01-01
    相关资源
    最近更新 更多