Поделиться через


Настройка маршаллинга структуры

Иногда стандартные правила маршалинга структур не совсем подходят. Среды выполнения .NET предоставляют несколько точек расширения для настройки макета структуры и способов маршалловки полей. Настройка макета структуры поддерживается для всех сценариев, но настройка маршаллинга полей поддерживается только для сценариев, в которых включена маршалинг среды выполнения. Если маршалирование среды выполнения отключено, то любое маршалирование полей должно выполняться вручную.

Примечание.

В этой статье не описывается настройка маршалинга для создаваемого источника взаимодействия. Если вы используете созданный источник взаимодействия для P/Invokes или COM, ознакомьтесь с настройкой маршаллинга.

Настройка макета структуры

На платформе .NET предусмотрен атрибут System.Runtime.InteropServices.StructLayoutAttribute и перечисление System.Runtime.InteropServices.LayoutKind, которые позволяют настроить способ размещения полей в памяти. Следуйте указаниям ниже, чтобы избежать распространенных проблем.

✔️ ДОПУСТИМО использовать LayoutKind.Sequential во всех возможных случаях.

✔️ Do use LayoutKind.Explicit only in маршаллинга, если собственная структура также имеет явный макет, например объединение.

❌ Избегайте использования классов для выражения сложных собственных типов с помощью наследования.

❌ Избегайте использования LayoutKind.Explicit при маршаллинговых структурах на платформах, отличных от Windows, если необходимо использовать целевые среды выполнения до .NET Core 3.0. Среда выполнения .NET Core, предшествующая версии 3.0, не поддерживает передачу явных структур по значению в собственные функции в отличных от Windows 64-разрядных системах на базе процессора Intel или AMD. Но она поддерживает передачу явных структур по ссылке на всех платформах.

Настройка логического маршаллинга полей

Машинный код имеет множество логических представлений. Только в Windows существует три способа представления логических значений. Среда выполнения не знает собственного определения структуры, поэтому лучше всего сделать предположение о том, как маршалировать логические значения. Среда выполнения .NET позволяет указать, как маршалировать логическое поле. В следующих примерах показано, как маршалировать .NET bool в разные собственные логические типы.

По умолчанию логические значения маршалируются как собственное 4-байтное значение Win32 BOOL, как показано в примере ниже:

public struct WinBool
{
    public bool b;
}
struct WinBool
{
    public BOOL b;
};

Если необходимо передать явную структуру, можно использовать значение UnmanagedType.Bool, чтобы получить описанное выше поведение:

public struct WinBool
{
    [MarshalAs(UnmanagedType.Bool)]
    public bool b;
}
struct WinBool
{
    public BOOL b;
};

Если использовать значение UnmanagedType.U1 или UnmanagedType.I1 ниже, среда выполнения сможет маршалировать поле b как 1-байтный собственный тип bool.

public struct CBool
{
    [MarshalAs(UnmanagedType.U1)]
    public bool b;
}
struct CBool
{
    public bool b;
};

В Windows можно использовать UnmanagedType.VariantBool значение, чтобы сообщить среде выполнения маршалировать логическое значение 2-байтов VARIANT_BOOL :

public struct VariantBool
{
    [MarshalAs(UnmanagedType.VariantBool)]
    public bool b;
}
struct VariantBool
{
    public VARIANT_BOOL b;
};

Примечание.

Значение VARIANT_BOOL отличается от большинства типов логических значений в VARIANT_TRUE = -1 и VARIANT_FALSE = 0. Кроме того, все значения, которые не равны VARIANT_TRUE, считаются ложными.

Настройка маршалинга полей массивов

На платформе .NET также предусмотрено несколько способов настройки маршалинга массивов.

По умолчанию .NET маршалирует массивы как указатель на непрерывный список элементов:

public struct DefaultArray
{
    public int[] values;
}
struct DefaultArray
{
    int32_t* values;
};

Если вы взаимодействуете с API COM, возможно, понадобится маршалировать массивы как объекты SAFEARRAY*. Можно использовать значения System.Runtime.InteropServices.MarshalAsAttribute и UnmanagedType.SafeArray, чтобы среда выполнения маршалировала массив как SAFEARRAY*:

public struct SafeArrayExample
{
    [MarshalAs(UnmanagedType.SafeArray)]
    public int[] values;
}
struct SafeArrayExample
{
    SAFEARRAY* values;
};

Если нужно настроить тип элемента для SAFEARRAY, можно использовать поля MarshalAsAttribute.SafeArraySubType и MarshalAsAttribute.SafeArrayUserDefinedSubType для настройки точного типа элемента SAFEARRAY.

Если вам нужно маршалировать массив на месте, можно использовать UnmanagedType.ByValArray значение, чтобы сообщить маршалу, чтобы маршализатор маршалалировать массив на месте. При применении такого маршалинга также необходимо указать в поле MarshalAsAttribute.SizeConst количество элементов в массиве, чтобы среда выполнения могла правильно выделить место для структуры.

public struct InPlaceArray
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public int[] values;
}
struct InPlaceArray
{
    int values[4];
};

Примечание.

.NET не поддерживает маршалинг поля массива переменной длины как элемента гибкого массива C99.

Настройка маршалинга полей строки

В .NET также предусмотрены разнообразные варианты настроек для маршалинга полей строки.

По умолчанию .NET маршалирует строку как указатель на строку, которая оканчивается значением NULL. Кодирование зависит от значения поля StructLayoutAttribute.CharSet в атрибуте System.Runtime.InteropServices.StructLayoutAttribute. Если атрибут не указан, по умолчанию используется кодирование ANSI.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DefaultString
{
    public string str;
}
struct DefaultString
{
    char* str;
};
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct DefaultString
{
    public string str;
}
struct DefaultString
{
    char16_t* str; // Could also be wchar_t* on Windows.
};

Если нужно использовать разные кодировки для разных полей или более явное определение структуры, можно указать значение UnmanagedType.LPStr или UnmanagedType.LPWStr в атрибуте System.Runtime.InteropServices.MarshalAsAttribute.

public struct AnsiString
{
    [MarshalAs(UnmanagedType.LPStr)]
    public string str;
}
struct AnsiString
{
    char* str;
};
public struct UnicodeString
{
    [MarshalAs(UnmanagedType.LPWStr)]
    public string str;
}
struct UnicodeString
{
    char16_t* str; // Could also be wchar_t* on Windows.
};

Чтобы маршалировать строки с кодированием UTF-8, можно использовать значение UnmanagedType.LPUTF8Str в атрибуте MarshalAsAttribute.

public struct UTF8String
{
    [MarshalAs(UnmanagedType.LPUTF8Str)]
    public string str;
}
struct UTF8String
{
    char* str;
};

Примечание.

Значение UnmanagedType.LPUTF8Str доступно только на платформе .NET Framework 4.7 (или более поздних версий) или .NET Core 1.1 (или более поздних версий). Оно недоступно на платформе .NET Standard 2.0.

Если вы работаете с API COM, возможно, вам потребуется маршалировать строку как BSTR. Вы можете маршалировать строку как BSTR, используя значение UnmanagedType.BStr.

public struct BString
{
    [MarshalAs(UnmanagedType.BStr)]
    public string str;
}
struct BString
{
    BSTR str;
};

Если используется API на базе WinRT, возможно, потребуется маршалировать строку как HSTRING. Вы можете маршалировать строку как HSTRING, используя значение UnmanagedType.HString. HSTRING Маршаллирование поддерживается только в средах выполнения со встроенной поддержкой WinRT. Поддержка WinRT была удалена в .NET 5, поэтому HSTRING маршаллинг не поддерживается в .NET 5 или более поздней версии.

public struct HString
{
    [MarshalAs(UnmanagedType.HString)]
    public string str;
}
struct BString
{
    HSTRING str;
};

Если API требует передавать строку на месте в структуре, можно использовать значение UnmanagedType.ByValTStr. Обратите внимание, что кодирование для строки, которую маршалирует значение ByValTStr, определяется атрибутом CharSet. Кроме того, поле MarshalAsAttribute.SizeConst должно передавать значение длины строки.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DefaultString
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 4)]
    public string str;
}
struct DefaultString
{
    char str[4];
};
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct DefaultString
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 4)]
    public string str;
}
struct DefaultString
{
    char16_t str[4]; // Could also be wchar_t[4] on Windows.
};

Настройка маршалинга полей десятичных чисел

Если вы работаете в системе Windows, то некоторые API могут использовать собственную структуру CY или CURRENCY. По умолчанию тип .NET decimal маршалируется в собственную структуру DECIMAL. Однако можно использовать MarshalAsAttribute значение со UnmanagedType.Currency значением, чтобы указать маршаллеру преобразовать decimal значение в собственное CY значение.

public struct Currency
{
    [MarshalAs(UnmanagedType.Currency)]
    public decimal dec;
}
struct Currency
{
    CY dec;
};

Объединения

Объединение — это тип данных, который может содержать различные типы данных на вершине одной памяти. Это общая форма данных на языке C. Объединение может быть выражено в .NET с помощью LayoutKind.Explicit. Рекомендуется использовать структуры при определении объединения в .NET. Использование классов может вызвать проблемы макета и привести к непредсказуемому поведению.

struct device1_config
{
    void* a;
    void* b;
    void* c;
};
struct device2_config
{
    int32_t a;
    int32_t b;
};
struct config
{
    int32_t type;

    union
    {
        device1_config dev1;
        device2_config dev2;
    };
};
public unsafe struct Device1Config
{
    void* a;
    void* b;
    void* c;
}

public struct Device2Config
{
    int a;
    int b;
}

public struct Config
{
    public int Type;

    public _Union Anonymous;

    [StructLayout(LayoutKind.Explicit)]
    public struct _Union
    {
        [FieldOffset(0)]
        public Device1Config Dev1;

        [FieldOffset(0)]
        public Device2Config Dev2;
    }
}

Маршалинг System.Object

В системе Windows можно маршалировать поля типа object в машинный код. Эти поля можно маршалировать в один из трех типов:

По умолчанию поля типа object маршалируются в тип IUnknown*, который создает оболочку объекта.

public struct ObjectDefault
{
    public object obj;
}
struct ObjectDefault
{
    IUnknown* obj;
};

Чтобы маршалировать поле объекта в тип IDispatch*, добавьте атрибут MarshalAsAttribute со значением UnmanagedType.IDispatch.

public struct ObjectDispatch
{
    [MarshalAs(UnmanagedType.IDispatch)]
    public object obj;
}
struct ObjectDispatch
{
    IDispatch* obj;
};

Чтобы маршалировать его как VARIANT, добавьте атрибут MarshalAsAttribute со значением UnmanagedType.Struct.

public struct ObjectVariant
{
    [MarshalAs(UnmanagedType.Struct)]
    public object obj;
}
struct ObjectVariant
{
    VARIANT obj;
};

В приведенной ниже таблице показано, как разные типы среды выполнения поля obj соотносятся с разными типами, которые сохраняются в VARIANT:

Тип .NET Тип VARIANT
byte VT_UI1
sbyte VT_I1
short VT_I2
ushort VT_UI2
int VT_I4
uint VT_UI4
long VT_I8
ulong VT_UI8
float VT_R4
double VT_R8
char VT_UI2
string VT_BSTR
System.Runtime.InteropServices.BStrWrapper VT_BSTR
object VT_DISPATCH
System.Runtime.InteropServices.UnknownWrapper VT_UNKNOWN
System.Runtime.InteropServices.DispatchWrapper VT_DISPATCH
System.Reflection.Missing VT_ERROR
(object)null VT_EMPTY
bool VT_BOOL
System.DateTime VT_DATE
decimal VT_DECIMAL
System.Runtime.InteropServices.CurrencyWrapper VT_CURRENCY
System.DBNull VT_NULL