Serializar tipos

La serialización es el proceso de transformación de tipos cuando tienen que cruzar el límite entre administrado y nativo.

La serialización es necesaria porque los tipos del código administrado y del código no administrado son diferentes. En el código administrado, por ejemplo, tiene String, mientras que en el entorno no administrado, las cadenas pueden ser Unicode ("anchas"), no Unicode, terminadas en un valor nulo, ASCII, etc. De forma predeterminada, el subsistema de P/Invoke intenta hacer "lo correcto" según el comportamiento predeterminado, tal y como se describe en este artículo. Pero en aquellas situaciones en las que necesita un control adicional, puede emplear el atributo MarshalAs para especificar qué tipo se espera en el lado no administrado. Por ejemplo, si queremos que la cadena se envíe como una cadena ANSI terminada en un valor nulo, podemos hacerlo de la manera siguiente:

[DllImport("somenativelibrary.dll")]
static extern int MethodA([MarshalAs(UnmanagedType.LPStr)] string parameter);

Si se aplica el atributo System.Runtime.CompilerServices.DisableRuntimeMarshallingAttribute al ensamblado, no se aplican las reglas de la sección siguiente. Para obtener información sobre cómo se exponen los valores de .NET al código nativo cuando se aplica este atributo, vea Serialización en tiempo de ejecución deshabilitada.

Reglas predeterminadas para serializar tipos comunes

Por lo general, el entorno de ejecución intenta hacer "lo correcto" al serializar para requerir la menor cantidad de trabajo. En las tablas siguientes se describen cómo se serializa cada tipo de forma predeterminada cuando se utiliza en un parámetro o campo. El entero de ancho fijo C99/C++11 y los tipos de caracteres se usan para garantizar que la tabla siguiente es correcta para todas las plataformas. Puede usar cualquier tipo nativo que tenga la misma alineación y los requisitos de tamaño que estos tipos.

Esta primera tabla describe las asignaciones para distintos tipos para los que la serialización es el misma que en la serialización de campos y de P/Invoke.

Palabra clave de C# Tipo de .NET Tipo nativo
byte System.Byte uint8_t
sbyte System.SByte int8_t
short System.Int16 int16_t
ushort System.UInt16 uint16_t
int System.Int32 int32_t
uint System.UInt32 uint32_t
long System.Int64 int64_t
ulong System.UInt64 uint64_t
char System.Char char o char16_t según el elemento CharSet de la estructura o P/Invoke. Consulte la documentación de juego de caracteres.
System.Char char* o char16_t* según el elemento CharSet de la estructura o P/Invoke. Consulte la documentación de juego de caracteres.
nint System.IntPtr intptr_t
nuint System.UIntPtr uintptr_t
Tipos de puntero de .NET (por ejemplo, void*) void*
Tipo derivado de System.Runtime.InteropServices.SafeHandle void*
Tipo derivado de System.Runtime.InteropServices.CriticalHandle void*
bool System.Boolean Tipo BOOL de Win32
decimal System.Decimal Estructura DECIMAL de COM
Delegado de .NET Puntero de función nativa
System.DateTime Tipo DATE de Win32
System.Guid Tipo GUID de Win32

Algunas categorías de serialización tienen distintos valores predeterminados si está serializando un parámetro o una estructura.

Tipo de .NET Tipo nativo (parámetro) Tipo nativo (campo)
Matriz de .NET Un puntero al inicio de una matriz de representaciones nativas de los elementos de matriz. No se permite sin un atributo [MarshalAs]
Una clase con LayoutKind de Sequential o Explicit Un puntero a la representación nativa de la clase Representación nativa de la clase

En la tabla siguiente se incluyen las reglas de serialización predeterminadas que son solo de Windows. En las plataformas que no sean Windows, no puede serializar estos tipos.

Tipo de .NET Tipo nativo (parámetro) Tipo nativo (campo)
System.Object VARIANT IUnknown*
System.Array Interfaz COM No se permite sin un atributo [MarshalAs]
System.ArgIterator va_list No permitido
System.Collections.IEnumerator IEnumVARIANT* No permitido
System.Collections.IEnumerable IDispatch* No permitido
System.DateTimeOffset int64_t que representa el número de tics desde la medianoche del 1 de enero de 1601 int64_t que representa el número de tics desde la medianoche del 1 de enero de 1601

Algunos tipos solo pueden serializarse como parámetros y no como campos. Estas herramientas se muestran en la tabla siguiente:

Tipo de .NET Tipo nativo (solo parámetros)
System.Text.StringBuilder char* o char16_t* según el elemento CharSet de P/Invoke. Consulte la documentación de juego de caracteres.
System.ArgIterator va_list (solo en Windows x86/x64/arm64)
System.Runtime.InteropServices.ArrayWithOffset void*
System.Runtime.InteropServices.HandleRef void*

Si estos valores predeterminados no hacen exactamente lo que desea, puede personalizar cómo serializar los parámetros. El artículo sobre serialización de parámetros explica cómo personalizar la forma en que se serializan los diferentes tipos de parámetro.

Serialización predeterminada en escenarios COM

Cuando se llama a métodos en objetos COM en .NET, el runtime de .NET cambia las reglas de serialización predeterminadas para que coincidan con la semántica común de COM. En la tabla siguiente se enumeran las reglas que usan los runtime de .NET en escenarios de COM:

Tipo de .NET Tipo nativo (llamadas a métodos COM)
System.Boolean VARIANT_BOOL
StringBuilder LPWSTR
System.String BSTR
Tipos delegados _Delegate* en .NET Framework. No se permite en .NET Core, ni en .NET 5 y versiones posteriores.
System.Drawing.Color OLECOLOR
Matriz de .NET SAFEARRAY
System.String[] SAFEARRAY de BSTR

Serializar clases y estructuras

Otro aspecto de la serialización de tipos es cómo pasar una estructura a un método no administrado. Por ejemplo, algunos métodos no administrados requieren una estructura como parámetro. En estos casos, debemos crear una clase o una estructura correspondiente en la parte administrada del entorno para usarla como un parámetro. Pero no basta con definir la clase. También es necesario indicarle al serializador cómo asignar campos de la clase a la estructura no administrada. Aquí el atributo StructLayout resulta útil.

[DllImport("kernel32.dll")]
static extern void GetSystemTime(SystemTime systemTime);

[StructLayout(LayoutKind.Sequential)]
class SystemTime {
    public ushort Year;
    public ushort Month;
    public ushort DayOfWeek;
    public ushort Day;
    public ushort Hour;
    public ushort Minute;
    public ushort Second;
    public ushort Millisecond;
}

public static void Main(string[] args) {
    SystemTime st = new SystemTime();
    GetSystemTime(st);
    Console.WriteLine(st.Year);
}

En el ejemplo anterior se muestra un ejemplo simple de una llamada a la función GetSystemTime(). La parte interesante está en la línea 4. El atributo especifica que los campos de la clase se deben asignar secuencialmente a la estructura en el otro lado (el lado no administrado). Esto significa que la denominación de los campos no es importante. Solo su orden es importante, ya que es necesario que coincida con la estructura no administrada, tal como se muestra en el siguiente ejemplo:

typedef struct _SYSTEMTIME {
  WORD wYear;
  WORD wMonth;
  WORD wDayOfWeek;
  WORD wDay;
  WORD wHour;
  WORD wMinute;
  WORD wSecond;
  WORD wMilliseconds;
} SYSTEMTIME, *PSYSTEMTIME;

A veces, la serialización predeterminada para la estructura no hace lo que necesita. El artículo Personalización de la serialización de estructuras enseña a personalizar el modo de serializar la estructura.