El blog del burgués

3 agosto 2009

Interoperabilidad dinámica en C#

Filed under: C# — elburgues @ 7:02 PM
Tags: , ,

La plataforma .NET hace muy sencillo el hecho de llamar al API del sistema operativo subyacente así como las llamadas a librerías de código C no administrado usando la tecnología denominada invocación de la plataforma, o simplemente PInvoke. Reutilizar código no manejado desde código C# puede ser útil por razones de eficiencia pero reduce la portabilidad de la aplicación. Para hacer llamadas a código nativo desde código gestionado normalmente se hace uso del atributo System.Runtime.InteropServices.DllImport (este atributo no se usa para interoperabilidad COM). Este atributo requiere que el método externo que le siga sea estático, por ejemplo:

using System.Runtime.InteropServices;  // Aquí está definido DllImport
public class Interop
{
        [DllImport("kernel32.dll")]
        internal static extern IntPtr LoadLibrary(string DLLaCargar);
        public static void Main()
        {
              static IntPtr pDll;
              pDll = LoadLibrary(“ruta&nombre.dll”);
        }             
}

Lo que se le pasa como parámetro es el fichero donde se encuentra la implementación del método externo a continuación definido (aunque el atributo DllImport puede llevar más parámetros).

Lo que el código del ejemplo anterior hace es simplemente definir un método de nombre LoadLibrary cuyo código se corresponda con el de la función LoadLibrary del fichero kernel32.dll del API Win32. LoadLibrary se ha declarado como static y se le llama desde la misma clase donde se ha declarado (public static void Main).

Pero ¿como llamar a código no manejado cuando la dll en cuestión no es conocida hasta el tiempo de ejecución? Hay unas funciones de la API Win32 que nos van a ayudar a hacer esto. Las agrupamos en una clase “MetodosNativos” agregable directamente a cualquier proyecto:

using System;
using System.Runtime.InteropServices;

{ 
    internal static class MetodosNativos
    {

        /// <summary>
        /// Mapea el código ejecutable especificado (dll o exe) dentro del espacio de direcciones del proceso llamante.
        /// Devuelve un manejador al módulo en cuestión.
        /// </summary>
        [DllImport("kernel32.dll")]
        internal static extern IntPtr LoadLibrary(string dllToLoad);

        /// <summary>
        /// obtiene la dirección de una función exportada por la dll cargada con la función LoadLibrary.
        /// </summary>
        [DllImport("kernel32.dll")]
        internal static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);

        /// <summary>
        /// Se debe llamar a la función FreeLibrary para descargar el módulo de código ejecutable.
        /// </summary>
        [DllImport("kernel32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        internal static extern bool FreeLibrary(IntPtr hModule);

        /// <summary>
        /// Formatea una cadena de mensaje.
        /// </summary>
        [DllImport("Kernel32.dll", SetLastError = true)]
        internal static extern uint FormatMessage(uint dwFlags, IntPtr lpSource,
           uint dwMessageId, uint dwLanguageId, ref IntPtr lpBuffer,
           uint nSize, IntPtr pArguments);

        /// <summary>
        /// Libera la memoria local especificada e invalida su manejador.
        /// </summary>
        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        internal static extern IntPtr LocalFree(IntPtr hMem);
    }
}

Ahora lo que tenemos que hacer es, en otra clase a parte, definir una serie de delegados, tantos como funciones que queramos llamar de ese módulo de código ejecutable al que vamos a llamar de forma dinámica (lógicamente, delegados y funciones nativas deben coincidir en sus firmas). Utilizamos la convención de llamada Cdecl para especificar que se va a llamar a un procedimiento creado en otro lenguaje. A continuación ponemos varios ejemplos hipotéticos:


        #region "Delegados"
        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        private delegate int Fun1();

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        private delegate int Fun2(UInt32 p1, UInt32 p2);

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        private delegate uint Fun3(UInt32 p1);

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        private delegate int Fun4(UInt32 p, UInt32 p2, string p3, UInt32 p4, string p5);

        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        private delegate int Fun5(UInt32 p1, UInt32 p2,
             byte[] p3, UInt32 p4);
        #endregion
 

El resto es muy simple. Usamos LoadLibrary y GetProcAddress para obtener la dirección de la función dentro de la dll que queremos llamar  y luego usamos el método estático GetDelegateForFunctionPointer  dentro de la clase Marshall para asignar esta dirección al delegado c# que hemos definido (por ejemplo Fun1):


        [EnvironmentPermissionAttribute(SecurityAction.LinkDemand, Unrestricted = true)]
        public static void UsoEnTiempoEjecucion()
        {
            pDll = MetodosNativos.LoadLibrary(“Ruta de la Librería que queremos cargar”);
            if (pDll == IntPtr.Zero)
                ThrowException();

            IntPtr pAddressOfFunctionToCall =
                MetodosNativos.GetProcAddress(pDll, "Nombre de la function que queremos usar");

            if (pAddressOfFunctionToCall == IntPtr.Zero)
                ThrowException();

            Fun1 MiFuncion =
                (Fun1)Marshal.GetDelegateForFunctionPointer
                (pAddressOfFunctionToCall, typeof(Fun1));

            //Ejecutamos la función en cuestión.
            MiFuncion();

            //al terminar, descargamos la librería:
            if (pDll != IntPtr.Zero)
            {
                if (MetodosNativos.FreeLibrary(pDll) == false)
                    ThrowException();
            }

            return;
        }
Por último, este es el código de la función ThrowException():          

        [EnvironmentPermissionAttribute(SecurityAction.LinkDemand, Unrestricted = true)]

        private static void ThrowException()
        {
            const uint FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100;
            const uint FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200;
            const uint FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000;

            int nLastError = Marshal.GetLastWin32Error();
            IntPtr lpMsgBuf = IntPtr.Zero;

            uint dwChars = NativeMethods.FormatMessage(
                FORMAT_MESSAGE_ALLOCATE_BUFFER |
                FORMAT_MESSAGE_FROM_SYSTEM |
                FORMAT_MESSAGE_IGNORE_INSERTS,
                IntPtr.Zero,
                (uint)nLastError,
                0, // Default language
                ref lpMsgBuf,
                0,
                IntPtr.Zero);

            if (dwChars == 0)
            {
                // Handle the error.
                int le = Marshal.GetLastWin32Error();
                throw new InvalidOperationException(
                    le.ToString(CultureInfo.CurrentCulture) +
                    ": " +
                    "FormatMessage Native Method fails");
            }
            else
            {
                string sRet = Marshal.PtrToStringAnsi(lpMsgBuf);
                // Free the buffer.
                lpMsgBuf = NativeMethods.LocalFree(lpMsgBuf);
                throw new InvalidOperationException(
                    nLastError.ToString(CultureInfo.CurrentCulture) +
                    ": " +
                    sRet);
            }
        }

Anuncios

4 comentarios »

  1. ¿Como manejas los memory leaks?

    Comentario por Pardety — 4 agosto 2009 @ 5:11 PM | Responder

    • Te responderé en la siguiente entrada.

      Comentario por elburgues — 5 agosto 2009 @ 12:12 PM | Responder

  2. Hola amigo, me interesa este post pero la solución propuesta no me servi ya que trabajo con el Framework 1.1 y este no trae incorporada a la clase Marshall la función GetFunctionPointerForDelegate, por lo que me gustaría saber si tiene una variante de implementación para la misma.
    Saludos.

    Comentario por Gendry Alfonso Francia — 25 marzo 2011 @ 5:10 PM | Responder

    • Los métodos static definidos en la clase Marshal son esenciales para trabajar con código no administrado. Los desarrolladores que necesitan proporcionar un puente entre los modelos de programación administrados y no administrados usan normalmente la mayoría de los métodos definidos en esta clase. En versiones 1.0 y 1.1 de .NET Framework, era posible pasar un delegado que representaba un método administrado a código no administrado como un puntero a función, lo que permitía que el código no administrado llamara al método administrado mediante el puntero a función. También era posible que el código no administrado pasara ese puntero a función de nuevo al código administrado y el puntero se resolvía correctamente en el método administrado subyacente.

      Comentario por elburgues — 30 marzo 2011 @ 5:30 PM | Responder


RSS feed for comments on this post. TrackBack URI

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

Blog de WordPress.com.

A %d blogueros les gusta esto: