Compartilhar via


Personalizar o marshalling de estrutura

Às vezes, as regras de marshalling padrão para estruturas não são exatamente o que você precisa. Os runtimes do .NET fornecem alguns pontos de extensão para você personalizar o layout de sua estrutura e como é feito o marshaling dos campos. A personalização do layout da estrutura tem suporte para todos os cenários, mas a personalização do marshalling de campo só tem suporte para cenários em que o marshalling de runtime está habilitado. Se o marshalling de runtime estiver desabilitado, qualquer marshalling de campo deverá ser feito manualmente.

Observação

Este artigo não aborda a personalização do marshalling para interoperabilidade gerada pela origem. Se você estiver usando a interoperabilidade gerada pela origem para P/Invokes ou COM, consulte personalização de marshalling.

Personalização do layout da estrutura

O .NET fornece o atributo System.Runtime.InteropServices.StructLayoutAttribute e a enumeração System.Runtime.InteropServices.LayoutKind para permitir que você personalize como os campos são colocados na memória. As diretrizes a seguir ajudarão a evitar problemas comuns.

✔️ CONSIDERE usar LayoutKind.Sequential sempre que possível.

✔️ USE LayoutKind.Explicit em marshalling apenas quando seu struct nativo também tiver um layout explícito, como uma união.

❌ EVITE usar classes para expressar tipos nativos complexos por meio de herança.

❌ EVITE usar LayoutKind.Explicit ao enviar estruturas em plataformas que não sejam do Windows se precisar direcionar runtimes antes do .NET Core 3.0. O runtime do .NET Core anterior ao 3.0 não dá suporte à passagem de estruturas explícitas por valor para funções nativas em sistemas não Windows de 64 bits AMD ou Intel. No entanto, o runtime dá suporte à passagem de estruturas explícitas por referência em todas as plataformas.

Como personalizar o marshalling de campo booliano

O código nativo tem muitas representações boolianas diferentes. Somente no Windows, há três formas de representar valores boolianos. O runtime não conhece a definição nativa de sua estrutura, portanto, o melhor que ele pode fazer é estimar como realizar marshaling de seus valores boolianos. O runtime do .NET permite indicar como realizar marshaling de seu campo booliano. Os exemplos a seguir mostram como realizar marshaling do .NET bool para diferentes tipos boolianos nativos.

Valores booleanos padrão para realizar marshal como um valor BOOL Win32 nativo de 4 bytes conforme mostrado no exemplo a seguir:

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

Se você quiser ser explícito, use o valor UnmanagedType.Bool para obter o mesmo comportamento descrito acima:

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

Usando os valores UnmanagedType.U1 ou UnmanagedType.I1 abaixo, você pode informar o runtime para realizar marshal do campo b como um tipo bool nativo de 1 byte.

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

No Windows, use o valor UnmanagedType.VariantBool para informar o runtime para realizar marshaling de seu valor booliano em um valor VARIANT_BOOL de 2 bytes:

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

Observação

VARIANT_BOOL é diferente da maioria dos tipos de bool em que VARIANT_TRUE = -1 e VARIANT_FALSE = 0. Além disso, todos os valores que não são iguais a VARIANT_TRUE são considerados falsos.

Personalização do marshalling de campo de matriz

O .NET também inclui algumas formas de personalizar o marshalling de matriz.

Por padrão, o .NET realiza marshal de matrizes como um ponteiro para uma lista contígua dos elementos:

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

Se você estiver interagindo com APIs COM, talvez seja necessário realizar marshal de matrizes como objetos SAFEARRAY*. Use o valor System.Runtime.InteropServices.MarshalAsAttribute e UnmanagedType.SafeArray para informar o runtime para realizar marshal de uma matriz como uma SAFEARRAY*:

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

Se for necessário personalizar o tipo de elemento que está na SAFEARRAY, use os campos MarshalAsAttribute.SafeArraySubType e MarshalAsAttribute.SafeArrayUserDefinedSubType para personalizar o tipo de elemento exato da SAFEARRAY.

Se você precisar realizar marshaling da matriz in-loco, use o valor UnmanagedType.ByValArray para dizer para o marshaller realizar marshaling da matriz in-loco. Ao usar esse marshalling, você também deve fornecer um valor ao campo MarshalAsAttribute.SizeConst para o número de elementos na matriz, para que o runtime possa alocar espaço corretamente para a estrutura.

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

Observação

O .NET não dá suporte à realização de marshal de um campo de matriz de comprimento variável como um Membro de Matriz Flexível C99.

Personalização do marshalling de campo de cadeia de caracteres

O .NET também fornece uma ampla variedade de personalizações para realizar marshal de campos de cadeia de caracteres.

Por padrão, o .NET realiza marshal de uma cadeia de caracteres como um ponteiro para uma cadeia de caracteres terminada em nulo. A codificação depende do valor do campo StructLayoutAttribute.CharSet no System.Runtime.InteropServices.StructLayoutAttribute. Se nenhum atributo for especificado, a codificação será padronizada para uma codificação 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.
};

Se você precisar usar codificações diferentes para campos distintos ou apenas prefere ser mais explícito em sua definição de struct, use os valores UnmanagedType.LPStr ou UnmanagedType.LPWStr em um atributo 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.
};

Se quiser realizar marshal de suas cadeias de caracteres usando a codificação UTF-8, use o valor UnmanagedType.LPUTF8Str em seu MarshalAsAttribute.

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

Observação

O uso de UnmanagedType.LPUTF8Str requer o .NET Framework 4.7 (ou versões posteriores) ou o .NET Core 1.1 (ou versões posteriores). Não está disponível no .NET Standard 2.0.

Se você estiver trabalhando com APIs COM, talvez seja necessário realizar marshal de uma cadeia de caracteres como um BSTR. Usando o valor UnmanagedType.BStr, você pode realizar marshal de uma cadeia de caracteres como um BSTR.

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

Ao usar uma API baseada no WinRT, talvez seja necessário realizar marshal de uma cadeia de caracteres como um HSTRING. Usando o valor UnmanagedType.HString, você pode realizar marshal de uma cadeia de caracteres como um HSTRING. O marshalling HSTRING só tem suporte em runtimes com suporte interno do WinRT. O suporte ao WinRT foi removido no .NET 5, portanto, não há suporte para marshalling HSTRING no .NET 5 ou mais recente.

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

Se sua API requer que você passe a cadeia de caracteres in-loco na estrutura, use o valor UnmanagedType.ByValTStr. A codificação de uma cadeia de caracteres com marshal realizado por ByValTStr é determinada por meio do atributo CharSet. Além disso, ela requer que o comprimento da cadeia de caracteres seja passado pelo campo 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.
};

Personalização do marshalling de campo decimal

Se você estiver trabalhando no Windows, poderá encontrar algumas APIs que usam a estrutura CY ou CURRENCY nativa. Por padrão, o tipo .NET decimal realizar marshal da estrutura nativa DECIMAL. No entanto, você pode usar um MarshalAsAttribute com o valor UnmanagedType.Currency para instruir o marshaller a converter um valor decimal em um valor CY nativo.

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

Uniões

Uma união é um tipo de dados que pode conter diferentes tipos de dados na mesma memória. É uma forma comum de dados na linguagem C. Uma união pode ser expressa no .NET usando LayoutKind.Explicit. É recomendável usar structs ao definir uma união no .NET. O uso de classes pode causar problemas de layout e produzir um comportamento imprevisível.

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;
    }
}

Realizar marshaling de System.Object

No Windows, você pode realizar marshal dos campos tipados de object para o código nativo. Você pode realizar marshal desses campos para um dos três tipos:

Por padrão, um campo tipado de object terá o marshal realizado para um IUnknown* que envolve o objeto.

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

Se você quiser realizar marshal de um campo de objeto para um IDispatch*, adicione um MarshalAsAttribute com o valor UnmanagedType.IDispatch.

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

Se você quiser realizar marshal dele como um VARIANT, adicione um MarshalAsAttribute com o valor UnmanagedType.Struct.

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

A tabela a seguir descreve como diferentes tipos de runtime do campo obj são mapeados para os vários tipos armazenados em um VARIANT:

Tipo .NET Tipo de VARIANTE
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