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 un string
, mientras que las cadenas no administradas pueden tener codificación .NET string
(UTF-16), codificación ANSI Code Page, UTF-8, terminación nula, 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 UTF-8 terminada en un valor nulo, podemos hacerlo de la manera siguiente:
[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 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 en función de la codificación de P/Invoke o de la estructura. Consulte la documentación de juego de caracteres. |
System.Char |
char* o char16_t* en función de la codificación de P/Invoke o de la estructura. 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.
[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);
}
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.