Procedimientos recomendados de interoperabilidad nativa
.NET ofrece diversas formas de personalizar el código de interoperabilidad nativa. En este artículo se incluye la guía que siguen los equipos de .NET de Microsoft para realizar la interoperabilidad nativa.
Orientación general
La guía de esta sección se aplica a todos los escenarios de interoperabilidad.
- ✔️ USE
[LibraryImport]
, si fuera posible, si tiene como destino .NET 7+.- Hay casos en los que el uso de
[DllImport]
resulta adecuado. Un analizador de código con id. SYSLIB1054 indica cuándo es el caso.
- Hay casos en los que el uso de
- ✔️ USE la misma nomenclatura y uso de mayúsculas y minúsculas para los métodos y parámetros que el método nativo al que quiere llamar.
- ✔️ PLANTÉESE usar la misma nomenclatura y uso de mayúsculas y minúsculas para los valores constantes.
- ✔️ USE los tipos de .NET que se asignen lo más cerca posible del tipo nativo. Por ejemplo, en C#, utilice
uint
cuando el tipo nativo esunsigned int
. - ✔️ SÍ prefiera expresar tipos nativos de nivel superior mediante structs de .NET en lugar de clases.
- ✔️ USE punteros de función en lugar de tipos
Delegate
al pasar devoluciones de llamada a funciones no administradas en C#. - ✔️
[In]
Use y[Out]
atributos en los parámetros de matriz. - ✔️ USE solamente atributos
[In]
y[Out]
cuando quiera que el comportamiento sea diferente al predeterminado. - ✔️ PLANTÉESE usar System.Buffers.ArrayPool<T> para agrupar los búferes de matriz nativa.
- ✔️ PLANTÉESE encapsular las declaraciones P/Invoke en una clase con el mismo nombre y uso de mayúsculas y minúsculas que la biblioteca nativa.
- De esta forma, los atributos
[LibraryImport]
o[DllImport]
usarán la característica de lenguajenameof
de C# para pasar el nombre de la biblioteca nativa y asegurarse de no escribirla mal.
- De esta forma, los atributos
- ✔️ Use identificadores
SafeHandle
para administrar la duración de los objetos que encapsulan recursos no administrados. Para obtener más información, vea Limpieza de recursos no administrados. - ❌ EVITE los finalizadores para administrar la duración de los objetos que encapsulan recursos no administrados. Para obtener más información, vea Implementación de un método Dispose.
Configuración del atributo LibraryImport
Un analizador de código con id. SYSLIB1054 le guía con LibraryImportAttribute
. En la mayoría de los casos, el uso de LibraryImportAttribute
requiere una declaración explícita en lugar de confiar en la configuración predeterminada. Este diseño es intencionado y ayuda a evitar el comportamiento no deseado en escenarios de interoperabilidad.
Configuración del atributo DllImport
Parámetro | Default | Recomendación | Detalles |
---|---|---|---|
PreserveSig | true |
Mantenga el valor predeterminado | Cuando se establece explícitamente en false, los valores devueltos de HRESULT con errores se convierten en excepciones (y el valor devuelto en la definición se convierte en NULL). |
SetLastError | false |
Depende de la API | Establezca este valor en true si la API utiliza GetLastError y use Marshal.GetLastWin32Error para obtener el valor. Si la API establece una condición que indica que tiene un error, obtenga el error antes de realizar otras llamadas para evitar que accidentalmente se sobrescriba. |
CharSet | Definido por el compilador (especificado en la documentación de juegos de caracteres) | Usar explícitamente CharSet.Unicode o CharSet.Ansi cuando haya cadenas o caracteres en la definición |
Esto especifica el comportamiento de serialización de cadenas y lo que hace ExactSpelling cuando false . Tenga en cuenta que CharSet.Ansi es realmente UTF8 en Unix. La mayoría de las veces Windows utiliza Unicode, mientras que Unix usa UTF8. Obtenga más información en la documentación de juegos de caracteres. |
ExactSpelling | false |
true |
Establezca este valor en true y obtenga una ventaja de rendimiento ligero, ya que el entorno de ejecución no busca nombres de función alternativos con un sufijo "A" o "W" en función del valor de la configuración de CharSet ("A" para CharSet.Ansi y "W" para CharSet.Unicode ). |
Parámetros de cadena
string
se ancla y usa directamente por código nativo (en lugar de copiado) cuando se pasa por valor (no ref
ni out
) y cualquiera de los siguientes:
- LibraryImportAttribute.StringMarshalling se define como Utf16.
- El argumento se marca explícitamente como
[MarshalAs(UnmanagedType.LPWSTR)]
. - DllImportAttribute.CharSet es Unicode.
❌ NO use parámetros [Out] string
. Los parámetros de cadena pasados por valor con el atributo [Out]
pueden llegar a desestabilizar el entorno de ejecución si la cadena es una cadena internalizada. Obtenga más información sobre el internamiento de cadenas en la documentación de String.Intern.
✔️ CONSIDERE las matrices char[]
o byte[]
de ArrayPool
cuando se espera que el código nativo rellene un búfer de caracteres. Esto requiere pasar el argumento como [Out]
.
Guía específica de DllImport
✔️ CONSIDERE establecer la propiedad CharSet
en [DllImport]
para que el tiempo de ejecución conozca la codificación de cadena esperada.
✔️ CONSIDERE evitar los parámetros StringBuilder
. La serialización StringBuilder
siempre crea una copia del búfer nativo. Por lo tanto, puede ser extremadamente ineficaz. Siga el escenario típico de una llamada a una API de Windows que toma una cadena:
- Cree un parámetro
StringBuilder
de la capacidad deseada (asigna la capacidad administrada) {1}. - Invoke:
- Asigna un búfer nativo {2}.
- Copia el contenido si
[In]
(el valor predeterminado de un parámetroStringBuilder
). - Copia el búfer nativo en una matriz administrada recién asignada si
[Out]
{3} (también el valor predeterminado deStringBuilder
).
ToString()
asigna otra matriz {4} administrada.
Es decir, asignaciones {4} para obtener una cadena del código nativo. Lo mejor que puede hacer para limitar esto consiste en reusar StringBuilder
en otra llamada, pero esta todavía solo guarda una asignación. Es mucho mejor usar y copiar en caché un búfer de caracteres desde ArrayPool
. Después, se puede centrar en la asignación de ToString()
en las llamadas siguientes.
El otro problema con StringBuilder
es que siempre copia la copia de seguridad del búfer de retorno en el primer valor NULL. Si la cadena pasada anterior no está terminada o es una cadena terminada en doble NULL, el valor P/Invoke será incorrecto en el mejor de los casos.
Si usaStringBuilder
, un último problema es que la capacidad no incluyen un valor NULL oculto, que siempre se tiene en cuenta en la interoperabilidad. Es habitual equivocarse, ya que la mayoría de las API quieren que el tamaño del búfer incluyan el valor NULL. Esto puede dar lugar a asignaciones innecesarias. Además, este problema impide que el entorno de ejecución optimice la serialización StringBuilder
para minimizar las copias.
Para obtener más información sobre la serialización cadenas, vea Cálculo de referencias predeterminado para cadenas y Personalización de la serialización de campos de cadena.
Específico para Windows Para las cadenas
[Out]
, CLR utilizaráCoTaskMemFree
de forma predeterminada para liberar las cadenas oSysStringFree
para las cadenas que están marcadas comoUnmanagedType.BSTR
. Para la mayoría de las API con un búfer de cadena de salida: El número de caracteres pasados debe incluir el valor NULL. Si el valor devuelto es menor que el número de caracteres pasados, la llamada se realiza correctamente y el valor es el número de caracteres sin el carácter NULL. En caso contrario, el número es el tamaño del búfer necesario incluyendo el carácter NULL.
- Pase 5 y obtenga 4: la cadena tiene 4 caracteres de longitud con un valor NULL final.
- Pase 5 y obtenga 6: la cadena tiene 5 caracteres de longitud y necesita un búfer de 6 caracteres para contener el valor NULL. Tipos de datos de Windows para cadenas
Parámetros y campos booleanos
Los valores booleanos se desordenan fácilmente. De forma predeterminada, .NET bool
se serializa en un valor BOOL
de Windows de 4 bytes. Sin embargo, los tipos _Bool
y bool
en C y C++ tienen un solo byte. De este modo, puede ser complicado realizar un seguimiento de los errores porque la mitad del valor devuelto se descarta, con lo que posiblemente se modifica el resultado. Para obtener más información sobre la serialización de valores bool
de .NET en tipos bool
de C o C++, consulte la documentación sobre cómo personalizar la serialización de campos booleanos.
GUID
Los GUID se pueden usar directamente en las firmas. Muchas de las API de Windows toman los alias de tipo GUID&
como REFIID
. Cuando la firma del método contiene un parámetro de referencia, coloque una palabra clave ref
o un atributo [MarshalAs(UnmanagedType.LPStruct)]
en la declaración del parámetro GUID.
GUID | GUID por referencia |
---|---|
KNOWNFOLDERID |
REFKNOWNFOLDERID |
❌ NO USE [MarshalAs(UnmanagedType.LPStruct)]
para parámetros GUID que no sean ref
.
Tipos que pueden transferirse en bloque de bits
Los tipos que pueden transferirse en bloque de bits son tipos que tienen la misma representación de nivel de bits en código administrado y nativo. Por lo tanto, no es necesario convertirlos a otro formato para serializarlos con código nativo como punto de partida o destino, y como se mejora el rendimiento, deben preferirse estos tipos. Algunos tipos no pueden transferirse en bloque de bits, pero se sabe que contienen contenido que sí. Estos tipos tienen optimizaciones similares como tipos que pueden transferirse en bloque de bits cuando no están incluidos en otro tipo, pero no se consideran que se puedan transferir en bloque de bits cuando se encuentran en campos de estructuras o con fines de UnmanagedCallersOnlyAttribute
.
Tipos que pueden transferirse en bloque de bits al habilitar la serialización en runtime
Tipos que pueden transferirse en bloque de bits:
byte
,sbyte
,short
,ushort
,int
,uint
,long
,ulong
,single
,double
- Estructuras con distribución fija que solo tienen tipos de valor que pueden transferirse en bloque de bits, por ejemplo, campos
- La distribución fija requiere
[StructLayout(LayoutKind.Sequential)]
o[StructLayout(LayoutKind.Explicit)]
. - De forma predeterminada, las estructuras son
LayoutKind.Sequential
- La distribución fija requiere
Tipos con contenido que puede transferirse en bloque de bits:
- Matrices unidimensionales no anidadas de tipos primitivos que pueden transferirse en bloque de bits (por ejemplo,
int[]
) - Clases con distribución fija que solo tienen tipos de valor que pueden transferirse en bloque de bits, por ejemplo, campos
- La distribución fija requiere
[StructLayout(LayoutKind.Sequential)]
o[StructLayout(LayoutKind.Explicit)]
. - De forma predeterminada, las clases son
LayoutKind.Auto
- La distribución fija requiere
Tipos que no se pueden transferir en bloque de bits:
bool
Tipos que A VECES se pueden transferir en bloque de bits:
char
Tipos con contenido que A VECES puede transferirse en bloque de bits:
string
Cuando los tipos que pueden transferirse en bloque de bits se pasan por referencia con in
, ref
o out
, o cuando los tipos con contenido que puede transferirse en bloque de bits se pasan por valor, simplemente se anclan mediante el serializador en lugar de copiarse en un búfer intermedio.
char
puede transferirse en bloque de bits en una matriz unidimensional o si forma parte de un tipo que lo contiene, se marca explícitamente con [StructLayout]
con CharSet = CharSet.Unicode
.
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct UnicodeCharStruct
{
public char c;
}
string
contiene contenido que puede transferirse en bloque de bits si no está contenido en otro tipo y se pasa por valor (no ref
ni out
) como argumento y cualquiera de los siguientes:
- StringMarshalling se define como Utf16.
- El argumento se marca explícitamente como
[MarshalAs(UnmanagedType.LPWSTR)]
. - CharSet es Unicode.
Puede ver si un tipo puede transferirse en bloque de bits o contiene contenido que puede transferirse en bloque de bits al intentar crear un controlador GCHandle
anclado. Si el tipo no es una cadena o no puede transferirse en bloque de bits, GCHandle.Alloc
producirá una excepción ArgumentException
.
Tipos que pueden transferirse en bloque de bits cuando la serialización en runtime está deshabilitada
Cuando la serialización en tiempo de ejecución está deshabilitada, las reglas que determinan qué tipos pueden transferirse en bloque de bits son significativamente más simples. Todos los tipos que son tipos de C# unmanaged
y no tienen ningún campo marcado con [StructLayout(LayoutKind.Auto)]
pueden transferirse en bloque de bits. Todos los tipos que no son tipos de C# unmanaged
no pueden transferirse en bloque de bits. El concepto de tipos con contenido que puede transferirse en bloque de bits, como matrices o cadenas, no se aplica cuando la serialización en runtime está deshabilitada. Cualquier tipo que no se considere que puede transferirse en bloque de bits por la regla mencionada anteriormente no se admite cuando la serialización en runtime está deshabilitada.
Estas reglas difieren del sistema integrado principalmente en situaciones en las que se usan bool
y char
. Cuando la serialización está deshabilitada, bool
se pasa como un valor de 1 byte y no se normaliza, y char
siempre se pasa como un valor de 2 bytes. Cuando la serialización en runtime está habilitada, bool
puede asignarse a un valor de 1, 2 o 4 bytes y siempre se normaliza y char
se asigna a un valor de 1 o 2 bytes en función de CharSet
.
✔️ CREE estructuras que puedan transferirse en bloque de bits cuando sea posible.
Para obtener más información, consulte:
Forma de mantener activos los objetos administrados
GC.KeepAlive()
asegurará que un objeto permanezca dentro del ámbito hasta que se alcance el método KeepAlive.
HandleRef
permite que el serializador mantenga un objeto activo para la duración de P/Invoke. Se puede usar en lugar de IntPtr
en firmas de método. SafeHandle
reemplaza esta clase de forma eficaz y se debe usar en su lugar.
GCHandle
permite anclar un objeto administrado y obtener el puntero nativo. El patrón básico es el siguiente:
GCHandle handle = GCHandle.Alloc(obj, GCHandleType.Pinned);
IntPtr ptr = handle.AddrOfPinnedObject();
handle.Free();
Anclar no es la acción predeterminada para GCHandle
. El otro patrón principal se usa para pasar una referencia a un objeto administrado a través de código nativo y devolverse al código administrado, normalmente con una devolución de llamada. Este es el patrón:
GCHandle handle = GCHandle.Alloc(obj);
SomeNativeEnumerator(callbackDelegate, GCHandle.ToIntPtr(handle));
// In the callback
GCHandle handle = GCHandle.FromIntPtr(param);
object managedObject = handle.Target;
// After the last callback
handle.Free();
No olvide que GCHandle
debe liberarse explícitamente para evitar fugas de memoria.
Tipos de datos de Windows comunes
Esta es una lista de los tipos de datos que se usan frecuentemente en las API de Windows y los tipos de C# que se deben usar al llamar al código de Windows.
Los siguientes tipos tienen el mismo tamaño en Windows 32 bits y 64 bits, a pesar de sus nombres.
Ancho | Windows | C# | Alternativa |
---|---|---|---|
32 | BOOL |
int |
bool |
8 | BOOLEAN |
byte |
[MarshalAs(UnmanagedType.U1)] bool |
8 | BYTE |
byte |
|
8 | UCHAR |
byte |
|
8 | UINT8 |
byte |
|
8 | CCHAR |
byte |
|
8 | CHAR |
sbyte |
|
8 | CHAR |
sbyte |
|
8 | INT8 |
sbyte |
|
16 | CSHORT |
short |
|
16 | INT16 |
short |
|
16 | SHORT |
short |
|
16 | ATOM |
ushort |
|
16 | UINT16 |
ushort |
|
16 | USHORT |
ushort |
|
16 | WORD |
ushort |
|
32 | INT |
int |
|
32 | INT32 |
int |
|
32 | LONG |
int |
Consulte CLong y CULong . |
32 | LONG32 |
int |
|
32 | CLONG |
uint |
Consulte CLong y CULong . |
32 | DWORD |
uint |
Consulte CLong y CULong . |
32 | DWORD32 |
uint |
|
32 | UINT |
uint |
|
32 | UINT32 |
uint |
|
32 | ULONG |
uint |
Consulte CLong y CULong . |
32 | ULONG32 |
uint |
|
64 | INT64 |
long |
|
64 | LARGE_INTEGER |
long |
|
64 | LONG64 |
long |
|
64 | LONGLONG |
long |
|
64 | QWORD |
long |
|
64 | DWORD64 |
ulong |
|
64 | UINT64 |
ulong |
|
64 | ULONG64 |
ulong |
|
64 | ULONGLONG |
ulong |
|
64 | ULARGE_INTEGER |
ulong |
|
32 | HRESULT |
int |
|
32 | NTSTATUS |
int |
Los siguientes tipos, que son punteros, siguen el ancho de la plataforma. Use IntPtr
/UIntPtr
para estos.
Tipos de puntero con signo (use IntPtr ) |
Tipos de puntero sin signo (use UIntPtr ) |
---|---|
HANDLE |
WPARAM |
HWND |
UINT_PTR |
HINSTANCE |
ULONG_PTR |
LPARAM |
SIZE_T |
LRESULT |
|
LONG_PTR |
|
INT_PTR |
Un tipo PVOID
de Windows, que es un tipo void*
de C, se puede serializar como IntPtr
o UIntPtr
, pero prefiere void*
cuando sea posible.
Tipos admitidos integrados anteriormente
Hay instancias poco frecuentes cuando se quita la compatibilidad integrada con un tipo.
La compatibilidad serializada e integrada de UnmanagedType.HString
y UnmanagedType.IInspectable
se quitó en la versión .NET 5. Debe volver a compilar archivos binarios que usen este tipo de serialización y que tengan como destino un marco anterior. Todavía es posible serializar este tipo, pero debe serializarlo de forma manual, como se muestra en el ejemplo de código siguiente. Este código funcionará al avanzar y también es compatible con marcos anteriores.
public sealed class HStringMarshaler : ICustomMarshaler
{
public static readonly HStringMarshaler Instance = new HStringMarshaler();
public static ICustomMarshaler GetInstance(string _) => Instance;
public void CleanUpManagedData(object ManagedObj) { }
public void CleanUpNativeData(IntPtr pNativeData)
{
if (pNativeData != IntPtr.Zero)
{
Marshal.ThrowExceptionForHR(WindowsDeleteString(pNativeData));
}
}
public int GetNativeDataSize() => -1;
public IntPtr MarshalManagedToNative(object ManagedObj)
{
if (ManagedObj is null)
return IntPtr.Zero;
var str = (string)ManagedObj;
Marshal.ThrowExceptionForHR(WindowsCreateString(str, str.Length, out var ptr));
return ptr;
}
public object MarshalNativeToManaged(IntPtr pNativeData)
{
if (pNativeData == IntPtr.Zero)
return null;
var ptr = WindowsGetStringRawBuffer(pNativeData, out var length);
if (ptr == IntPtr.Zero)
return null;
if (length == 0)
return string.Empty;
return Marshal.PtrToStringUni(ptr, length);
}
[DllImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static extern int WindowsCreateString([MarshalAs(UnmanagedType.LPWStr)] string sourceString, int length, out IntPtr hstring);
[DllImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static extern int WindowsDeleteString(IntPtr hstring);
[DllImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static extern IntPtr WindowsGetStringRawBuffer(IntPtr hstring, out int length);
}
// Example usage:
[DllImport("api-ms-win-core-winrt-l1-1-0.dll", PreserveSig = true)]
internal static extern int RoGetActivationFactory(
/*[MarshalAs(UnmanagedType.HString)]*/[MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(HStringMarshaler))] string activatableClassId,
[In] ref Guid iid,
[Out, MarshalAs(UnmanagedType.IUnknown)] out object factory);
Consideraciones sobre el tipo de datos multiplataforma
Hay tipos en el lenguaje C/C++ que tienen latitud en cómo se definen. Al escribir interoperabilidad multiplataforma, pueden surgir casos en los que las plataformas difieren y pueden causar problemas si no se tienen en cuenta.
C/C++ long
El elemento long
de C/C++ y el elemento long
de C# no tienen necesariamente el mismo tamaño.
El tipo long
en C/C++ se define para tener "al menos 32" bits. Esto significa que hay un número mínimo de bits necesarios, pero las plataformas pueden optar por usar más bits si lo desean. En la tabla siguiente se muestran las diferencias en los bits proporcionados para el tipo de datos de C/C++ long
entre plataformas.
Plataforma | 32 bits | 64 bits |
---|---|---|
Windows | 32 | 32 |
macOS/*nix | 32 | 64 |
En cambio, el elemento long
de C# siempre es de 64 bits. Por este motivo, es mejor evitar el uso del elemento long
de C# para interoperabilidad con el long
de C/C++.
(Este problema con el long
de C/C++ no existe para el char
, short
, int
y long long
de C/C++, ya que son 8, 16, 32 y 64 bits respectivamente en todas estas plataformas).
En .NET 6 y versiones posteriores, use los tipos CLong
y CULong
para la interoperabilidad con los tipos de datos C/C++ long
y unsigned long
. El ejemplo siguiente es para CLong
, pero puede usar CULong
para abstraer de unsigned long
de forma similar.
// Cross platform C function
// long Function(long a);
[DllImport("NativeLib")]
extern static CLong Function(CLong a);
// Usage
nint result = Function(new CLong(10)).Value;
Al establecer como destino .NET 5 y versiones anteriores, debe declarar firmas independientes de Windows y que no sean de Windows para controlar el problema.
static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
// Cross platform C function
// long Function(long a);
[DllImport("NativeLib", EntryPoint = "Function")]
extern static int FunctionWindows(int a);
[DllImport("NativeLib", EntryPoint = "Function")]
extern static nint FunctionUnix(nint a);
// Usage
nint result;
if (IsWindows)
{
result = FunctionWindows(10);
}
else
{
result = FunctionUnix(10);
}
Estructuras
Las estructuras administradas se crean en la pila y no se quitan hasta que el método se devuelve. Por definición, se anclan (la recolección de elementos no utilizados no las mueve). Puede simplemente tomar la dirección en bloques de código no seguros si el código nativo no utilizará el puntero más allá del final del método actual.
Las estructuras de bits/bytes son mucho más eficaces, ya que simplemente puede utilizarlas directamente la capa de serialización. Trate de crear estructuras que pueden transferirse en bloque de bits (por ejemplo, evite bool
). Para obtener más información, consulte la sección Tipos que pueden transferirse en bloque de bits.
Si la estructura se puede transferir en bloque de bits, use sizeof()
en lugar de Marshal.SizeOf<MyStruct>()
para mejorar el rendimiento. Como se mencionó anteriormente, puede validar que el tipo se pueda transferir en bloques de bits al intentar crear un controlador GCHandle
anclado. Si el tipo no es una cadena o no puede transferirse en bloque de bits, GCHandle.Alloc
producirá una excepción ArgumentException
.
Los punteros a las estructuras en definiciones deben pasarse por ref
o usar unsafe
y *
.
✔️ HAGA COINCIDIR la estructura administrada lo máximo posible con la forma y los nombres que se usan en el encabezado o la documentación oficial de la plataforma.
✔️ USE el tipo sizeof()
de C# en lugar de Marshal.SizeOf<MyStruct>()
para las estructuras que pueden transferirse en bloque de bits con el fin de mejorar el rendimiento.
❌ EVITE el uso de clases para expresar tipos nativos complejos a través de la herencia.
❌ EVITE el uso de campos System.Delegate
o System.MulticastDelegate
para representar campos de puntero de función en estructuras.
Dado que System.Delegate y System.MulticastDelegate no tienen una firma obligatoria, no garantizan que el delegado que se pasa coincida con la firma que el código nativo espera. Además, en .NET Framework y .NET Core, la serialización de una estructura que contenga System.Delegate
o System.MulticastDelegate
desde su representación nativa a un objeto administrado puede desestabilizar el runtime si el valor del campo en la representación nativa no es un puntero de función que encapsula un delegado administrado. En .NET 5 y versiones posteriores, no se admite la serialización de un campo System.Delegate
o System.MulticastDelegate
de una representación nativa a un objeto administrado. Use un tipo de delegado específico en lugar de System.Delegate
o System.MulticastDelegate
.
Búferes fijos
Una matriz como INT_PTR Reserved1[2]
tiene que serializarse a dos campos IntPtr
, Reserved1a
y Reserved1b
. Cuando la matriz nativa es un tipo primitivo, podemos usar la palabra clave fixed
para escribir de forma más limpia. Por ejemplo, SYSTEM_PROCESS_INFORMATION
tiene este aspecto en el encabezado nativo:
typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
BYTE Reserved1[48];
UNICODE_STRING ImageName;
...
} SYSTEM_PROCESS_INFORMATION
En C#, podemos escribirlo así:
internal unsafe struct SYSTEM_PROCESS_INFORMATION
{
internal uint NextEntryOffset;
internal uint NumberOfThreads;
private fixed byte Reserved1[48];
internal Interop.UNICODE_STRING ImageName;
...
}
Sin embargo, existen los búferes fijos tienen algunas trampas. Los búferes fijos de tipos que no pueden transferirse en bloques de bits no se serializarán correctamente, por lo que la matriz en contexto debe expandirse a varios campos individuales. Además, en .NET Framework y .NET Core antes de la versión 3.0, si una estructura que contiene un campo de búfer fijo se anida dentro de una estructura que no puede transferirse en bloque de bits, el campo de búfer fijo no se serializará correctamente al código nativo.