C# Language
interoperabilità
Ricerca…
Osservazioni
Lavorare con API Win32 usando C #
Windows espone molte funzionalità sotto forma di API Win32. Usando queste API è possibile eseguire operazioni dirette in Windows, il che aumenta le prestazioni della tua applicazione. Source Clicca qui
Windows espone un'ampia gamma di API. Per ottenere informazioni su varie API puoi controllare siti come pinvoke .
Funzione di importazione da 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
è definita:
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));
}
}
Vedi Calling convenzioni e manomissione dei nomi in C ++ per spiegazioni sul perché extern "C"
e __stdcall
siano necessari.
Trovare la libreria dinamica
Quando viene invocato per la prima volta il metodo extern, il programma C # cercherà e caricherà la DLL appropriata. Per ulteriori informazioni su dove viene eseguita la ricerca per trovare la DLL e su come è possibile influenzare le posizioni di ricerca, consultare questa domanda StackOverflow .
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;
}
}
}
Mancanza di nomi in C ++
I compilatori C ++ codificano informazioni aggiuntive nei nomi delle funzioni esportate, come i tipi di argomenti, per rendere possibili sovraccarichi con argomenti diversi. Questo processo è chiamato mangling del nome . Ciò causa problemi con l'importazione di funzioni in C # (e l'interoperabilità con altre lingue in generale), poiché il nome della funzione int add(int a, int b)
non viene 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 della manomissione dei nomi:
Esportare funzioni usando
extern "C"
per passare al collegamento esterno C che utilizza il nome C mangling:extern "C" __declspec(dllexport) int __stdcall add(int a, int b)
[DllImport("myDLL.dll")]
Il nome della funzione sarà ancora
_add@8
(_add@8
), maStdCall
+extern "C"
nome mangling è riconosciuto dal compilatore C #.Specifica dei nomi delle funzioni esportate nel file di definizione del modulo
myDLL.def
:EXPORTS add
int __stdcall add(int a, int b)
[DllImport("myDLL.dll")]
In questo caso, il nome della funzione sarà pura
add
.Importazione del nome mutilato. Avrai bisogno di un visualizzatore DLL per vedere il nome storpiato, quindi puoi specificarlo esplicitamente:
__declspec(dllexport) int __stdcall add(int a, int b)
[DllImport("myDLL.dll", EntryPoint = "?add@@YGHHH@Z")]
Chiamare convenzioni
Esistono diverse convenzioni delle funzioni di chiamata, che specificano chi (chiamante o chiamato) apre gli argomenti dallo stack, come vengono passati gli argomenti e in quale ordine. C ++ utilizza la convenzione di chiamata Cdecl
per impostazione predefinita, ma C # si aspetta che StdCall
venga utilizzato di solito dalle API di Windows. Devi cambiare uno o l'altro:
Modifica convenzione di chiamata a
StdCall
in C ++:extern "C" __declspec(dllexport) int __stdcall add(int a, int b)
[DllImport("myDLL.dll")]
In alternativa, modifica la convenzione di
Cdecl
inCdecl
in C #:extern "C" __declspec(dllexport) int /*__cdecl*/ add(int a, int b)
[DllImport("myDLL.dll", CallingConvention = CallingConvention.Cdecl)]
Se si desidera utilizzare una funzione con convenzione di chiamata Cdecl
e un nome storpiato, il 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 ) è usato principalmente in funzioni che sono membri di una classe.
Quando una funzione utilizza thiscall ( __thiscall ), un puntatore alla classe viene passato come primo parametro.
Caricamento e scaricamento dinamico di DLL non gestite
Quando si utilizza l'attributo DllImport
è necessario conoscere la dll corretta e il nome del metodo in fase di compilazione . Se si desidera essere più flessibili e decidere in fase di esecuzione quali DLL e metodi da caricare, è possibile utilizzare i metodi dell'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 utilizzando Marshal.GetDelegateForFunctionPointer()
.
Il seguente esempio di codice lo dimostra con myDLL.dll
degli 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 Win32
Quando si utilizzano i metodi di interoperabilità, è possibile utilizzare l'API GetLastError per ottenere ulteriori informazioni sulle chiamate API dell'utente.
Attributo DllImport Attributo SetLastError
SetLastError = true
Indica che il callee chiamerà SetLastError (funzione API Win32).
SetLastError = false
Indica che il chiamato non chiamerà SetLastError (funzione API Win32), quindi non si otterranno informazioni di errore.
Quando SetLastError non è impostato, è impostato su false (valore predefinito).
È possibile ottenere il codice di errore utilizzando Marshal.GetLastWin32Error Method:
Esempio:
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr OpenMutex(uint access, bool handle, string lpName);
Se si sta tentando di aprire il 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 del sistema possono essere trovati qui:
https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx
API GetLastError
Esiste un'API GetLastError nativa che puoi usare anche:
[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 chiamata Win32 che imposta l'errore (chiama SetLastError), il CLR può chiamare anche altre chiamate Win32 che potrebbero chiamare SetLastError , questo comportamento può ignorare il valore dell'errore. In questo scenario, se si chiama GetLastError è possibile ottenere un errore non valido.
Impostazione SetLastError = true , assicura che il CLR recuperi il codice di errore prima di eseguire altre chiamate Win32.
Oggetto appuntato
GC (Garbage Collector) è responsabile della pulizia della nostra spazzatura.
Mentre GC pulisce la nostra spazzatura, rimuove gli oggetti non utilizzati dall'heap gestito che causano la frammentazione dell'heap. Quando GC ha terminato la rimozione, esegue una compressione heap (defragmintation) che implica lo spostamento di oggetti nell'heap.
Dato che GC non è deterministico, quando passa il riferimento / puntatore dell'oggetto gestito al codice nativo, GC può dare il via in qualsiasi momento, se si verifica subito dopo la chiamata Inerop, c'è una buona possibilità che l'oggetto (il riferimento passato a nativo) possa essere spostati sull'heap gestito - di conseguenza, otteniamo un riferimento non valido sul lato gestito.
In questo scenario, è necessario bloccare l'oggetto prima di passarlo al codice nativo.
Oggetto appuntato
L'oggetto appuntato è un oggetto che non è autorizzato a spostare da GC.
Maniglia appuntata Gc
Puoi creare un oggetto pin usando il metodo Gc.Alloc
GCHandle handle = GCHandle.Alloc(yourObject, GCHandleType.Pinned);
- Ottenere un GCHandle aggiunto all'oggetto gestito contrassegna un oggetto specifico come uno che non può essere spostato da GC , fino a liberare l'handle
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 immobilizza (specialmente oggetti 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 ad esempio finaly)
Leggere strutture con Maresciallo
La classe Marshal contiene una funzione denominata PtrToStructure , questa funzione ci dà la possibilità di leggere le strutture con un puntatore non gestito.
La funzione PtrToStructure ha molti sovraccarichi, ma tutti hanno la stessa intenzione.
PtrToStructure generico:
public static T PtrToStructure<T>(IntPtr ptr);
T - tipo di struttura.
ptr - 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 appuntare 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;
}