Interoperabilità

Interoperabilità

# Funzione di importazione da una DLL C++ non gestita

Ecco un esempio di come importare una funzione definita in una DLL C++ non gestita. Nel codice sorgente C++ per "myDLL.dll", la funzione add è definito:

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

Quindi può essere incluso in un programma C# come segue:

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));
    }
}

Consulta Convenzioni di chiamata e modifica dei nomi C++ per spiegazioni sul perché extern "C" e __stdcall sono necessari.

# Trovare la libreria dinamica

Quando il metodo extern viene richiamato per la prima volta, il programma C# cercherà e caricherà la DLL appropriata. Per ulteriori informazioni su dove viene cercata la DLL e su come puoi influenzare le posizioni di ricerca, consulta questa domanda sull'overflow dello stack.

# Convenzioni di chiamata

Esistono diverse convenzioni per chiamare le funzioni, specificando chi (chiamante o chiamato) apre gli argomenti dallo stack, come vengono passati gli argomenti e in quale ordine. C++ usa Cdecl convenzione di chiamata per impostazione predefinita, ma C# prevede StdCall , che viene solitamente utilizzato dall'API di Windows. Devi cambiare l'uno o l'altro:

  • Cambia la convenzione di chiamata in `StdCall` in C++:
    extern "C" __declspec(dllexport) int __stdcall add(int a, int b)
    
    
    [DllImport("myDLL.dll")]
    
    
  • Oppure, cambia la convenzione di chiamata in `Cdecl` in C#:
    extern "C" __declspec(dllexport) int /*__cdecl*/ add(int a, int b)
    
    
    [DllImport("myDLL.dll", CallingConvention = CallingConvention.Cdecl)]
    
    
  • Se vuoi usare una funzione con Cdecl convenzione di chiamata e un nome alterato, il tuo codice sarà simile a questo:

    __declspec(dllexport) int add(int a, int b)
    
    
    [DllImport("myDLL.dll", CallingConvention = CallingConvention.Cdecl,
               EntryPoint = "?add@@YAHHH@Z")]
    
    
  • **thiscall**(**__thiscall**) viene utilizzato principalmente nelle funzioni che sono membri di una classe.
  • Quando una funzione usa **thiscall**(**__thiscall**) , un puntatore alla classe viene passato come primo parametro.
  • # Modifica nomi C++

    I compilatori C++ codificano informazioni aggiuntive nei nomi delle funzioni esportate, ad esempio i tipi di argomenti, per rendere possibili sovraccarichi con argomenti diversi. Questo processo è chiamato alterazione dei nomi. Ciò causa problemi con l'importazione di funzioni in C# (e l'interoperabilità con altri linguaggi in generale), come il nome di int add(int a, int b) la funzione non è più add , può essere ?add@@YAHHH@Z , _add@8 o qualsiasi altra cosa, a seconda del compilatore e della convenzione di chiamata.

    Esistono diversi modi per risolvere il problema dell'alterazione dei nomi:

  • Esportazione di funzioni utilizzando `extern "C"` per passare al collegamento esterno C che utilizza la modifica del nome C:
    extern "C" __declspec(dllexport) int __stdcall add(int a, int b)
    
    
    [DllImport("myDLL.dll")]
    
    

    Il nome della funzione sarà ancora alterato (_add@8 ), ma StdCall +extern "C" la modifica dei nomi è riconosciuta dal compilatore C#.

  • Specifica dei nomi delle funzioni esportate in myDLL.def file di definizione del modulo:

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

    Il nome della funzione sarà puro add in questo caso.

  • Importazione di nome alterato. Avrai bisogno di un visualizzatore DLL per vedere il nome alterato, quindi puoi specificarlo esplicitamente:

    __declspec(dllexport) int __stdcall add(int a, int b)
    
    
    [DllImport("myDLL.dll", EntryPoint = "?add@@YGHHH@Z")]
    
    
  • # Caricamento e scaricamento dinamico di DLL non gestite

    Quando si utilizza il DllImport attributo devi conoscere la dll corretta e il nome del metodo in ora di compilazione . Se vuoi essere più flessibile e decidere in runtime quale dll e metodi caricare, puoi utilizzare i metodi API di Windows LoadLibrary() , GetProcAddress() e FreeLibrary() . Questo può essere utile se la libreria da utilizzare dipende dalle condizioni di runtime.

    Il puntatore restituito da GetProcAddress() può essere convertito in un delegato usando Marshal.GetDelegateForFunctionPointer() .

    L'esempio di codice seguente lo dimostra con myDLL.dll dagli esempi precedenti:

    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);
        }
    }
    
    

    # Gestione degli errori di Win32

    Quando si utilizzano metodi di interoperabilità, è possibile utilizzare GetLastError API per ottenere ulteriori informazioni sulle tue chiamate API.

    Attributo DllImport SetLastError Attributo

    SetLastError=true

    Indica che il chiamato chiamerà SetLastError (funzione API Win32).

    SetLastError=false

    Indica che il chiamato non lo farà chiama SetLastError (funzione API Win32), quindi non otterrai informazioni di errore.

  • Quando SetLastError non è impostato, è impostato su false (valore predefinito).
  • Puoi ottenere il codice di errore utilizzando il metodo Marshal.GetLastWin32Error:
  • Esempio:

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

    Se stai cercando di aprire mutex che non esiste, GetLastError restituirà ERROR_FILE_NOT_FOUND .

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

    I codici di errore di sistema possono essere trovati qui:

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

    API GetLastError

    Esiste un GetLastError nativo API che puoi usare anche tu :

    [DllImport("coredll.dll", SetLastError=true)]
    static extern Int32 GetLastError();
    
    
    • Quando si chiama l'API Win32 dal codice gestito, è necessario utilizzare sempre Marshal.GetLastWin32Error .

    Ecco perché:

    Tra la tua chiamata Win32 che imposta l'errore (chiama SetLastError), il CLR può chiamare altre chiamate Win32 che potrebbero chiamare SetLastError inoltre, questo comportamento può sovrascrivere il tuo valore di errore. In questo scenario, se chiami GetLastError puoi ottenere un errore non valido.

    Impostazione SetLastError =true , si assicura che CLR recuperi il codice di errore prima di eseguire altre chiamate Win32.

    # Strutture di lettura con Marshal

    La classe Marshal contiene una funzione denominata PtrToStructure , questa funzione ci dà la possibilità di leggere le strutture tramite un puntatore non gestito.

    PtrToStructure la funzione ha avuto molti sovraccarichi, ma hanno tutti la stessa intenzione.

    PtrToStructure generico :

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

    T - tipo di struttura.

    punto - Un puntatore a un blocco di memoria non gestito.

    Esempio:

    NATIVE_STRUCT result = Marshal.PtrToStructure<NATIVE_STRUCT>(ptr);       
    
    
    • Se hai a che fare con oggetti gestiti durante la lettura di strutture native, non dimenticare di bloccare il tuo oggetto 😃
    
    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;
        }
    
    

    # Codice semplice per esporre la classe per 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;
            }
        }
    }
    
    

    # Oggetto bloccato

    CG (Garbage Collector) è responsabile della pulizia dei nostri rifiuti.

    Mentre GC pulisce la nostra spazzatura, rimuove gli oggetti inutilizzati dall'heap gestito che causano la frammentazione dell'heap. Quando GC viene eseguita con la rimozione, esegue una compressione dell'heap (deframmentazione) che comporta lo spostamento di oggetti sull'heap.

    Dal GC non è deterministico, quando si passa un riferimento/puntatore a un oggetto gestito al codice nativo, GC può iniziare in qualsiasi momento, se si verifica subito dopo la chiamata Inerop, c'è un'ottima possibilità che l'oggetto (il cui riferimento è passato a nativo) venga spostato sull'heap gestito - di conseguenza, otteniamo un riferimento non valido sul lato gestito .

    In questo scenario, dovresti bloccare l'oggetto prima di passarlo al codice nativo.

    Oggetto bloccato

    L'oggetto bloccato è un oggetto che non può essere spostato da GC.

    Manico appuntato Gc

    Puoi creare un oggetto pin utilizzando Gc.Alloc metodo

    GCHandle handle = GCHandle.Alloc(yourObject, GCHandleType.Pinned); 
    
    
    • Ottenere un GCHandle bloccato per oggetto gestito contrassegna un oggetto specifico come uno che non può essere spostato da GC , fino a liberare il manico

    Esempio:

    [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()
        }
    }
    
    

    Precauzioni

    • Quando si blocca un oggetto (soprattutto di grandi dimensioni), provare a rilasciare il GcHandle bloccato il più velocemente possibile, poiché interrompe la deframmentazione dell'heap.
    • Se dimentichi di liberare GcHandle niente lo farà. Fallo in una sezione di codice sicura (come finaly)

    # Osservazioni

    Utilizzo dell'API Win32 utilizzando C#

    Windows espone molte funzionalità sotto forma di API Win32. Usando queste API puoi eseguire operazioni dirette in Windows, il che aumenta le prestazioni della tua applicazione. Fonte Clicca qui

    Windows espone un'ampia gamma di API. Per ottenere informazioni su varie API puoi controllare siti come pinvoke .