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

我如何编组这个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; } 

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

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

令人讨厌的是,您不能在结构或类的数组元素上使用自定义封送处理。 此限制没有记录或逻辑上的原因,编译器不会抱怨,但您将在运行时获得exception。 此外,还有一个自定义封送程序必须实现的函数,即int GetNativeDataSize() ,这显然无法准确实现(它不会传递给对象的实例来询问它的大小,所以你只能int GetNativeDataSize()这个类型,当然是可变的大小!)幸运的是,这个function并不重要。 我从来没有看到它被调用,并且自定义编组程序工作正常,即使它返回一个虚假值(一个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); 

这是你如何使用自定义封送程序(它确实应该有效)的天真版本。 我会稍微谈谈编组人员……

 [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))] 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之外的任何ByValArray 。 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))] // Don't need to use "ref" anymore; arrays are ref types and pass as pointer-to abs_data[] pData); 

在那个例子中,我保留了abs_data类型,以防你想对它做一些特殊的事情(构造函数,静态函数,属性,inheritance等等)。 如果数组元素由复杂类型组成,则可以修改结构以表示该复杂类型。 但是,在这种情况下, 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[] pData); 

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

 // 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 : ICustomMarshaler { // All custom marshalers require a static factory method with this signature. public static ICustomMarshaler GetInstance (String cookie) { return new ArrayMarshaler(); } // 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(); // Populate the array for (int i = 0; i < length; i++) { array[i] = Marshal.PtrToStructure(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(); // 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(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(); } } 

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

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

 IntPtr nativeData = ... ; var length = Marshal.ReadUInt32 (nativeData) ; var bytes = new byte[length] ; Marshal.Copy (new IntPtr ((long)nativeData + 4), bytes, 0, length) ; 

如果保存的数据不是字符串,则不必将其存储在字符串中。 除非原始数据类型是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); 

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

 Marshal.FreeHGlobal(yourABSObject.Data); 

你试图编组一个byte[ABS_VARLEN]东西,好像它是一个长度为1的string 。你需要弄清楚ABS_VARLEN常量是什么,并将数组编组为:

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

(1024有一个占位符;填写ASB_VARLEN的实际值。)

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

假设您需要将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(); }