Compartir a través de


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 marshalización es necesaria porque los tipos del código administrado y no administrado son diferentes. En el código administrado, por ejemplo, tiene un string, mientras que las cadenas no administradas pueden ser codificación de .NET string (UTF-16), codificación de página de códigos ANSI, UTF-8, terminada en null, ASCII, etc. De forma predeterminada, el subsistema P/Invoke intenta hacer lo correcto en función del comportamiento predeterminado, descrito en este artículo. Sin embargo, para aquellas situaciones en las que necesita un control adicional, puede emplear el atributo MarshalAs para especificar cuál es el tipo esperado en el lado no administrado. Por ejemplo, si desea que la cadena se envíe como una cadena UTF-8 terminada en NULL, puede hacerlo de la siguiente manera:

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

// or

[LibraryImport("somenativelibrary.dll", StringMarshalling = StringMarshalling.Utf8)]
static extern int MethodB(string parameter);

Si aplica el System.Runtime.CompilerServices.DisableRuntimeMarshallingAttribute atributo 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, consulte 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 describe cómo se procesa por defecto cada tipo cuando se usa en un parámetro o campo. Los tipos de caracteres y enteros de ancho fijo de C99/C++11 se usan para asegurarse de que la tabla siguiente es correcta para todas las plataformas. Puede usar cualquier tipo nativo que tenga los mismos requisitos de alineación y 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 .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 Ya sea char o char16_t en función de la codificación de P/Invoke o de la estructura. Consulte la documentación del conjunto de caracteres.
System.Char Ya sea char* o char16_t* en función de la codificación de P/Invoke o de la estructura. Consulte la documentación del conjunto 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 Win32 BOOL
decimal System.Decimal Estructura DECIMAL de COM
Delegado de .NET Puntero de función nativa
System.DateTime Tipo Win32 DATE
System.Guid Tipo Win32 GUID

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

Tipo .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 permitido sin un [MarshalAs] atributo
Una clase con LayoutKind, 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 .NET Tipo nativo (parámetro) Tipo nativo (campo)
System.Object VARIANT IUnknown*
System.Array Interfaz COM No permitido sin un [MarshalAs] atributo
System.ArgIterator va_list No permitida
System.Collections.IEnumerator IEnumVARIANT* No permitida
System.Collections.IEnumerable IDispatch* No permitida
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 se pueden organizar como parámetros y no como campos. Estos tipos se enumeran en la tabla siguiente:

Tipo .NET Tipo nativo (solo parámetro)
System.Text.StringBuilder Ya sea char* o char16_t* dependiendo de CharSet de P/Invoke. Consulte la documentación del conjunto 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 se gestionan los parámetros. El artículo de serialización de parámetros le guía a través de cómo personalizar cómo se serializan los distintos tipos de parámetros.

Serialización predeterminada en escenarios COM

Al llamar a métodos en objetos COM en .NET, el tiempo de ejecución de .NET modifica las reglas de marshaling predeterminadas para que se ajusten a la semántica COM común. En la tabla siguiente se enumeran las reglas que usan los entornos de ejecución de .NET en escenarios COM:

Tipo .NET Tipo nativo (llamadas al método COM)
System.Boolean VARIANT_BOOL
StringBuilder LPWSTR
System.String BSTR
Tipos delegados _Delegate* en .NET Framework. No permitido en .NET Core y .NET 5+.
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 de los métodos no administrados requieren una estructura como parámetro. En estos casos, debe crear una estructura correspondiente o una clase en parte administrada del mundo para usarla como 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 StructLayout atributo resulta útil.

[LibraryImport("kernel32.dll")]
static partial void GetSystemTime(out SystemTime systemTime);

[StructLayout(LayoutKind.Sequential)]
struct 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);
}

El código anterior muestra un ejemplo sencillo de llamar a una GetSystemTime() función. 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 del otro lado (no administrado). Esto significa que la nomenclatura de los campos no es importante, solo su orden es importante, ya que debe corresponder a la estructura no administrada, que se muestra en el ejemplo siguiente:

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.