Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 22 de enero de 2019

Blogger invitado

Blogger invitado

Jorge Turrado

Apasionado de la programación, siempre buscando la manera de mejorar el día a día con el desarrollo de tecnologías .NET. Apasionado de este momento tan cambiante y fabuloso para ser desarrollador C#.
Blog: Fixed Buffer
Hola a todos los lectores de Variable Not Found, es un gusto para mi poder hacer esta colaboración con José, en la que vengo a hablaros de cómo poder utilizar código nativo desde .NET Core, sin perder la capacidad de ejecución multiplataforma que nos ofrece este maravilloso framework.

¿Y por qué me debería interesar utilizar código nativo si .NET Core ya es lo bastante rápido? Esa es una muy buena pregunta, es cierto que con .NET Core y su filosofía cloud se ha mejorado muchísimo el rendimiento, y se ha modularizado el framework consiguiendo unos resultados muy buenos.

Sin embargo, imagina que tu aplicación web necesita de un rendimiento excelente, por ejemplo, porque necesites enviarle imágenes y tengas que procesarlas en el servidor para leer un OCR o procesar su contenido. En estos casos, cada ciclo cuenta, ya que afecta directamente a la escalabilidad de tu aplicación; no es lo mismo tardar 10 milisegundos que 20, cuando hablas de un gran número de usuarios concurrentes.

El framework nos da la posibilidad de ejecutar código nativo (ya .NET Framework nos daba esa posibilidad hace mucho), pero el código nativo es código compilado directamente para el sistema operativo, y esto hace que una librería de Windows sólo sirva para Windows, lo mismo que si fuese de Linux o MacOS.

Hoy vamos a abordar ese problema, creando librerías nativas multiplataforma, que podamos compilar sin tener que cambiar nada en ellas (lo cual nos permite tener una única librería que mantener) y consumiéndolas desde .NET Core. Por eso, en mi blog FixedBuffer he dejado la primera parte de esta entrada:

Crear y utilizar librerías multiplataforma con C++ y .NET Core (Parte 1)

A partir de aquí, vamos a considerar que ya tenéis vuestra librería nativa compilada, y vamos a centrarnos solo en la parte de C# y .NET Core.


Llamadas a librerías nativas

.NET Core Lo primero que hay que saber es que las librerías nativas no se referencian dentro del proyecto como haríamos con librerías .NET (administradas), son código no administrado al que tenemos que acceder directamente conociendo los métodos que expone.

Para esto, el framework nos ofrece una herramienta que nos permite consumir código nativo, P/Invoke (dentro del namespace System.Runtime.InteropServices). Gracias a esto, vamos a poder consumir código nativo sin problemas.

Como vimos en la primera parte, cada sistema operativo genera ficheros de librería con diferente extensión, pero esto es capaz de manejarlo .NET Core perfectamente. Supongamos que tenemos algo así:
[DllImport("mylib")]
static extern void DoThing();
Internamente, el framework intentará acceder al binario con los siguientes nombres:
  • mylib.dll
  • mylib.so
  • mylib.dylib
  • libmylib.so
  • libmylib.dylib
El gran problema aquí podría ser si nuestras librerías se llamasen de manera diferente en cada sistema operativo (para nuestro ejemplo no es el caso). Imaginemos que nuestra librería, en vez de ser "mylib", fuese "winlib", "linuxlib" y "maclib"; el framework sería incapaz de resolver esa situación (de hecho, se debate desde 2015), pero podemos echarle una mano evaluando nosotros el sistema operativo a través de la clase RuntimeInformation, que nos permite saber en qué sistema operativo corremos, así como detalles del mismo:
[DllImport("winlib")]
static extern void DoThing_Windows();
[DllImport("linuxlib")]
static extern void DoThing_Linux();
[DllImport("maclib")]
static extern void DoThing_Mac();

public void DoThing()
{
    if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        DoThing_Windows();
    else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
        DoThing_Linux();
    else
        DoThing_Mac();
}
Esta característica está disponible desde .NET Framework 4.7.1 , en todos los .NET Core y .NET Standard, y no sólo nos permite hacer diferenciación entre llamadas nativas, sino que nos podría servir por ejemplo para el sistema de archivos, ejecución de comandos PS o Bash, o cualquier otra cosa en la que nos interese saber si estamos sobre Windows, Linux o MacOS.

Dicho todo esto, vamos a ponerlo en práctica con la librería que ya hemos preparado, y veamos cómo se comporta, para eso, he creado un ejecutable de consola como el siguiente:
using System;
using System.Runtime.InteropServices;
using System.Text;

namespace PostNetCoreNativo
{
    class Program
    {
        //====================CONSTANTES====================
        const int STRING_MAX_LENGTH = 1024;

        //====================DECLARACIÓN DE LLAMADAS NATIVAS===================================
        [DllImport("EjemploNativo", EntryPoint = "GetStringMessage")]
        public static extern void GetStringMessageNativo(StringBuilder sb, int StringLenght);

        [DllImport("EjemploNativo", EntryPoint = "Suma")]
        public static extern int SumaNativo(int A, int B);

        //Obtenemos el mensaje desde C++
        static string GetStringMessage()
        {
            //Declaramos el objeto que nos devolverá el mensaje
            StringBuilder sb = new StringBuilder(STRING_MAX_LENGTH);

            //Llamamos a la librería nativa
            GetStringMessageNativo(sb, STRING_MAX_LENGTH);

            return sb.ToString();
        }


        static void Main(string[] args)
        {
            //Obtenemos el sistema operativo sobre el que corre la aplicación
            string OS = RuntimeInformation.OSDescription;
            int sumando1 = 123, sumando2 = 3245;

            Console.WriteLine($"Mensaje escrito en C# sobre {OS}");
            Console.WriteLine(GetStringMessage());
            Console.WriteLine(
                $"Suma desde código nativo '{sumando1} + {sumando2} = {SumaNativo(sumando1, sumando2)}'");

            Console.Read();
        }
    }
}
En él, vemos que importamos nuestra librería a través de P/Invoke (como se llama igual en todas las plataformas, no necesitamos tener más cosas en cuenta), y después simplemente mostramos por consola un texto con el sistema operativo sobre el que corremos, un mensaje obtenido desde la librería en C++, y la suma de 2 números que también ejecuta el código en C++.

Tenemos que tener en cuenta aquí, que la librería, al no estar referenciada en la solución, no se copiará automáticamente a la carpeta de salida, por lo que es necesario que seamos nosotros quienes la copiemos después de compilar, o en caso contrario tendremos una excepción del tipo System.DllNotFoundException.

Una vez que hemos compilado la solución, y que hemos copiado la librería, vamos a ver qué es lo que nos muestra en los diferentes entornos.

Windows:
Resultado de ejecutar la aplicación en Windows

Linux:
Resultado de ejecutar la aplicación en Linux

MacOS:
Resultado de ejecutar la aplicación en Windows

Como se puede ver, el mismo ejecutable es capaz de consumir las distintas librerías en función de la plataforma, y nos muestra la salida esperada en todas ellas.

Y con esto me despido, para mí ha sido un placer, y espero que os haya gustado el tema, como siempre, os dejo en GitHub el código completo para poder probarlo. Intentaré volver (si José me deja obviamente) con alguna cosa interesante si la situación lo permite.
José M. Aguilar> El placer ha sido nuestro, Jorge. Estaremos atentos a tu blog, y esperamos volver a verte pronto escribiendo por aquí ;) ¡Muchas gracias por publicar en Variable Not Found!

3 Comentarios:

Iñaki dijo...

Por qué usas el ToString() en un string?

La línea:
Console.WriteLine(GetStringMessage().ToString());

no sería suficiente con Console.WriteLine(GetStringMessage());

Otra pregunta ¿el compilador la quita y se pone por claridad?

De todas formas el artículo está muy bien y me ha resultado bastante interesante.

También hay la posibilidad de hacer dll que usen el .Net Framework en C++ y una versión mixta. Es decir este ejemplo se podría haber hecho compilando la dll en C++.Net y usando el import de toda la vida para usarla.

JorTurFer dijo...

Buenas Jose Ingacio,
¡Gracias por tu comentario! Voy a contestarlo por partes:

1-Respecto al tema del "ToString()", toda la razón!, esa invocación sobra, y es un error que viene de que en una primera versión, GetStringMessage devolvía un StringBuilder, después lo cambie por simplificar, y olvide quitarlo de ahí, lo he actualizado en el repositorio ya, y si José tiene un momento seguro que lo actualiza aquí también (a que sí? xD). Si el compilador lo optimiza y lo borra o no, tendríamos que ver el bytecode generado para poder decir algo concreto.

2-Respecto al tema de C++ utilizando una librería mixta, sí y no. Esa opción que tu planteas, es perfectamente válida en .Net Framework (el de toda la vida), pero para poder hacer eso multiplataforma, deberías poder compilar tu librería C++/Cli a NetStandard o NetCore, y actualmente (hasta donde yo sé), C++/Cli no soporta compilar ni a NetCore ni a NetStandard y no hay planes de que lo haga (he hecho una búsqueda rápida y parece que el tema sigue así, pero no es mi campo, si lo han soportado avísame por favor), además, entiendo que no es algo banal, ya que las toolkits de C++ varían en función del compilador, y hace falta que estén presentes para ejecutar el código. Además de que para ser realmente nativo y no administrado, habría que compilar/cross-compilar para cada plataforma en la que vas a ejecutarlo (exactamente igual que harías con C++ nativo, así que no aporta una gran ventaja).
Por último, está el hecho de que si haces la librería en C++ nativo siguiendo el criterio multiplataforma, te sirve para consumirla desde cualquier lenguaje y entorno (Python, Java,…), y no solo desde NetCore (aunque lo que nos interesa en este caso es solo NetCore).

Espero haber resuelto tus dudas, cualquier cosa estaré encantado de leerla! xD
P.D: Gracias de nuevo por lo de “ToString()”, como compila y funciona, ni me había dado cuenta…

José María Aguilar dijo...

Hola a ambos,

he eliminado el ToString(), que ciertamente era redundante :)

Muchas gracias!