Personalización de la serialización de estructuras

A veces, las reglas de serialización predeterminadas para las estructuras no es exactamente lo que necesita. Los entornos de ejecución de .NET proporcionan unos puntos de extensión que permiten personalizar el diseño de la estructura y cómo se serializan los campos. La personalización del diseño de la estructura es compatible con todos los escenarios, pero la personalización de la serialización de campos solo se admite en escenarios en los que la serialización en tiempo de ejecución está habilitada. Si la serialización en tiempo de ejecución está deshabilitada, cualquier serialización de campos debe realizarse manualmente.

Nota:

En este artículo no se trata la personalización de la serialización para la interoperabilidad generada por el origen. Si usa la interoperabilidad generada por el origen para P/Invokes o COM, consulte personalización de la serialización.

Personalización del diseño de la estructura

.NET proporciona el System.Runtime.InteropServices.StructLayoutAttribute atributo y la System.Runtime.InteropServices.LayoutKind enumeración para permitirle personalizar cómo se colocan los campos en la memoria. Las instrucciones siguientes le ayudarán a evitar problemas comunes.

✔️ CONSIDERE la posibilidad de usar LayoutKind.Sequential siempre que sea posible.

✔️ USAR solo LayoutKind.Explicit en la serialización cuando la estructura nativa tenga un diseño explícito, como una unión.

❌ EVITE el uso de clases para expresar tipos nativos complejos a través de la herencia.

❌ EVITA utilizar LayoutKind.Explicit al marshalling de estructuras en plataformas que no sean Windows si necesitas compilar para entornos de ejecución anteriores a .NET Core 3.0. El entorno de ejecución de .NET Core antes de la versión 3.0 no admite el paso de estructuras explícitas por valor a funciones nativas en sistemas intel o AMD de 64 bits no Windows. Sin embargo, el entorno de ejecución admite pasar estructuras explícitas por referencia en todas las plataformas.

Búferes de tamaño fijo y arreglos integrados

Si necesita definir un búfer de tamaño fijo en una estructura, no use la StructLayoutAttribute.Size propiedad para asignar espacio adicional. La propiedad Size controla la disposición no administrada del tipo (la disposición que se utiliza al llamar a Marshal.StructureToPtr) y solo afecta a la disposición administrada en el caso de los tipos blittables. En el caso de los tipos no blittables, el tiempo de ejecución solo asigna espacio en función de los campos reales, lo que puede provocar una corrupción de la memoria si se intenta utilizar el espacio adicional.

❌ EVITE usar StructLayoutAttribute.Size para crear búferes de tamaño fijo en tipos administrados.

✔️ UTILIZA System.Runtime.CompilerServices.InlineArrayAttribute para definir búferes de tamaño fijo en .NET moderno (C# 12 y versiones posteriores).

En el ejemplo siguiente se muestra el enfoque incorrecto que puede provocar daños en la memoria:

// ❌ DON'T: This is dangerous and can corrupt memory
[StructLayout(LayoutKind.Explicit, Size = 100)]
class Bad100ByteBuffer
{
    [FieldOffset(0)]
    private byte b;

    unsafe void UseBuffer()
    {
        fixed (byte* p = &b)
        {
            // Writing beyond the first byte corrupts memory
        }
    }
}

En su lugar, use InlineArrayAttribute para crear un búfer de tamaño correcto:

// ✔️ DO: Use InlineArrayAttribute for fixed-size buffers
class Good100ByteBuffer
{
    [InlineArray(100)]
    private struct Buffer100
    {
        private byte _element0;
    }

    private Buffer100 buffer;

    unsafe void UseBuffer()
    {
        fixed (byte* p = &buffer[0])
        {
            // The buffer is guaranteed to be 100 bytes
        }
    }
}

Para obtener más información sobre las matrices insertadas, consulte la referencia del lenguaje C#.

Personalización de la serialización de campos Booleanos

El código nativo tiene muchas representaciones booleanas diferentes. Solo en Windows, hay tres maneras de representar valores booleanos. El entorno de ejecución no conoce la definición nativa de tu estructura, por lo que lo mejor que puede hacer es adivinar cómo convertir tus valores Boolean. El entorno de ejecución de .NET proporciona una manera de indicar cómo serializar el campo Booleano. En los ejemplos siguientes se muestra cómo serializar el tipo bool de .NET a diferentes tipos nativos Booleanos.

Los valores booleanos predeterminados se ordenan como un valor nativo de 4 bytes de Win32 BOOL como se muestra en el siguiente ejemplo:

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

Si desea ser explícito, puede usar el UnmanagedType.Bool valor para obtener el mismo comportamiento que antes:

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

Con los valores UnmanagedType.U1 o UnmanagedType.I1 indicados a continuación, puede indicar al entorno de ejecución que trate el campo b como un tipo nativo bool de 1 byte.

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

En Windows, puede usar el valor UnmanagedType.VariantBool para indicar al entorno de ejecución que convierta el valor booleano en un valor VARIANT_BOOL de 2 bytes:

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

Nota:

VARIANT_BOOL es diferente de la mayoría de los tipos bool en ese VARIANT_TRUE = -1 y VARIANT_FALSE = 0. Además, todos los valores que no son iguales a VARIANT_TRUE se consideran false.

Personalización de la serialización de campos de matriz

.NET también incluye algunos ejemplos de cómo personalizar la serialización de matrices.

De forma predeterminada, .NET serializa las matrices como un puntero a una lista contigua de elementos:

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

Si está interactuando con API de COM, es posible que deba serializar las matrices como objetos SAFEARRAY*. Puede usar System.Runtime.InteropServices.MarshalAsAttribute y el valor UnmanagedType.SafeArray para indicar al entorno de ejecución que serialice una matriz como SAFEARRAY*:

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

Si necesita personalizar el tipo de elemento en SAFEARRAY, puede usar los MarshalAsAttribute.SafeArraySubType campos y MarshalAsAttribute.SafeArrayUserDefinedSubType para personalizar el tipo de elemento exacto de SAFEARRAY.

Si necesita serializar la matriz en contexto, puede usar el valor UnmanagedType.ByValArray para indicar el serializador con el fin de serializar la matriz en contexto. Cuando se usa esta serialización, también debe proporcionar un valor para el campo MarshalAsAttribute.SizeConst para el número de elementos de la matriz para que el entorno de ejecución pueda asignar correctamente espacio para la estructura.

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

Nota:

.NET no admite la serialización de un campo de matriz de longitud variable como un miembro de matriz flexible C99.

Personalización de la serialización de campos de cadena

.NET también proporciona una amplia variedad de personalizaciones para serializar los campos de cadena.

De forma predeterminada, .NET serializa una cadena como un puntero a una cadena terminada en un valor NULL. La codificación depende del valor del StructLayoutAttribute.CharSet campo en .System.Runtime.InteropServices.StructLayoutAttribute Si no se especifica ningún atributo, la codificación tiene como valor predeterminado una codificación 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.
};

Si necesita usar diferentes codificaciones para campos diferentes o prefiere ser más explícita en la definición de estructura, puede usar los UnmanagedType.LPStr valores o UnmanagedType.LPWStr en un System.Runtime.InteropServices.MarshalAsAttribute atributo.

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

Si desea serializar las cadenas mediante la codificación UTF-8, puede usar el valor UnmanagedType.LPUTF8Str en MarshalAsAttribute.

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

Nota:

El uso UnmanagedType.LPUTF8Str requiere .NET Framework 4.7 (o versiones posteriores) o .NET Core 1.1 (o versiones posteriores). No está disponible en .NET Standard 2.0.

Si está trabajando con API de COM, es posible que deba serializar una cadena como BSTR. Mediante el valor UnmanagedType.BStr, se puede serializar una cadena como BSTR.

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

Cuando se usa una API basada en WinRT, es posible que deba serializar una cadena como HSTRING. Mediante el valor UnmanagedType.HString, se puede serializar una cadena como HSTRING. HSTRING El marshaling solo se admite en entornos de ejecución con compatibilidad integrada con WinRT. La compatibilidad con WinRT se eliminó en .NET 5, por lo que el HSTRING marshalling no es compatible con .NET 5 ni con versiones posteriores.

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

Si su API requiere que pase la cadena directamente en la estructura, puede usar el valor UnmanagedType.ByValTStr. Tenga en cuenta que la codificación de una cadena serializada por ByValTStr se determina a partir del atributo CharSet. Además, requiere que se pase una longitud de la cadena mediante el 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.
};

Personalización de la serialización de campos decimales

Si está trabajando en Windows, es posible que encuentre algunas API que utilizan la estructura nativa CY o CURRENCY. De forma predeterminada, el tipo decimal de .NET serializa a la estructura DECIMAL nativa. Sin embargo, puede usar MarshalAsAttribute con el valor UnmanagedType.Currency para indicar al serializador que convierta un valor decimal al valor CY nativo.

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

Uniones

Una unión es un tipo de datos que puede contener diferentes tipos de datos sobre la misma memoria. Es una forma común de datos en el lenguaje C. Una unión se puede expresar en .NET mediante LayoutKind.Explicit. Se recomienda usar estructuras al definir una unión en .NET. El uso de clases puede provocar problemas de diseño y producir un comportamiento impredecible.

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

Serialización de System.Object

En Windows, puede serializar campos con tipo object al código nativo. Puede convertir estos campos en uno de estos tres tipos:

De forma predeterminada, un campo de tipo object se convertirá en un IUnknown* que encapsula el objeto.

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

Si desea serializar un campo de objeto a IDispatch*, agregue MarshalAsAttribute con el valor UnmanagedType.IDispatch.

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

Si quiere serializarlo como VARIANT, agregue MarshalAsAttribute con el valor UnmanagedType.Struct.

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

En la siguiente tabla se describe cómo los distintos tipos en tiempo de ejecución del campo obj se corresponden con los distintos tipos almacenados en un VARIANT:

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