Invocación de plataforma (P/Invoke)
P/Invoke es una tecnología que permite acceder a estructuras, devoluciones de llamada y funciones de bibliotecas no administradas desde el código administrado. La mayor parte de la API de P/Invoke se encuentra en dos espacios de nombres: System
y System.Runtime.InteropServices
. Mediante estos dos espacios de nombres puede acceder a las herramientas que describen cómo quiere comunicarse con el componente nativo.
Empecemos por el ejemplo más común, es decir, llamar a funciones no administradas en el código administrado. Vamos a mostrar un cuadro de mensaje desde una aplicación de línea de comandos:
using System;
using System.Runtime.InteropServices;
public partial class Program
{
// Import user32.dll (containing the function we need) and define
// the method corresponding to the native function.
[LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
private static partial int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);
public static void Main(string[] args)
{
// Invoke the function as a regular managed method.
MessageBox(IntPtr.Zero, "Command-line message box", "Attention!", 0);
}
}
El ejemplo anterior es simple, pero resalta lo que es necesario para invocar las funciones no administradas desde código administrado. Veamos en detalle el ejemplo:
- En la línea 2 se muestra la directiva
using
para el espacio de nombresSystem.Runtime.InteropServices
, que es el espacio de nombres que contiene todos los elementos que necesitamos. - En la línea 8 se presenta el atributo LibraryImportAttribute. Este atributo indica al tiempo de ejecución que debe cargar el binario no administrado. La cadena pasada es el binario no administrado que contiene la función de destino. Además, especifica la codificación que se va a usar para serializar las cadenas. Por último, especifica que esta función llama a SetLastError y que el runtime debe capturar ese código de error para que el usuario pueda recuperarlo a través de Marshal.GetLastPInvokeError().
- La línea 9 es la esencia del trabajo de P/Invoke. Define un método administrado que tiene exactamente la misma firma que el no administrado. La declaración usa el atributo
LibraryImport
y la palabra clavepartial
para indicar a una extensión del compilador que genere código para llamar a en la biblioteca no administrada.- Dentro del código generado y antes de .NET 7, se usa el
DllImport
. Esta declaración usa la palabra claveextern
para indicar al runtime que se trata de un método externo, y que cuando lo invoque, el runtime debería encontrarlo en el binario no administrado especificado en el atributoDllImport
.
- Dentro del código generado y antes de .NET 7, se usa el
El resto del ejemplo invoca el método como lo haría con cualquier otro método administrado.
El ejemplo es parecido para macOS. El nombre de la biblioteca en el atributo LibraryImport
debe cambiarse, ya que macOS tiene un esquema diferente para la nomenclatura de bibliotecas dinámicas. En el ejemplo siguiente se usa la función getpid(2)
para obtener el identificador de proceso de la aplicación e imprimirlo en la consola:
using System;
using System.Runtime.InteropServices;
namespace PInvokeSamples
{
public static partial class Program
{
// Import the libSystem shared library and define the method
// corresponding to the native function.
[LibraryImport("libSystem.dylib")]
private static partial int getpid();
public static void Main(string[] args)
{
// Invoke the function and get the process ID.
int pid = getpid();
Console.WriteLine(pid);
}
}
}
También es similar en Linux. El nombre de la función es el mismo, ya que getpid(2)
es la llamada del sistema estándar de POSIX.
using System;
using System.Runtime.InteropServices;
namespace PInvokeSamples
{
public static partial class Program
{
// Import the libc shared library and define the method
// corresponding to the native function.
[LibraryImport("libc.so.6")]
private static partial int getpid();
public static void Main(string[] args)
{
// Invoke the function and get the process ID.
int pid = getpid();
Console.WriteLine(pid);
}
}
}
Invocar código administrado desde código no administrado
El entorno de ejecución permite que la comunicación fluya en ambas direcciones, lo que permite llamar a código administrado desde funciones nativas mediante el uso de punteros de función. Lo más parecido a un puntero de función en código administrado es un delegado, por lo que esto es lo que se usa para permitir las devoluciones de llamada de código nativo a código administrado.
La forma en que se usa esta característica se parece al proceso de administrado a nativo que se ha descrito anteriormente. En el caso de una devolución de llamada específica, debe definir un delegado que coincida con la firma y pasarlo al método externo. El tiempo de ejecución se encargará de todo lo demás.
using System;
using System.Runtime.InteropServices;
namespace ConsoleApplication1
{
public static partial class Program
{
// Define a delegate that corresponds to the unmanaged function.
private delegate bool EnumWC(IntPtr hwnd, IntPtr lParam);
// Import user32.dll (containing the function we need) and define
// the method corresponding to the native function.
[LibraryImport("user32.dll")]
private static partial int EnumWindows(EnumWC lpEnumFunc, IntPtr lParam);
// Define the implementation of the delegate; here, we simply output the window handle.
private static bool OutputWindow(IntPtr hwnd, IntPtr lParam)
{
Console.WriteLine(hwnd.ToInt64());
return true;
}
public static void Main(string[] args)
{
// Invoke the method; note the delegate as a first parameter.
EnumWindows(OutputWindow, IntPtr.Zero);
}
}
}
Antes de examinar el ejemplo, conviene que analicemos las firmas de las funciones no administradas con las que tenemos que trabajar. La función a la que queremos llamar para enumerar todas las ventanas tiene la firma siguiente: BOOL EnumWindows (WNDENUMPROC lpEnumFunc, LPARAM lParam);
El primer parámetro es una devolución de llamada. Dicha devolución de llamada tiene la firma siguiente: BOOL CALLBACK EnumWindowsProc (HWND hwnd, LPARAM lParam);
Ahora, examinemos el ejemplo:
- En la línea 9 del ejemplo se define un delegado que coincide con la firma de la devolución de llamada desde código no administrado. Observe cómo se representan los tipos LPARAM y HWND mediante el uso de
IntPtr
en el código administrado. - En las líneas 13 y 14 se introduce la función
EnumWindows
desde la biblioteca user32.dll. - En las líneas de la 17 a la 20 se implementa el delegado. En este sencillo ejemplo, solo queremos generar el identificador de la consola.
- Por último, en la línea 24, se invoca el método externo y se pasa el delegado.
Los ejemplos de Linux y macOS se muestran a continuación. Para ellos, usamos la función ftw
que se encuentra en libc
, la biblioteca de C. Esta función se usa para atravesar las jerarquías de directorio y toma un puntero a una función como uno de sus parámetros. Dicha función tiene la firma siguiente: int (*fn) (const char *fpath, const struct stat *sb, int typeflag)
.
using System;
using System.Runtime.InteropServices;
namespace PInvokeSamples
{
public static partial class Program
{
// Define a delegate that has the same signature as the native function.
private delegate int DirClbk(string fName, ref Stat stat, int typeFlag);
// Import the libc and define the method to represent the native function.
[LibraryImport("libc.so.6", StringMarshalling = StringMarshalling.Utf16)]
private static partial int ftw(string dirpath, DirClbk cl, int descriptors);
// Implement the above DirClbk delegate;
// this one just prints out the filename that is passed to it.
private static int DisplayEntry(string fName, ref Stat stat, int typeFlag)
{
Console.WriteLine(fName);
return 0;
}
public static void Main(string[] args)
{
// Call the native function.
// Note the second parameter which represents the delegate (callback).
ftw(".", DisplayEntry, 10);
}
}
// The native callback takes a pointer to a struct. This type
// represents that struct in managed code.
[StructLayout(LayoutKind.Sequential)]
public struct Stat
{
public uint DeviceID;
public uint InodeNumber;
public uint Mode;
public uint HardLinks;
public uint UserID;
public uint GroupID;
public uint SpecialDeviceID;
public ulong Size;
public ulong BlockSize;
public uint Blocks;
public long TimeLastAccess;
public long TimeLastModification;
public long TimeLastStatusChange;
}
}
El ejemplo de macOS usa la misma función. La única diferencia es el argumento del atributo LibraryImport
, ya que macOS guarda libc
en un lugar diferente.
using System;
using System.Runtime.InteropServices;
namespace PInvokeSamples
{
public static partial class Program
{
// Define a delegate that has the same signature as the native function.
private delegate int DirClbk(string fName, ref Stat stat, int typeFlag);
// Import the libc and define the method to represent the native function.
[LibraryImport("libSystem.dylib", StringMarshalling = StringMarshalling.Utf16)]
private static partial int ftw(string dirpath, DirClbk cl, int descriptors);
// Implement the above DirClbk delegate;
// this one just prints out the filename that is passed to it.
private static int DisplayEntry(string fName, ref Stat stat, int typeFlag)
{
Console.WriteLine(fName);
return 0;
}
public static void Main(string[] args)
{
// Call the native function.
// Note the second parameter which represents the delegate (callback).
ftw(".", DisplayEntry, 10);
}
}
// The native callback takes a pointer to a struct. This type
// represents that struct in managed code.
[StructLayout(LayoutKind.Sequential)]
public struct Stat
{
public uint DeviceID;
public uint InodeNumber;
public uint Mode;
public uint HardLinks;
public uint UserID;
public uint GroupID;
public uint SpecialDeviceID;
public ulong Size;
public ulong BlockSize;
public uint Blocks;
public long TimeLastAccess;
public long TimeLastModification;
public long TimeLastStatusChange;
}
}
Los dos ejemplos anteriores dependen de parámetros y, en ambos casos, los parámetros se proporcionan como tipos administrados. El tiempo de ejecución hace "lo correcto" y los procesa en sus equivalentes en el otro lado. Obtenga información sobre cómo los tipos se serializan al código nativo en nuestra página en Serialización de tipos.
Más recursos
- Escritura de código de P/Invoke multiplataforma
- Serialización P/Invoke generada por el origen
- El Generador de origen C#/Win32 P/Invoke genera automáticamente definiciones para las API de Windows.
- P/Invoke en C++/CLI
- Documentación de Mono en P/Invoke