interoperabilidad

interoperabilidad

# Función de importación desde C++ DLL no administrado

Este es un ejemplo de cómo importar una función que está definida en una DLL de C++ no administrada. En el código fuente de C++ para "myDLL.dll", la función add se define:

extern "C" __declspec(dllexport) int __stdcall add(int a, int b)
{
    return a + b;
}

Luego se puede incluir en un programa C# de la siguiente manera:

class Program
{
    // This line will import the C++ method.
    // The name specified in the DllImport attribute must be the DLL name.
    // The names of parameters are unimportant, but the types must be correct.
    [DllImport("myDLL.dll")]
    private static extern int add(int left, int right);

    static void Main(string[] args)
    {
        //The extern method can be called just as any other C# method.
        Console.WriteLine(add(1, 2));
    }
}

Consulte Convenciones de llamadas y manipulación de nombres de C++ para obtener explicaciones sobre por qué extern "C" y __stdcall son necesarios.

# Encontrar la biblioteca dinámica

Cuando se invoca por primera vez el método externo, el programa C# buscará y cargará la DLL adecuada. Para obtener más información sobre dónde se busca para encontrar la DLL y cómo puede influir en las ubicaciones de búsqueda, consulte esta pregunta de stackoverflow.

# Convenciones de llamadas

Existen varias convenciones para llamar a funciones, que especifican quién (llamador o destinatario) saca los argumentos de la pila, cómo se pasan los argumentos y en qué orden. C++ usa Cdecl convención de llamadas por defecto, pero C# espera StdCall , que suele utilizar la API de Windows. Necesitas cambiar uno u otro:

  • Cambiar la convención de llamadas a `StdCall` en C++:
    extern "C" __declspec(dllexport) int __stdcall add(int a, int b)
    
    
    [DllImport("myDLL.dll")]
    
    
  • O cambie la convención de llamadas a `Cdecl` en C#:
    extern "C" __declspec(dllexport) int /*__cdecl*/ add(int a, int b)
    
    
    [DllImport("myDLL.dll", CallingConvention = CallingConvention.Cdecl)]
    
    
  • Si desea utilizar una función con Cdecl convención de llamadas y un nombre alterado, su código se verá así:

    __declspec(dllexport) int add(int a, int b)
    
    
    [DllImport("myDLL.dll", CallingConvention = CallingConvention.Cdecl,
               EntryPoint = "?add@@YAHHH@Z")]
    
    
  • **thiscall**(**__thiscall**) se usa principalmente en funciones que son miembros de una clase.
  • Cuando una función usa **thiscall**(**__thiscall**) , se pasa un puntero a la clase como primer parámetro.
  • # C++ manipulación de nombres

    Los compiladores de C++ codifican información adicional en los nombres de las funciones exportadas, como los tipos de argumentos, para hacer posibles las sobrecargas con diferentes argumentos. Este proceso se llama manipulación de nombres. Esto causa problemas con la importación de funciones en C# (y la interoperabilidad con otros lenguajes en general), como el nombre de int add(int a, int b) la función ya no es add , puede ser ?add@@YAHHH@Z , _add@8 o cualquier otra cosa, según el compilador y la convención de llamadas.

    Hay varias formas de resolver el problema de la manipulación de nombres:

  • Exportación de funciones usando `extern "C"` para cambiar a enlace externo C que usa C name mangling:
    extern "C" __declspec(dllexport) int __stdcall add(int a, int b)
    
    
    [DllImport("myDLL.dll")]
    
    

    El nombre de la función aún se modificará (_add@8 ), pero StdCall +extern "C" el compilador de C# reconoce la manipulación de nombres.

  • Especificar nombres de funciones exportadas en myDLL.def archivo de definición de módulo:

    EXPORTS
      add
    
    
    int __stdcall add(int a, int b)
    
    
    [DllImport("myDLL.dll")]
    
    

    El nombre de la función será puro add en este caso.

  • Importando nombre alterado. Necesitará algún visor de DLL para ver el nombre alterado, luego puede especificarlo explícitamente:

    __declspec(dllexport) int __stdcall add(int a, int b)
    
    
    [DllImport("myDLL.dll", EntryPoint = "?add@@YGHHH@Z")]
    
    
  • # Carga y descarga dinámica de DLLs no gestionadas

    Al usar el DllImport atributo que debe conocer el dll correcto y el nombre del método en tiempo de compilación . Si quiere ser más flexible y decidir en tiempo de ejecución qué dll y métodos cargar, puede usar los métodos de la API de Windows LoadLibrary() , GetProcAddress() y FreeLibrary() . Esto puede ser útil si la biblioteca a usar depende de las condiciones de tiempo de ejecución.

    El puntero devuelto por GetProcAddress() se puede convertir en un delegado usando Marshal.GetDelegateForFunctionPointer() .

    El siguiente ejemplo de código demuestra esto con el myDLL.dll de los ejemplos anteriores:

    class Program
    {
        // import necessary API as shown in other examples
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr LoadLibrary(string lib);
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern void FreeLibrary(IntPtr module);
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr GetProcAddress(IntPtr module, string proc);
    
        // declare a delegate with the required signature
        private delegate int AddDelegate(int a, int b);
    
        private static void Main()
        {
            // load the dll
            IntPtr module = LoadLibrary("myDLL.dll");
            if (module == IntPtr.Zero) // error handling
            {
                Console.WriteLine($"Could not load library: {Marshal.GetLastWin32Error()}");
                return;
            }
    
            // get a "pointer" to the method
            IntPtr method = GetProcAddress(module, "add");
            if (method == IntPtr.Zero) // error handling
            {
                Console.WriteLine($"Could not load method: {Marshal.GetLastWin32Error()}");
                FreeLibrary(module);  // unload library
                return;
            }
                
            // convert "pointer" to delegate
            AddDelegate add = (AddDelegate)Marshal.GetDelegateForFunctionPointer(method, typeof(AddDelegate));
        
            // use function    
            int result = add(750, 300);
            
            // unload library   
            FreeLibrary(module);
        }
    }
    
    

    # Lidiando con Errores Win32

    Al usar métodos de interoperabilidad, puede usar GetLastError API para obtener información adicional sobre sus llamadas API.

    Atributo DllImportar atributo SetLastError

    EstablecerÚltimoError=verdadero

    Indica que el destinatario llamará a SetLastError (función API Win32).

    EstablecerÚltimoError=falso

    Indica que el destinatario no llame a SetLastError (función API de Win32), por lo tanto, no obtendrá información de error.

  • Cuando SetLastError no está establecido, se establece en falso (valor predeterminado).
  • Puede obtener el código de error utilizando Marshal.GetLastWin32Error Method:
  • Ejemplo:

    [DllImport("kernel32.dll", SetLastError=true)]
    public static extern IntPtr OpenMutex(uint access, bool handle, string lpName);
    
    

    Si intenta abrir un mutex que no existe, GetLastError devolverá ERROR_FILE_NOT_FOUND .

    var lastErrorCode = Marshal.GetLastWin32Error();
    
    if (lastErrorCode == (uint)ERROR_FILE_NOT_FOUND)
    {
        //Deal with error         
    }
    
    

    Los códigos de error del sistema se pueden encontrar aquí:

    https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx

    API GetLastError

    Hay un GetLastError nativo API que también puedes usar:

    [DllImport("coredll.dll", SetLastError=true)]
    static extern Int32 GetLastError();
    
    
    • Al llamar a la API de Win32 desde el código administrado, siempre debe usar el Marshal.GetLastWin32Error .

    He aquí por qué:

    Entre su llamada de Win32 que establece el error (llama a SetLastError), el CLR puede llamar a otras llamadas de Win32 que podrían llamar a SetLastError además, este comportamiento puede anular su valor de error. En este escenario, si llama a GetLastError puede obtener un error no válido.

    Configuración de SetLastError =verdadero , se asegura de que CLR recupere el código de error antes de ejecutar otras llamadas de Win32.

    # Lectura de estructuras con Marshal

    La clase Marshal contiene una función llamada PtrToStructure , esta función nos brinda la capacidad de leer estructuras mediante un puntero no administrado.

    PtrAEstructura La función tiene muchas sobrecargas, pero todas tienen la misma intención.

    PtrToStructure genérico :

    public static T PtrToStructure<T>(IntPtr ptr);
    
    

    T - tipo de estructura.

    ptr - Un puntero a un bloque de memoria no administrado.

    Ejemplo:

    NATIVE_STRUCT result = Marshal.PtrToStructure<NATIVE_STRUCT>(ptr);       
    
    
    • Si trata con objetos administrados mientras lee estructuras nativas, no olvide anclar su objeto 😃
    
    T Read<T>(byte[] buffer)
        {
            T result = default(T);
            
            var gch = GCHandle.Alloc(buffer, GCHandleType.Pinned);
        
            try
            {
                result = Marshal.PtrToStructure<T>(gch.AddrOfPinnedObject());
            }
            finally
            {
                gch.Free();
            }
            
            return result;
        }
    
    

    # Código simple para exponer la clase para com

    using System;
    using System.Runtime.InteropServices;
     
    namespace ComLibrary
    {
        [ComVisible(true)]
        public interface IMainType
        {
            int GetInt();
     
            void StartTime();
     
            int StopTime();
        }
     
        [ComVisible(true)]
        [ClassInterface(ClassInterfaceType.None)]
        public class MainType : IMainType
        {
            private Stopwatch stopWatch;
     
            public int GetInt()
            {
                return 0;
            }
     
            public void StartTime()
            {
                stopWatch= new Stopwatch();
                stopWatch.Start();
            }
     
            public int StopTime()
            {
                return (int)stopWatch.ElapsedMilliseconds;
            }
        }
    }
    
    

    # Objeto Anclado

    GC (Recolector de Basura) es el encargado de limpiar nuestra basura.

    Mientras que GC limpia nuestra basura, elimina los objetos no utilizados del montón administrado que causan la fragmentación del montón. Cuando GC se realiza con la eliminación, realiza una compresión de almacenamiento dinámico (desfragmentación) que implica mover objetos en el almacenamiento dinámico.

    Desde GC no es determinista, al pasar la referencia/el puntero del objeto administrado al código nativo, GC puede activarse en cualquier momento, si ocurre justo después de la llamada de Inerop, existe una gran posibilidad de que el objeto (cuya referencia pasó al nativo) se mueva al montón administrado; como resultado, obtenemos una referencia no válida en el lado administrado .

    En este escenario, debe fijar el objeto antes de pasarlo al código nativo.

    Objeto anclado

    El objeto anclado es un objeto que GC no permite mover.

    Manija fijada Gc

    Puede crear un objeto pin usando Gc.Alloc método

    GCHandle handle = GCHandle.Alloc(yourObject, GCHandleType.Pinned); 
    
    
    • Obtención de un GCHandle anclado a objeto administrado marca un objeto específico como uno que no puede ser movido por GC , hasta liberar el asa

    Ejemplo:

    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern void EnterCriticalSection(IntPtr ptr);
    
    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern void LeaveCriticalSection(IntPtr ptr);
           
    public void EnterCriticalSection(CRITICAL_SECTION section)
    {
        try
        {
            GCHandle handle = GCHandle.Alloc(section, GCHandleType.Pinned); 
            EnterCriticalSection(handle.AddrOfPinnedObject());
            //Do Some Critical Work
            LeaveCriticalSection(handle.AddrOfPinnedObject());
        }
        finaly
        {
            handle.Free()
        }
    }
    
    

    Precauciones

    • Al fijar objetos (especialmente los grandes), intente soltar el GcHandle fijado. lo más rápido posible, ya que interrumpe la desfragmentación del montón.
    • Si olvida liberar GcHandle nada lo hará. Hazlo en una sección de código seguro (como finalmente)

    # Comentarios

    Trabajar con la API de Win32 usando C#

    Windows expone muchas funciones en forma de API Win32. Con estas API, puede realizar operaciones directas en Windows, lo que aumenta el rendimiento de su aplicación. Fuente Haga clic aquí

    Windows expone una amplia gama de API. Para obtener información sobre varias API, puede consultar sitios como pinvoke.