Compartir a través de


Comportamiento de serialización predeterminado

La serialización de interoperabilidad funciona con reglas que dictan cómo se comportan los datos asociados con parámetros de método cuando pasan entre memoria administrada y no administrada. Estas reglas integradas controlan las actividades de serialización como transformaciones de tipos de datos, si un destinatario puede cambiar los datos que recibe y devolver esos cambios al llamador, y en qué circunstancias el serializador proporciona optimizaciones de rendimiento.

En esta sección se identifican las características predeterminadas de comportamiento del servicio de serialización de interoperabilidad. Muestra información detallada sobre la serialización de matrices, tipos booleanos, tipos de caracteres, delegados, clases, objetos, cadenas y estructuras.

Nota:

No se admite la serialización de tipos genéricos. Para obtener más información, consulte Interoperación mediante tipos genéricos.

Administración de memoria con el serializador de interoperabilidad

El serializador de interoperabiliad siempre intenta liberar memoria asignada por código no administrado. Este comportamiento cumple con las reglas de administración de memoria COM, pero difiere de las reglas que rigen C++nativo.

Se puede producir confusión si se prevé un comportamiento de C++ nativo (sin liberación de memoria) al usar invocación de plataforma, que automáticamente libera memoria para los punteros. Por ejemplo, llamar al siguiente método no administrado desde un archivo DLL de C++ no libera automáticamente ninguna memoria.

Firma sin gestión

BSTR MethodOne (BSTR b) {  
     return b;  
}  

Sin embargo, si define el método como un prototipo de invocación de plataforma, reemplace cada tipo BSTR por un tipo String, y llame a MethodOne, el Common Language Runtime intentará liberar b dos veces. Puedes cambiar el comportamiento de serialización mediante tipos IntPtr en lugar de tipos String.

El tiempo de ejecución siempre usa el método CoTaskMemFree en Windows y el método libre en otras plataformas para liberar memoria. Si la memoria con la que trabaja no se asignó con el método CoTaskMemAlloc en Windows o el método malloc en otras plataformas, debe usar intPtr y liberar la memoria manualmente mediante el método adecuado. Del mismo modo, puede evitar la libreción automática de memoria en situaciones en las que nunca se debe liberar memoria, como cuando se usa la función GetCommandLine de Kernel32.dll, que devuelve un puntero a la memoria del kernel. Para obtener más información sobre cómo liberar memoria manualmente, consulte el Buffers Sample.

Serialización predeterminada para clases

Las clases solo se pueden gestionar mediante COM interop y siempre se gestionarán como interfaces. En algunos casos, la interfaz utilizada para manejar la clase se conoce como la interfaz de clase. Para obtener información sobre cómo invalidar la interfaz de clase con una interfaz de su elección, consulte Introducción a la interfaz de clase.

Pasar clases a COM

Cuando una clase administrada se pasa a COM, el serializador de interoperabilidad automáticamente encapsula la clase con un proxy COM y pasa la interfaz de clase generada por el proxy a la llamada de método COM. A continuación, el proxy delega todas las llamadas en la interfaz de clase al objeto administrado. El proxy también expone otras interfaces que la clase no implementa explícitamente. El proxy implementa automáticamente interfaces como IUnknown e IDispatch en nombre de la clase.

Pasar clases a código de .NET

Las coclases no suelen usarse como argumentos de método en COM. En su lugar, normalmente se utiliza una interfaz predeterminada en sustitución de la coclase.

Cuando una interfaz se pasa a código administrado, el serializador de interoperabilidad es responsable de encapsular la interfaz en el contenedor adecuado y pasar este contenedor al método administrado. Determinar qué contenedor usar puede ser difícil. Cada instancia de un objeto COM tiene un único contenedor único, independientemente de cuántas interfaces implemente el objeto. Por ejemplo, un único objeto COM que implementa cinco interfaces distintas tiene solo un contenedor. El mismo envoltorio expone todas las cinco interfaces. Si se crean dos instancias del objeto COM, entonces se crean dos instancias del envoltorio.

Para que el contenedor mantenga el mismo tipo a lo largo del tiempo, el serializador de interoperabilidad debe identificar el contenedor correcto la primera vez que una interfaz expuesta por el objeto se pasa a través del serializador. El organizador identifica el objeto al mirar una de las interfaces que implementa el objeto.

Por ejemplo, el serializador determina que el contenedor de clase se debe usar para encapsular la interfaz que se pasó a código administrado. Cuando la interfaz se pasa por primera vez a través del serializador, el serializador comprueba si la interfaz procede de un objeto conocido. Esta comprobación se produce en dos situaciones:

  • Otro objeto administrado que se ha pasado a COM en otro lugar está implementando una interfaz. El marcador puede identificar fácilmente las interfaces expuestas por los objetos administrados y es capaz de asociar la interfaz al objeto administrado que proporciona la implementación. A continuación, el objeto administrado se pasa al método y no se necesita ningún contenedor.

  • Un objeto que ya se ha envuelto está implementando la interfaz. Para determinar si este es el caso, el serializador consulta el objeto para su interfaz IUnknown y compara la interfaz devuelta con las interfaces de otros objetos que ya están encapsulados. Si la interfaz es la misma que la de otro contenedor, los objetos tienen la misma identidad y el contenedor existente se pasa al método .

Si una interfaz no procede de un objeto conocido, el marshalizador hace lo siguiente:

  1. El serializador consulta al objeto la interfaz IProvideClassInfo2. Si se proporciona, el marshalizador usa el CLSID devuelto por IProvideClassInfo2.GetGUID para identificar la coclase que proporciona la interfaz. Con el CLSID, el serializador puedes ubicar el contenedor en el Registro si previamente se ha registrado el ensamblado.

  2. El serializador consulta a la interfaz la interfaz IProvideClassInfo. Si se proporciona, el marshalizador usa ITypeInfo devuelto por IProvideClassInfo.GetClassInfo para determinar el CLSID de la clase que expone la interfaz. El serializador puede usar el CLSID para buscar los metadatos del contenedor.

  3. Si el serializador sigue sin poder identificar la clase, encapsula la interfaz en una clase de contenedor genérica denominada System.__ComObject.

Serialización predeterminada para delegados

Un delegado administrado se serializa como una interfaz COM o como un puntero de función según el mecanismo de llamada:

  • Para invocación de plataforma, se serializa un delegado como un puntero de función no administrada de forma predeterminada.

  • Para interoperabilidad COM, se serializa un delegado como una interfaz COM de tipo _Delegate de forma predeterminada. La interfaz _Delegate se define en la biblioteca de tipos Mscorlib.tlb y contiene el Delegate.DynamicInvoke método , que permite llamar al método al que hace referencia el delegado.

En la siguiente tabla se muestran las opciones de serialización para el tipo de datos de delegado administrado. El atributo MarshalAsAttribute proporciona varios valores de enumeración UnmanagedType para serializar los delegados.

Tipo de enumeración Descripción del formato no administrado
UnmanagedType.FunctionPtr Es un puntero de función no administrada.
UnmanagedType.Interface Interfaz de tipo _Delegate, tal como se define en Mscorlib.tlb.

Considere el siguiente código de ejemplo en el que los métodos de DelegateTestInterface se exportan a una biblioteca de tipos COM. Tenga en cuenta que solo los delegados marcados con la palabra clave ref (o ByRef) se pasan como parámetros de entrada/salida.

using System;  
using System.Runtime.InteropServices;  
  
public interface DelegateTest {  
void m1(Delegate d);  
void m2([MarshalAs(UnmanagedType.Interface)] Delegate d);
void m3([MarshalAs(UnmanagedType.Interface)] ref Delegate d);
void m4([MarshalAs(UnmanagedType.FunctionPtr)] Delegate d);
void m5([MarshalAs(UnmanagedType.FunctionPtr)] ref Delegate d);
}  

Representación de la biblioteca de tipos

importlib("mscorlib.tlb");  
interface DelegateTest : IDispatch {  
[id(…)] HRESULT m1([in] _Delegate* d);  
[id(…)] HRESULT m2([in] _Delegate* d);  
[id(…)] HRESULT m3([in, out] _Delegate** d);  
[id()] HRESULT m4([in] int d);  
[id()] HRESULT m5([in, out] int *d);  
   };  

Se puede desreferenciar un puntero de función, al igual que cualquier otro puntero de función no administrado se puede desreferenciar.

En este ejemplo, al serializar los dos delegados como UnmanagedType.FunctionPtr, el resultado es un elemento int y un puntero a un elemento int. Como los tipos de delegado se serializan, aquí int representa un puntero a un valor void (void*), que es la dirección del delegado en la memoria. En otras palabras, este resultado es específico de los sistemas Windows de 32 bits, ya que int aquí representa el tamaño del puntero de función.

Nota:

Una referencia al puntero de función para un delegado administrado mantenido por código no administrado no impide que Common Language Runtime realice la recolección de elementos no utilizados en el objeto administrado.

Por ejemplo, el código siguiente es incorrecto porque la referencia al cb objeto, que se pasa al SetChangeHandler método, no mantiene cb vivo más allá de la duración del Test método. Una vez recopilados los elementos no utilizados del objeto cb, el puntero de función pasado a SetChangeHandler deja de ser válido.

public class ExternalAPI {  
   [DllImport("External.dll")]  
   public static extern void SetChangeHandler(  
      [MarshalAs(UnmanagedType.FunctionPtr)]ChangeDelegate d);  
}  
public delegate bool ChangeDelegate([MarshalAs(UnmanagedType.LPWStr) string S);  
public class CallBackClass {  
   public bool OnChange(string S){ return true;}  
}  
internal class DelegateTest {  
   public static void Test() {  
      CallBackClass cb = new CallBackClass();  
      // Caution: The following reference on the cb object does not keep the
      // object from being garbage collected after the Main method
      // executes.  
      ExternalAPI.SetChangeHandler(new ChangeDelegate(cb.OnChange));
   }  
}  

Para compensar la recolección inesperada de elementos no utilizados, el llamador debe asegurarse de que el objeto cb se mantiene activo mientras el puntero de función no administrada está en uso. Opcionalmente, puede hacer que el código no administrado notifique al código administrado cuando el puntero de función ya no sea necesario, como se muestra en el ejemplo siguiente.

internal class DelegateTest {  
   CallBackClass cb;  
   // Called before ever using the callback function.  
   public static void SetChangeHandler() {  
      cb = new CallBackClass();  
      ExternalAPI.SetChangeHandler(new ChangeDelegate(cb.OnChange));  
   }  
   // Called after using the callback function for the last time.  
   public static void RemoveChangeHandler() {  
      // The cb object can be collected now. The unmanaged code is
      // finished with the callback function.  
      cb = null;  
   }  
}  

Serialización predeterminada para tipos de valor

La mayoría de los tipos de valor, como enteros y números de punto flotante, pueden transferirse en bloque de bits y no requieren serialización. Otros tipos que no pueden transferirse en bloque de bits tienen representaciones distintas en memoria administrada y no administrada, y requieren serialización. Otros tipos requieren formato explícito a través del límite de interoperación.

En esta sección se proporciona información sobre los siguientes tipos de valor con formato:

Además de describir tipos con formato, en este tema se identifican tipos de valor System que tienen un comportamiento de serialización poco habitual.

Un tipo con formato es un tipo complejo que contiene información que controla explícitamente la distribución de sus miembros en la memoria. La información de distribución de miembros se proporciona mediante el atributo StructLayoutAttribute. La distribución puede ser uno de los siguientes valores de enumeración LayoutKind:

  • LayoutKind.Auto

    Indica que Common Language Runtime puede volver a ordenar los miembros del tipo para lograr una mayor eficacia. Sin embargo, cuando un tipo de valor se pasa a código no administrado, la distribución de los miembros es predecible. Si se intenta serializar automáticamente una estructura de este tipo, se produce una excepción.

  • LayoutKind.Sequential

    Indica que los miembros del tipo deben estar dispuestos en memoria no administrada en el mismo orden en el que aparecen en la definición de tipo administrado.

  • LayoutKind.Explicit

    Indica que los miembros están dispuestos según el FieldOffsetAttribute proporcionado con cada campo.

Tipos de valor usados en la invocación de plataforma

En el ejemplo siguiente, los tipos Point y Rect proporcionan información sobre el diseño de miembros mediante StructLayoutAttribute.

Imports System.Runtime.InteropServices  
<StructLayout(LayoutKind.Sequential)> Public Structure Point  
   Public x As Integer  
   Public y As Integer  
End Structure  
<StructLayout(LayoutKind.Explicit)> Public Structure Rect  
   <FieldOffset(0)> Public left As Integer  
   <FieldOffset(4)> Public top As Integer  
   <FieldOffset(8)> Public right As Integer  
   <FieldOffset(12)> Public bottom As Integer  
End Structure  
using System.Runtime.InteropServices;  
[StructLayout(LayoutKind.Sequential)]  
public struct Point {  
   public int x;  
   public int y;  
}
  
[StructLayout(LayoutKind.Explicit)]  
public struct Rect {  
   [FieldOffset(0)] public int left;  
   [FieldOffset(4)] public int top;  
   [FieldOffset(8)] public int right;  
   [FieldOffset(12)] public int bottom;  
}  

Al serializar a código no administrado, estos tipos con formato se serializan como estructuras de estilo C. Esto proporciona una manera sencilla de llamar a una API no administrada que tenga argumentos de estructura. Por ejemplo, POINT y RECT estructuras se pueden pasar a la función PtInRect de la API de Microsoft Windows de la siguiente manera:

BOOL PtInRect(const RECT *lprc, POINT pt);  

Las estructuras se pueden pasar mediante la siguiente definición de invocación de plataforma:

Friend Class NativeMethods
    Friend Declare Auto Function PtInRect Lib "User32.dll" (
        ByRef r As Rect, p As Point) As Boolean
End Class
internal static class NativeMethods
{
   [DllImport("User32.dll")]
   internal static extern bool PtInRect(ref Rect r, Point p);
}

El tipo de valor Rect se debe pasar por referencia porque la API no administrada espera que un puntero a un RECT se pase a la función. El tipo de valor Point se pasa por valor porque la API no administrada espera que POINT se pase en la pila. Esta sutil diferencia es muy importante. Las referencias se pasan a código no administrado como punteros. Los valores se pasan a código no administrado en la pila.

Nota:

Cuando se gestiona un tipo formateado como una estructura, solo se puede acceder a los campos del tipo. Si el tipo tiene métodos, propiedades o eventos, no se puede acceder al código no administrado.

Las clases también se pueden transformar en código no administrado como estructuras de estilo C, siempre que tengan un diseño de miembros fijo. La información sobre el diseño de los miembros de una clase también se proporciona con el atributo StructLayoutAttribute. La principal diferencia entre los tipos de valor con diseño fijo y las clases con diseño fijo es la forma en que se convierten a código no administrado. Los tipos de valor se pasan por valor (en la pila) y, por consiguiente, el llamador no ve los cambios realizados por el destinatario en los miembros del tipo. Los tipos de referencia se pasan por referencia (se pasa una referencia al tipo en la pila); en consecuencia, el llamador ve todos los cambios realizados por el destinatario en miembros de un tipo que pueden transferirse en bloque de bits.

Nota:

Si un tipo de referencia tiene miembros de tipos que no pueden transferirse en bloque de bits, se requiere una conversión doble: la primera vez cuando se pasa un argumento al lado no administrado y la segunda vez en la devolución de la llamada. Debido a esta sobrecarga adicional, los parámetros In/Out deben aplicarse explícitamente a un argumento si el llamador desea ver los cambios realizados por el destinatario.

En el ejemplo siguiente, la SystemTime clase tiene un diseño de miembro secuencial y se puede pasar a la función GetSystemTime de la API de Windows.

<StructLayout(LayoutKind.Sequential)> Public Class SystemTime  
   Public wYear As System.UInt16  
   Public wMonth As System.UInt16  
   Public wDayOfWeek As System.UInt16  
   Public wDay As System.UInt16  
   Public wHour As System.UInt16  
   Public wMinute As System.UInt16  
   Public wSecond As System.UInt16  
   Public wMilliseconds As System.UInt16  
End Class  
[StructLayout(LayoutKind.Sequential)]  
   public class SystemTime {  
   public ushort wYear;
   public ushort wMonth;  
   public ushort wDayOfWeek;
   public ushort wDay;
   public ushort wHour;
   public ushort wMinute;
   public ushort wSecond;
   public ushort wMilliseconds;
}  

La función GetSystemTime se define de la manera siguiente:

void GetSystemTime(SYSTEMTIME* SystemTime);  

La definición de invocación de plataforma equivalente para GetSystemTime es la siguiente:

Friend Class NativeMethods
    Friend Declare Auto Sub GetSystemTime Lib "Kernel32.dll" (
        ByVal sysTime As SystemTime)
End Class
internal static class NativeMethods
{
   [DllImport("Kernel32.dll", CharSet = CharSet.Auto)]
   internal static extern void GetSystemTime(SystemTime st);
}

Observe que el SystemTime argumento no está escrito como argumento de referencia porque SystemTime es una clase, no un tipo de valor. A diferencia de los tipos de valor, las clases siempre se pasan por referencia.

En el ejemplo de código siguiente se muestra una clase diferente Point que tiene un método denominado SetXY. Dado que el tipo tiene un diseño secuencial, se puede pasar al código no gestionado y tratarse como una estructura. Sin embargo, el miembro SetXY no es invocable desde código no administrado, aunque el objeto se pase por referencia.

<StructLayout(LayoutKind.Sequential)> Public Class Point  
   Private x, y As Integer  
   Public Sub SetXY(x As Integer, y As Integer)  
      Me.x = x  
      Me.y = y  
   End Sub  
End Class  
[StructLayout(LayoutKind.Sequential)]  
public class Point {  
   int x, y;  
   public void SetXY(int x, int y){
      this.x = x;  
      this.y = y;  
   }  
}  

Tipos de valor usados en la interoperabilidad COM

Los tipos con formato también pueden pasarse a llamadas de métodos de interoperabilidad COM. De hecho, cuando se exporta a una biblioteca de tipos, los tipos de valor se convierten automáticamente en estructuras. Como se muestra en el ejemplo siguiente, el Point tipo de valor se convierte en una definición de tipo (typedef) con el nombre Point. Todas las referencias al Point tipo de valor en otra parte de la biblioteca de tipos se reemplazan por la Point definición de tipo.

Representación de la biblioteca de tipos

typedef struct tagPoint {  
   int x;  
   int y;  
} Point;  
interface _Graphics {  
   …  
   HRESULT SetPoint ([in] Point p)  
   HRESULT SetPointRef ([in,out] Point *p)  
   HRESULT GetPoint ([out,retval] Point *p)  
}  

Las mismas reglas usadas para serializar valores y referencias para llamadas de invocación de plataforma se usan al serializar a través de interfaces COM. Por ejemplo, cuando una instancia del tipo de valor Point se pasa de .NET Framework a COM, Point se pasa por valor. Si el tipo de valor Point se pasa por referencia, un puntero a un Point se pasa en la pila. El serializador de interoperabilidad no admite niveles superiores de direccionamiento indirecto (Punto) en ninguna dirección.

Nota:

Las estructuras que tienen el LayoutKind valor de enumeración establecido en Explicit no se pueden usar en la interoperabilidad COM porque la biblioteca de tipos exportada no puede expresar un diseño explícito.

Tipos de valor del sistema

El System espacio de nombres tiene varios tipos de valor que representan la forma encapsulada de los tipos primitivos en tiempo de ejecución. Por ejemplo, la estructura tipo de valor System.Int32 representa la forma empaquetada de ELEMENT_TYPE_I4. En lugar de serializar estos tipos como estructuras, como otros tipos con formato, se serializan de la misma forma que los tipos primitivos a los que aplican conversión boxing. Por lo tanto, System.Int32 se gestionará como ELEMENT_TYPE_I4 en lugar de como una estructura que contenga un único miembro de tipo long. La siguiente tabla contiene una lista de los tipos de valores en el espacio de nombres System que son representaciones encapsuladas de tipos primitivos.

Tipo de valor del sistema Tipo de elemento
System.Boolean ELEMENT_TYPE_BOOLEAN
System.SByte ELEMENT_TYPE_I1
System.Byte ELEMENT_TYPE_UI1
System.Char ELEMENT_TYPE_CHAR
System.Int16 ELEMENT_TYPE_I2
System.UInt16 ELEMENT_TYPE_U2
System.Int32 ELEMENT_TYPE_I4
System.UInt32 ELEMENT_TYPE_U4
System.Int64 ELEMENT_TYPE_I8
System.UInt64 ELEMENT_TYPE_U8
System.Single ELEMENT_TYPE_R4
System.Double ELEMENT_TYPE_R8
System.String ELEMENT_TYPE_STRING
System.IntPtr ELEMENT_TYPE_I
System.UIntPtr ELEMENT_TYPE_U

Otros tipos de valor del espacio de nombres System se controlan de forma diferente. Dado que el código no administrado ya tiene formatos establecidos para estos tipos, la serialización tiene reglas especiales para serializarlos. En la tabla siguiente se enumeran los tipos de valor especiales del espacio de nombres System, así como el tipo no administrado al que se serializan.

Tipo de valor del sistema Tipo IDL
System.DateTime FECHA
System.Decimal decimal
System.Guid GUID
System.Drawing.Color OLE_COLOR

El código siguiente muestra la definición de los tipos no administrados DATE, GUID, DECIMAL y OLE_COLOR en la biblioteca de tipos Stdole2.

Representación de la biblioteca de tipos

typedef double DATE;  
typedef DWORD OLE_COLOR;  
  
typedef struct tagDEC {  
    USHORT    wReserved;  
    BYTE      scale;  
    BYTE      sign;  
    ULONG     Hi32;  
    ULONGLONG Lo64;  
} DECIMAL;  
  
typedef struct tagGUID {  
    DWORD Data1;  
    WORD  Data2;  
    WORD  Data3;  
    BYTE  Data4[ 8 ];  
} GUID;  

El código siguiente muestra las definiciones correspondientes en la interfaz administrada IValueTypes .

Public Interface IValueTypes  
   Sub M1(d As System.DateTime)  
   Sub M2(d As System.Guid)  
   Sub M3(d As System.Decimal)  
   Sub M4(d As System.Drawing.Color)  
End Interface  
public interface IValueTypes {  
   void M1(System.DateTime d);  
   void M2(System.Guid d);  
   void M3(System.Decimal d);  
   void M4(System.Drawing.Color d);  
}  

Representación de la biblioteca de tipos

[…]  
interface IValueTypes : IDispatch {  
   HRESULT M1([in] DATE d);  
   HRESULT M2([in] GUID d);  
   HRESULT M3([in] DECIMAL d);  
   HRESULT M4([in] OLE_COLOR d);  
};  

Consulte también