c# - 如何将包含可变大小数组的结构编码到 C#?

标签 c# .net c++ marshalling

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

ABS_DATA 结构用于将任意长度的数据 block 与长度信息相关联。 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;
    }

最佳答案

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

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

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

首先,我认为您的 native 原型(prototype)可能如下所示(我在这里使用 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);

不幸的是,在运行时,除了 SafeArray 之外,您显然不能将数据结构中的数组编码为任何内容。或 ByValArray . SafeArrays 被计算在内,但它们看起来与您在此处寻找的(非常常见的)格式完全不同。所以这行不通。当然,ByValArray 要求在编译时知道长度,所以这也不起作用(就像你遇到的那样)。奇怪的是,您可以在数组参数上使用自定义编码(marshal)处理,这很烦人,因为您必须将 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基本上只是一个重命名的字节 - 它甚至没有“包装”字节;就 native 代码而言,它更像是 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);

好的,现在您可以了解如何声明数组元素类型(如果需要),以及如何将数组传递给非托管函数。但是,我们仍然需要那个自定义编码(marshal)器。您应该阅读“Implementing the ICustomMarshaler Interface”,但我将在这里介绍这一点,并带有内联注释。请注意,我使用了一些需要 .NET 4.5.1 或更高版本的速记约定(如 Marshal.SizeOf<T>())。

// 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>();
    }
}

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

关于c# - 如何将包含可变大小数组的结构编码到 C#?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/5902103/

相关文章:

c# - 如何更改 Windows 窗体中 DataGridViewColumn 的标题?

c# - 获取 Picturebox 中缩放图像的确切大小

.net - 从自定义镜像创建 Azure VM

c# - 推荐用于在多台计算机之间共享项目的 C# .Net 项目配置

c++ - 通过参数返回值是一个好的设计吗?

c# - C# 中的非线性回归

c# - 当没有错误时,CSharpCodeProvider 不会返回编译器警告

c# - Monitor.PulseAll 的正确用法?

c++ - 自动提升算术运算

C++:POD 优缺点