Interoperabilidad nativaNative Interoperability

En este documento se profundiza un poco más en las tres formas de obtener "interoperabilidad nativa" disponibles mediante .NET.In this document, we will dive a little bit deeper into all three ways of doing "native interoperability" that are available using .NET.

Existen varios motivos por los que puede interesarle llamar a código nativo:There are a few of reasons why you would want to call into native code:

  • Los sistemas operativos incluyen un elevado volumen de API que no están presentes en las bibliotecas de clases administradas.Operating Systems come with a large volume of APIs that are not present in the managed class libraries. Un buen ejemplo de esto sería el acceso al hardware o a funciones de administración del sistema operativo.A prime example for this would be access to hardware or operating system management functions.
  • La comunicación con otros componentes que tienen o pueden generar ABI de estilo C (ABI nativos).Communicating with other components that have or can produce C-style ABIs (native ABIs). Esto incluye, por ejemplo, código Java que se expone a través de Java Native Interface (JNI) o cualquier lenguaje administrado que pueda producir un componente nativo.This covers, for example, Java code that is exposed via Java Native Interface (JNI) or any other managed language that could produce a native component.
  • En Windows, la mayor parte del software que se instala, como el conjunto de aplicaciones de Microsoft Office, registra los componentes COM que representan sus programas y permiten a los desarrolladores automatizarlos o usarlos.On Windows, most of the software that gets installed, such as Microsoft Office suite, registers COM components that represent their programs and allow developers to automate them or use them. Esto también requiere interoperabilidad nativa.This also requires native interoperability.

Por supuesto, la lista anterior no cubre todas las posibles situaciones y escenarios en los que el desarrollador puede querer o necesitar interactuar con componentes nativos.Of course, the list above does not cover all of the potential situations and scenarios in which the developer would want/like/need to interface with native components. La biblioteca de clases. NET, por ejemplo, usa la compatibilidad con la interoperabilidad nativa para implementar bastantes de sus API, como la compatibilidad con la consola y su manipulación, el acceso al sistema de archivos, etc..NET class library, for instance, uses the native interoperability support to implement a fair number of its APIs, like console support and manipulation, file system access and others. Pero es importante tener en cuenta que existe la opción de hacerlo, en caso de que sea necesario.However, it is important to note that there is an option, should one need it.

Nota

La mayoría de los ejemplos de este documento se presentarán para las tres plataformas compatibles con .NET Core (Windows, Linux y macOS).Most of the examples in this document will be presented for all three supported platforms for .NET Core (Windows, Linux and macOS). En cambio, en algunos ejemplos breves e ilustrativos solo se muestra un ejemplo que usa nombres de archivo y extensiones de Windows (es decir, "dll" en el caso de las bibliotecas).However, for some short and illustrative examples, just one sample is shown that uses Windows filenames and extensions (that is, "dll" for libraries). Esto no significa que esas características no estén disponibles en Linux o macOS, sino que simplemente se buscaba una mayor comodidad.This does not mean that those features are not available on Linux or macOS, it was done merely for convenience sake.

Invocación de plataforma (P/Invoke)Platform Invoke (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.P/Invoke is a technology that allows you to access structs, callbacks and functions in unmanaged libraries from your managed code. La mayor parte de la API de P/Invoke se encuentra en dos espacios de nombres: System y System.Runtime.InteropServices.Most of the P/Invoke API is contained in two namespaces: System and System.Runtime.InteropServices. Mediante estos dos espacios de nombres, puede acceder a los atributos que describen cómo quiere comunicarse con el componente nativo.Using these two namespaces will allow you access to the attributes that describe how you want to communicate with the native component.

Empecemos por el ejemplo más común, es decir, llamar a funciones no administradas en el código administrado.Let’s start from the most common example, and that is calling unmanaged functions in your managed code. Vamos a mostrar un cuadro de mensaje desde una aplicación de línea de comandos:Let’s show a message box from a command-line application:

using System.Runtime.InteropServices;

public class Program {

    // Import user32.dll (containing the function we need) and define
    // the method corresponding to the native function.
    [DllImport("user32.dll")]
    public static extern int MessageBox(IntPtr hWnd, String text, String caption, int options);

    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 bastante sencillo, pero resalta lo que es necesario para invocar las funciones no administradas desde código administrado.The example above is pretty simple, but it does show off what is needed to invoke unmanaged functions from managed code. Veamos en detalle el ejemplo:Let’s step through the example:

  • En la línea 1 se muestra el uso de la instrucción para System.Runtime.InteropServices, que es el espacio de nombres que contiene todos los elementos que necesitamos.Line #1 shows the using statement for the System.Runtime.InteropServices which is the namespace that holds all of the items we need.
  • En la línea 5 se introduce el atributo DllImport.Line #5 introduces the DllImport attribute. Este atributo es fundamental, ya que le indica al tiempo de ejecución que debe cargar la DLL no administrada.This attribute is crucial, as it tells the runtime that it should load the unmanaged DLL. Se trata de la DLL en la que queremos invocar.This is the DLL into which we wish to invoke.
  • La línea 6 es la esencia del trabajo de P/Invoke.Line #6 is the crux of the P/Invoke work. Define un método administrado que tiene exactamente la misma firma que el no administrado.It defines a managed method that has the exact same signature as the unmanaged one. Como puede ver, la declaración tiene una nueva palabra clave (extern) que le indica al tiempo de ejecución que esto es un método externo y que, cuando se invoca, el tiempo de ejecución debe buscarlo en el archivo DLL especificado en el atributo DllImport.The declaration has a new keyword that you can notice, extern, which tells the runtime this is an external method, and that when you invoke it, the runtime should find it in the DLL specified in DllImport attribute.

El resto del ejemplo simplemente invoca el método como si se tratara de cualquier otro método administrado.The rest of the example is just invoking the method as you would any other managed method.

El ejemplo es parecido para macOS.The sample is similar for macOS. Evidentemente, lo que debe cambiar es el nombre de la biblioteca en el atributo DllImport, ya que macOS tiene un esquema diferente para la nomenclatura de bibliotecas dinámicas.One thing that needs to change is, of course, the name of the library in the DllImport attribute, as macOS has a different scheme of naming dynamic libraries. 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.The sample below uses the getpid(2) function to get the process ID of the application and print it out to the console.

using System;
using System.Runtime.InteropServices;

namespace PInvokeSamples {
    public static class Program {

        // Import the libSystem shared library and define the method corresponding to the native function.
        [DllImport("libSystem.dylib")]
        private static extern 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.It is also similar on Linux. El nombre de la función es el mismo, ya que getpid(2) es la llamada del sistema estándar de POSIX.The function name is the same, since getpid(2) is a standard POSIX system call.

using System;
using System.Runtime.InteropServices;

namespace PInvokeSamples {
    public static class Program {

        // Import the libc shared library and define the method corresponding to the native function.
        [DllImport("libc.so.6")]
        private static extern 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 administradoInvoking managed code from unmanaged code

Por supuesto, el tiempo de ejecución permite que la comunicación fluya en ambas direcciones, lo que permite llamar a artefactos administrados desde funciones nativas mediante el uso de punteros de función.Of course, the runtime allows communication to flow both ways which enables you to call into managed artifacts from native functions, using function pointers. 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.The closest thing to a function pointer in managed code is a delegate, so this is what is used to allow callbacks from native code into managed code.

La forma en que se usa esta característica se parece al proceso de administrado a nativo que se ha descrito anteriormente.The way to use this feature is similar to managed to native process described above. 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.For a given callback, you define a delegate that matches the signature, and pass that into the external method. El tiempo de ejecución se encargará de todo lo demás.The runtime will take care of everything else.

using System;
using System.Runtime.InteropServices;

namespace ConsoleApplication1 {

    class Program {

        // Define a delegate that corresponds to the unmanaged function.
        delegate bool EnumWC(IntPtr hwnd, IntPtr lParam);

        // Import user32.dll (containing the function we need) and define
        // the method corresponding to the native function.
        [DllImport("user32.dll")]
        static extern int EnumWindows(EnumWC lpEnumFunc, IntPtr lParam);

        // Define the implementation of the delegate; here, we simply output the window handle.
        static bool OutputWindow(IntPtr hwnd, IntPtr lParam) {
            Console.WriteLine(hwnd.ToInt64());
            return true;
        }

        static void Main(string[] args) {
            // Invoke the method; note the delegate as a first parameter.
            EnumWindows(OutputWindow, IntPtr.Zero);
        }
    }
}

Antes de examinar nuestro ejemplo, conviene que analicemos las firmas de las funciones no administradas con las que tenemos que trabajar.Before we walk through our example, it is good to go over the signatures of the unmanaged functions we need to work with. La función a la que queremos llamar para enumerar todas las ventanas tiene la firma siguiente: BOOL EnumWindows (WNDENUMPROC lpEnumFunc, LPARAM lParam);The function we want to call to enumerate all of the windows has the following signature: BOOL EnumWindows (WNDENUMPROC lpEnumFunc, LPARAM lParam);

El primer parámetro es una devolución de llamada.The first parameter is a callback. Dicha devolución de llamada tiene la firma siguiente: BOOL CALLBACK EnumWindowsProc (HWND hwnd, LPARAM lParam);The said callback has the following signature: BOOL CALLBACK EnumWindowsProc (HWND hwnd, LPARAM lParam);

Teniendo esto en cuenta, examinemos el ejemplo:With this in mind, let’s walk through the example:

  • En la línea 8 del ejemplo se define un delegado que coincide con la firma de la devolución de llamada desde código no administrado.Line #8 in the example defines a delegate that matches the signature of the callback from unmanaged code. Observe cómo se representan los tipos LPARAM y HWND mediante el uso de IntPtr en el código administrado.Notice how the LPARAM and HWND types are represented using IntPtr in the managed code.
  • En las líneas 10 y 11 se introduce la función EnumWindows desde la biblioteca user32.dll.Lines #10 and #11 introduce the EnumWindows function from the user32.dll library.
  • En las líneas de la 13 a la 16 se implementa el delegado.Lines #13 - 16 implement the delegate. En este sencillo ejemplo, solo queremos generar el identificador de la consola.For this simple example, we just want to output the handle to the console.
  • Por último, en la línea 19, se invoca el método externo y se pasa el delegado.Finally, in line #19 we invoke the external method and pass in the delegate.

Los ejemplos de Linux y macOS se muestran a continuación.The Linux and macOS examples are shown below. Para ellos, usamos la función ftw que se encuentra en libc, la biblioteca de C.For them, we use the ftw function that can be found in libc, the C library. 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.This function is used to traverse directory hierarchies and it takes a pointer to a function as one of its parameters. Dicha función tiene la firma siguiente: int (*fn) (const char *fpath, const struct stat *sb, int typeflag).The said function has the following signature: int (*fn) (const char *fpath, const struct stat *sb, int typeflag).

using System;
using System.Runtime.InteropServices;

namespace PInvokeSamples {
    public static class Program {

            // Define a delegate that has the same signature as the native function.
            delegate int DirClbk(string fName, StatClass stat, int typeFlag);

            // Import the libc and define the method to represent the native function.
            [DllImport("libc.so.6")]
            static extern 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.
            static int DisplayEntry(string fName, StatClass 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. The below class
    // represents that struct in managed code. You can find more information
    // about this in the section on marshalling below.
    [StructLayout(LayoutKind.Sequential)]
    public class StatClass {
            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 DllImport, ya que macOS guarda libc en un lugar diferente.macOS example uses the same function, and the only difference is the argument to the DllImport attribute, as macOS keeps libc in a different place.

using System;
using System.Runtime.InteropServices;

namespace PInvokeSamples {
        public static class Program {

                // Define a delegate that has the same signature as the native function.
                delegate int DirClbk(string fName, StatClass stat, int typeFlag);

                // Import the libc and define the method to represent the native function.
                [DllImport("libSystem.dylib")]
                static extern 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.
                static int DisplayEntry(string fName, StatClass 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. The below class
        // represents that struct in managed code. You can find more information
        // about this in the section on marshalling below.
        [StructLayout(LayoutKind.Sequential)]
        public class StatClass {
                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.Both of the above examples depend on parameters, and in both cases, the parameters are given as managed types. El tiempo de ejecución hace "lo correcto" y los procesa en sus equivalentes en el otro lado.Runtime does the "right thing" and processes these into its equivalents on the other side. Dado que este proceso es muy importante para escribir código de interoperabilidad nativa de calidad, vamos a ver lo que sucede cuando el tiempo de ejecución serializa los tipos.Since this process is really important to writing quality native interop code, let’s take a look at what happens when the runtime marshals the types.

Serializar tiposType marshalling

La serialización es el proceso de transformación de tipos cuando tienen que cruzar el límite de administrado a nativo y viceversa.Marshalling is the process of transforming types when they need to cross the managed boundary into native and vice versa.

La serialización es necesaria porque los tipos del código administrado y del código no administrado son diferentes.The reason marshalling is needed is because the types in the managed and unmanaged code are different. En el código administrado, por ejemplo, tiene String, mientras que en el entorno no administrado, las cadenas pueden ser Unicode ("anchas"), no Unicode, terminadas en un valor nulo, ASCII, etc. De forma predeterminada, el subsistema de P/Invoke intentará hacer "lo correcto" según el comportamiento predeterminado.In managed code, for instance, you have a String, while in the unmanaged world strings can be Unicode ("wide"), non-Unicode, null-terminated, ASCII, etc. By default, the P/Invoke subsystem will try to do the right thing based on the default behavior. 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.However, for those situations where you need extra control, you can employ the MarshalAs attribute to specify what is the expected type on the unmanaged side. Por ejemplo, si queremos que la cadena se envíe como una cadena ANSI terminada en un valor nulo, podemos hacerlo de la manera siguiente:For instance, if we want the string to be sent as a null-terminated ANSI string, we could do it like this:

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

Serializar clases y estructurasMarshalling classes and structs

Otro aspecto de la serialización de tipos es cómo pasar una estructura a un método no administrado.Another aspect of type marshalling is how to pass in a struct to an unmanaged method. Por ejemplo, algunos métodos no administrados requieren una estructura como parámetro.For instance, some of the unmanaged methods require a struct as a parameter. En estos casos, debemos crear una clase o una estructura correspondiente en la parte administrada del entorno para usarla como un parámetro.In these cases, we need to create a corresponding struct or a class in managed part of the world to use it as a parameter. Pero no basta con definir simplemente la clase. También es necesario indicarle al contador de referencias cómo asignar campos de la clase a la estructura no administrada.However, just defining the class is not enough, we also need to instruct the marshaler how to map fields in the class to the unmanaged struct. Aquí es donde entra en juego el atributo StructLayout.This is where the StructLayout attribute comes into play.

[DllImport("kernel32.dll")]
static extern void GetSystemTime(SystemTime systemTime);

[StructLayout(LayoutKind.Sequential)]
class SystemTime {
    public ushort Year;
    public ushort Month;
    public ushort DayOfWeek;
    public ushort Day;
    public ushort Hour;
    public ushort Minute;
    public ushort Second;
    public ushort Milsecond;
}

public static void Main(string[] args) {
    SystemTime st = new SystemTime();
    GetSystemTime(st);
    Console.WriteLine(st.Year);
}

En el ejemplo anterior se muestra un ejemplo sencillo de una llamada a la función GetSystemTime().The example above shows off a simple example of calling into GetSystemTime() function. La parte interesante está en la línea 4.The interesting bit is on line 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).The attribute specifies that the fields of the class should be mapped sequentially to the struct on the other (unmanaged) side. 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 a continuación:This means that the naming of the fields is not important, only their order is important, as it needs to correspond to the unmanaged struct, shown below:

typedef struct _SYSTEMTIME {
  WORD wYear;
  WORD wMonth;
  WORD wDayOfWeek;
  WORD wDay;
  WORD wHour;
  WORD wMinute;
  WORD wSecond;
  WORD wMilliseconds;
} SYSTEMTIME, *PSYSTEMTIME*;

Ya vimos el ejemplo de Linux y macOS para esto en el ejemplo anterior,We already saw the Linux and macOS example for this in the previous example. pero lo mostramos de nuevo a continuación.It is shown again below.

[StructLayout(LayoutKind.Sequential)]
public class StatClass {
        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;
}

La clase StatClass representa una estructura que se devuelve mediante la llamada del sistema stat en sistemas UNIX.The StatClass class represents a structure that is returned by the stat system call on UNIX systems. Representa información sobre un archivo determinado.It represents information about a given file. La clase anterior es la representación de estructura estática en código administrado.The class above is the stat struct representation in managed code. De nuevo, los campos de la clase deben estar en el mismo orden que la estructura nativa (puede encontrarlos si lee con detenimiento las páginas del manual de su implementación de UNIX favorita) y deben ser del mismo tipo subyacente.Again, the fields in the class have to be in the same order as the native struct (you can find these by perusing man pages on your favorite UNIX implementation) and they have to be of the same underlying type.

Más recursosMore resources