Ответ 1
Старый вопрос, но я недавно должен был сделать это сам, и все существующие ответы плохие, поэтому...
Лучшим решением для маршалинга массива переменной длины в структуре является использование настраиваемого маршалера. Это позволяет вам управлять кодом, который использует среда выполнения для преобразования управляемых и неуправляемых данных. К сожалению, пользовательский маршалинг плохо документирован и имеет несколько странных ограничений. Я быстро их рассмотрю, затем перейду к решению.
Раздражающе, вы не можете использовать настраиваемый маршалинг для элемента массива структуры или класса. Там нет документированной или логической причины этого ограничения, и компилятор не будет жаловаться, но вы получите исключение во время выполнения. Кроме того, существует функция, которую должны реализовать пользовательские маршалисты, 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);
Здесь наивная версия того, как вы могли использовать пользовательский маршалер (который действительно должен был сработать). Я доберусь до самого маршалера...
[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 требует, чтобы длина была известна во время компиляции, так что она не работает (как вы столкнулись). Однако, как бы странно, вы можете использовать настраиваемое маршалинг по параметрам массива. Это раздражает, потому что вы должны поместить 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);
ОК, теперь вы можете увидеть, как объявить тип элемента массива (если необходимо) и как передать массив неуправляемой функции. Тем не менее, нам все еще нужен этот пользовательский маршалер. Вы должны прочитать "" Внедрение интерфейса 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>();
}
}
Ну, это было долго! Ну, вот оно. Я надеюсь, что люди это видят, потому что там много плохих ответов и недоразумений...