如何将包含可变大小数组的结构封送到C#?

Ezi*_*Ezi 18 .net c# c++ marshalling

我如何编组这个C++类型?

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

typedef struct abs_data {
  ABS_DWORD Length;
  ABS_BYTE Data[ABS_VARLEN];
} ABS_DATA;
Run Code Online (Sandbox Code Playgroud)

我尝试了以下代码,但它不起作用.数据变量总是空的,我确信它有数据.

[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;
    }
Run Code Online (Sandbox Code Playgroud)

CBH*_*ing 28

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

在结构中封送可变长度数组的最佳解决方案是使用自定义封送器.这使您可以控制运行时用于在托管和非托管数据之间进行转换的代码.不幸的是,自定义编组很难记录,并且有一些奇怪的限制.我将快速介绍这些内容,然后重新解决问题.

令人讨厌的是,您不能在结构或类的数组元素上使用自定义封送处理.这种限制没有记录或逻辑上的原因,编译器不会抱怨,但是你会在运行时得到异常.此外,还有一个自定义封送程序必须实现的功能,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);
Run Code Online (Sandbox Code Playgroud)

这是你如何使用自定义封送程序(它确实应该有效)的天真版本.我会稍微谈谈封送者本身......

[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);
Run Code Online (Sandbox Code Playgroud)

不幸的是,在运行时,您显然无法将数据结构内的数组编组为除了SafeArray或之外的任何内容ByValArray.SafeArrays被计算在内,但它们看起来与您在这里寻找的(非常常见的)格式完全不同.这样就行不通了.当然,ByValArray要求在编译时知道长度,因此它也不起作用(当你遇到).但是,奇怪的是,您可以对数组参数使用自定义编组,这很烦人,因为您必须将MarshalAsAttribute每个使用此类型的参数放在上面,而不是仅将其放在一个字段上,并且在您使用包含该字段的类型的任何位置都适用但是,请小姐.它看起来像这样:

[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);
Run Code Online (Sandbox Code Playgroud)

在那个例子中,我保留了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);
Run Code Online (Sandbox Code Playgroud)

好的,现在您可以看到如何声明数组元素类型(如果需要),以及如何将数组传递给非托管函数.但是,我们仍然需要自定义封送程序.您应该阅读" 实现ICustomMarshaler接口 ",但我将在此处介绍此内容,并提供内联注释.请注意,我使用了一些Marshal.SizeOf<T>()需要.NET 4.5.1或更高版本的简写约定(例如).

// 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>();
    }
}
Run Code Online (Sandbox Code Playgroud)

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


Ant*_*hyy 7

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

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

Marshal.Copy (new IntPtr ((long)nativeData + 4), bytes, 0, length) ;
Run Code Online (Sandbox Code Playgroud)

  • 遗憾的是,在.NET 4.0框架中不存在Marshal.ReadUInt32().有谁能解释为什么? (2认同)
  • 可能是因为无符号类型不符合 CLS。使用“Marshal.ReadIntNn”函数并手动转换为无符号。 (2认同)

Jon*_*son 5

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

尝试:

[MarshalAs(UnmanagedType.ByValArray, SizeConst=[whatever your size is]]
byte[] Data;
Run Code Online (Sandbox Code Playgroud)

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

System.Text.Encoding.UTF8.GetString(your byte array here). 
Run Code Online (Sandbox Code Playgroud)

显然,您需要根据需要改变编码,但UTF-8通常就足够了.

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

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

IntPtr Data;
Run Code Online (Sandbox Code Playgroud)

代替

[MarshalAs(UnmanagedType.ByValArray, SizeConst=[whatever your size is]]
byte[] Data;
Run Code Online (Sandbox Code Playgroud)

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

就像是:

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

Marshal.Copy(buffer, 0, yourABSObject.Data, length);
Run Code Online (Sandbox Code Playgroud)

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

Marshal.FreeHGlobal(yourABSObject.Data);
Run Code Online (Sandbox Code Playgroud)

  • 问题是长度是基于公共uint长度.但我不能在那里填写. (3认同)
  • @Jonathan:这里没有调用`Marshal.FreeHGlobal`,因为没有非托管数组可以释放.该数组是问题中声明的非托管结构的一部分.如果整个结构需要被释放,那是一个单独的事情.无论如何,必须通过分配器释放非托管指针,该分配器首先用于分配它们,并且有许多非托管内存分配器. (3认同)
  • 我认为你的答案会更容易理解,如果不是将"更新"添加到它的末尾,你只需*删除*不相关的部分.没有必要在问题中保留过去的回复迭代记录; 编辑历史已经有,以防万一有人真的想知道你的原始答案. (2认同)