虽然这是个老问题,但最近我不得不自己解决它,所有现有的答案都很差,所以...
在结构体中编组可变长度数组的最佳解决方案是使用自定义编组程序。这让您控制运行时用于在托管和非托管数据之间转换的代码。不幸的是,自定义编组程序文档不全,且存在一些奇怪的限制。我将快速介绍这些限制,然后介绍解决方案。
令人恼火的是,您不能在结构或类的数组成员上使用自定义编组程序。没有文件记录或逻辑原因可以解释这种限制,编译器也不会发出警告,但在运行时会引发异常。此外,自定义编组程序必须实现一个函数int GetNativeDataSize()
,显然无法准确地实现它(它不会传递对象实例来询问其大小,因此您只能根据类型进行推断,而类型当然是可变大小的!)幸运的是,此函数并不重要。我从未见过它被调用,即使它返回虚假值(MSDN的一个示例将其返回为 -1),自定义编组程序也可以正常工作。
首先,这是我认为您的本机原型可能看起来像的东西(我在这里使用P/Invoke,但它也适用于COM):
这是您可能会使用自定义编组程序的天真版本(实际上应该很有效)。稍后我会详细介绍编组程序本身...
[StructLayout(LayoutKind.Sequential)]
public struct abs_data
{
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef=typeof(ArrayMarshaler<byte>))]
public byte[] Data;
}
[DllImport("libname.dll")] public extern void DoThing (ref abs_data pData);
很不幸,运行时你似乎不能将数组作为除了SafeArray
或ByValArray
之外的任何数据结构进行编组。SafeArrays是有计数的,但它们看起来与你在这里寻找的(非常常见的)格式完全不同。所以那行不通。当然,ByValArray要求在编译时知道长度,因此也行不通(就像你遇到的那样)。但奇怪的是,你可以对数组参数使用自定义编组。这很麻烦,因为你必须在每个使用该类型的参数上放置MarshalAsAttribute
,而不仅仅是在一个字段上放置它,并且让它适用于你使用包含该字段的类型的所有地方,但这就是生活。它看起来像这样:
[StructLayout(LayoutKind.Sequential)]
public struct abs_data
{
public byte Data;
}
[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>))]
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)
好的,现在您可以看到如何声明数组元素类型(如果需要),以及如何将数组传递给非托管函数。但是,我们仍然需要自定义的marshaler。您应该阅读“实现ICustomMarshaler接口”,但我会在此处进行介绍,并带有内联注释。请注意,我使用了一些简写约定(例如Marshal.SizeOf<T>()
),这需要.NET 4.5.1或更高版本。
public class ArrayMarshaler<T> : ICustomMarshaler
{
public static ICustomMarshaler GetInstance (String cookie)
{
return new ArrayMarshaler<T>();
}
public Object MarshalNativeToManaged (IntPtr pNativeData)
{
if (IntPtr.Zero == pNativeData) return null;
int length = Marshal.ReadInt32(pNativeData);
T[] array = new T[length];
int elSiz = Marshal.SizeOf<T>();
for (int i = 0; i < length; i++)
{
array[i] = Marshal.PtrToStructure<T>(pNativeData + sizeof(int) + (elSiz * i));
}
return array;
}
public IntPtr MarshalManagedToNative (Object ManagedObject)
{
if (null == ManagedObject) return IntPtr.Zero;
T[] array = (T[])ManagedObj;
int elSiz = Marshal.SizeOf<T>();
int size = sizeof(int) + (elSiz * array.Length);
IntPtr ptr = Marshal.AllocHGlobal(size);
Marshal.WriteInt32(ptr, array.Length);
for (int i = 0; i < array.Length; i++)
{
Marshal.StructureToPtr<T>(array[i], ptr + sizeof(int) + (elSiz * i), false);
}
return ptr;
}
public void CleanUpNativeData (IntPtr pNativeData)
{
Marshal.FreeHGlobal(pNativeData);
}
public void CleanUpManagedData (Object ManagedObj)
{ }
public int GetNativeDataSize ()
{
return sizeof(int) + Marshal.SizeOf<T>();
}
}
哇,这太长了!好吧,你已经看到了。我希望人们能看到这篇文章,因为有很多错误答案和误解存在...