Chiamata a funzioni native da codice gestito

Common Language Runtime fornisce Platform Invocation Services o PInvoke, che consente al codice gestito di chiamare funzioni in stile C in librerie native collegate dinamiche (DLL). Lo stesso marshalling dei dati viene usato per l'interoperabilità COM con il runtime e per il meccanismo "It Just Works" o IJW.

Per altre informazioni, vedi:

Gli esempi in questa sezione illustrano semplicemente come PInvoke usare. PInvoke può semplificare il marshalling dei dati personalizzato perché si forniscono informazioni di marshalling dichiarative negli attributi anziché scrivere codice di marshalling procedurale.

Nota

La libreria di marshalling offre un modo alternativo per effettuare il marshalling dei dati tra ambienti nativi e gestiti in modo ottimizzato. Per altre informazioni sulla libreria di marshalling, vedere Panoramica del marshalling in C++ . La libreria di marshalling è utilizzabile solo per i dati e non per le funzioni.

PInvoke e l'attributo DllImport

L'esempio seguente illustra l'uso di PInvoke in un programma Visual C++. La funzione nativa viene definita in msvcrt.dll. L'attributo DllImport viene usato per la dichiarazione di inserisce.

// platform_invocation_services.cpp
// compile with: /clr
using namespace System;
using namespace System::Runtime::InteropServices;

[DllImport("msvcrt", CharSet=CharSet::Ansi)]
extern "C" int puts(String ^);

int main() {
   String ^ pStr = "Hello World!";
   puts(pStr);
}

L'esempio seguente equivale all'esempio precedente, ma usa IJW.

// platform_invocation_services_2.cpp
// compile with: /clr
using namespace System;
using namespace System::Runtime::InteropServices;

#include <stdio.h>

int main() {
   String ^ pStr = "Hello World!";
   char* pChars = (char*)Marshal::StringToHGlobalAnsi(pStr).ToPointer();
   puts(pChars);

   Marshal::FreeHGlobal((IntPtr)pChars);
}

Vantaggi di IJW

  • Non è necessario scrivere DLLImport dichiarazioni di attributo per le API non gestite usate dal programma. È sufficiente includere il file di intestazione e il collegamento alla libreria di importazione.

  • Il meccanismo IJW è leggermente più veloce (ad esempio, gli stub IJW non devono verificare la necessità di aggiungere o copiare elementi di dati perché vengono eseguiti in modo esplicito dallo sviluppatore).

  • Illustra chiaramente i problemi di prestazioni. In questo caso, il fatto che si sta convertendo da una stringa Unicode a una stringa ANSI e che si dispone di un'allocazione di memoria e deallocazione dell'operatore. In questo caso, uno sviluppatore che scrive il codice usando IJW si renderebbe conto che la chiamata _putws e l'uso PtrToStringChars sarebbero migliori per le prestazioni.

  • Se si chiamano molte API non gestite usando gli stessi dati, effettuarne il marshalling una sola volta e passare la copia con marshalling è molto più efficiente del marshalling ogni volta.

Svantaggi di IJW

  • Il marshalling deve essere specificato in modo esplicito nel codice anziché in base agli attributi (che spesso hanno le impostazioni predefinite appropriate).

  • Il codice di marshalling è inline, in cui è più invasivo nel flusso della logica dell'applicazione.

  • Poiché le API di marshalling esplicite restituiscono IntPtr tipi per la portabilità da 32 bit a 64 bit, è necessario usare chiamate aggiuntive ToPointer .

Il metodo specifico esposto da C++ è il metodo più efficiente ed esplicito, a costo di una maggiore complessità.

Se l'applicazione usa principalmente tipi di dati non gestiti o se chiama più API non gestite rispetto alle API .NET Framework, è consigliabile usare la funzionalità IJW. Per chiamare un'API occasionale non gestita in un'applicazione principalmente gestita, la scelta è più sottile.

PInvoke con LE API di Windows

PInvoke è utile per chiamare le funzioni in Windows.

In questo esempio un programma Visual C++ interagisce con la funzione MessageBox che fa parte dell'API Win32.

// platform_invocation_services_4.cpp
// compile with: /clr /c
using namespace System;
using namespace System::Runtime::InteropServices;
typedef void* HWND;
[DllImport("user32", CharSet=CharSet::Ansi)]
extern "C" int MessageBox(HWND hWnd, String ^ pText, String ^ pCaption, unsigned int uType);

int main() {
   String ^ pText = "Hello World! ";
   String ^ pCaption = "PInvoke Test";
   MessageBox(0, pText, pCaption, 0);
}

L'output è una finestra di messaggio con il titolo PInvoke Test e contiene il testo Hello World!.

Le informazioni di marshalling vengono usate anche da PInvoke per cercare le funzioni nella DLL. In user32.dll non esiste infatti alcuna funzione MessageBox, ma CharSet=CharSet::Ansi consente a PInvoke di usare MessageBoxA, la versione ANSI, anziché MessageBoxW, che è la versione Unicode. In generale, è consigliabile usare versioni Unicode di API non gestite perché elimina il sovraccarico della traduzione dal formato Unicode nativo degli oggetti stringa .NET Framework ad ANSI.

Quando non usare PInvoke

L'uso di PInvoke non è appropriato per tutte le funzioni in stile C nelle DLL. Si supponga, ad esempio, che sia presente una funzione MakeSpecial in mylib.dll dichiarata come segue:

char * MakeSpecial(char * pszString);

Se si usa PInvoke in un'applicazione Visual C++, è possibile scrivere qualcosa di simile al seguente:

[DllImport("mylib")]
extern "C" String * MakeSpecial([MarshalAs(UnmanagedType::LPStr)] String ^);

La difficoltà è che non è possibile eliminare la memoria per la stringa non gestita restituita da MakeSpecial. Altre funzioni chiamate tramite PInvoke restituiscono un puntatore a un buffer interno che non deve essere deallocato dall'utente. In questo caso, l'uso della funzionalità IJW è la scelta ovvia.

Limitazioni di PInvoke

Non è possibile restituire lo stesso puntatore esatto da una funzione nativa presa come parametro. Se una funzione nativa restituisce il puntatore di cui è stato effettuato il marshalling da PInvoke, è possibile che si verifichino danneggiamenti della memoria ed eccezioni.

__declspec(dllexport)
char* fstringA(char* param) {
   return param;
}

L'esempio seguente mostra questo problema e, anche se il programma potrebbe sembrare fornire l'output corretto, l'output proviene dalla memoria liberata.

// platform_invocation_services_5.cpp
// compile with: /clr /c
using namespace System;
using namespace System::Runtime::InteropServices;
#include <limits.h>

ref struct MyPInvokeWrap {
public:
   [ DllImport("user32.dll", EntryPoint = "CharLower", CharSet = CharSet::Ansi) ]
   static String^ CharLower([In, Out] String ^);
};

int main() {
   String ^ strout = "AabCc";
   Console::WriteLine(strout);
   strout = MyPInvokeWrap::CharLower(strout);
   Console::WriteLine(strout);
}

Marshalling di argomenti

Con PInvoke, non è necessario effettuare il marshalling tra i tipi primitivi nativi gestiti e C++ con lo stesso formato. Ad esempio, non è necessario effettuare il marshalling tra Int32 e int o tra Double e Double.

Tuttavia, è necessario effettuare il marshalling dei tipi che non hanno lo stesso formato. Sono inclusi i tipi char, string e struct. La tabella seguente illustra i mapping usati dal gestore di marshalling per vari tipi:

wtypes.h Visual C++ Visual C++ con /clr Common Language Runtime
HANDLE void* void* IntPtr, UIntPtr
BYTE char senza segno char senza segno Byte
SHORT short short Int16
WORD short senza segno short senza segno UInt16
INT int int Int32
UINT unsigned int unsigned int UInt32
LONG long long Int32
BOOL long bool Booleano
DWORD long senza segno long senza segno UInt32
ULONG long senza segno long senza segno UInt32
CHAR char char Char
LPSTR char * Stringa ^ [in], StringBuilder ^ [in, out] Stringa ^ [in], StringBuilder ^ [in, out]
LPCSTR const char* String^ String
LPWSTR wchar_t * Stringa ^ [in], StringBuilder ^ [in, out] Stringa ^ [in], StringBuilder ^ [in, out]
LPCWSTR const wchar_t* String^ String
FLOAT float float Singolo
DOUBLE double double Double

Il gestore di marshalling aggiunge automaticamente la memoria allocata nell'heap di runtime se il relativo indirizzo viene passato a una funzione non gestita. L'aggiunta impedisce al Garbage Collector di spostare il blocco di memoria allocato durante la compattazione.

Nell'esempio illustrato in precedenza in questo argomento, il parametro CharSet di DllImport specifica come eseguire il marshalling delle stringhe gestite; in questo caso, devono essere sottoposto a marshalling in stringhe ANSI per il lato nativo.

È possibile specificare le informazioni di marshalling per singoli argomenti di una funzione nativa usando l'attributo MarshalAs. Per effettuare il marshalling di un argomento String * sono disponibili diverse opzioni: BStr, ANSIBStr, TBStr, LPStr, LPWStr e LPTStr. Il valore predefinito è LPStr.

In questo esempio la stringa viene sottoposta a marshalling come stringa di caratteri Unicode a byte doppio, LPWStr. L'output è la prima lettera di Hello World! poiché il secondo byte della stringa con marshalling è Null e lo interpreta come marcatore di fine stringa.

// platform_invocation_services_3.cpp
// compile with: /clr
using namespace System;
using namespace System::Runtime::InteropServices;

[DllImport("msvcrt", EntryPoint="puts")]
extern "C" int puts([MarshalAs(UnmanagedType::LPWStr)] String ^);

int main() {
   String ^ pStr = "Hello World!";
   puts(pStr);
}

L'attributo MarshalAs si trova nello spazio dei nomi System::Runtime::InteropServices. L'attributo può essere usato con altri tipi di dati, ad esempio matrici.

Come accennato in precedenza nell'argomento, la libreria di marshalling fornisce un nuovo metodo ottimizzato per il marshalling dei dati tra ambienti nativi e gestiti. Per altre informazioni, vedere Panoramica del marshalling in C++.

Considerazioni sulle prestazioni

PInvoke ha un sovraccarico compreso tra 10 e 30 istruzioni x86 per chiamata. Oltre a questo costo fisso, il marshalling crea un sovraccarico aggiuntivo. Non esiste alcun costo di marshalling tra i tipi copiabili blttable con la stessa rappresentazione nel codice gestito e non gestito. Ad esempio, non è previsto alcun costo per la conversione tra int e Int32.

Per prestazioni migliori, avere un minor numero di chiamate PInvoke che effettua il marshalling del maggior numero possibile di dati, invece di più chiamate che escludono meno dati per ogni chiamata.

Vedi anche

Interoperabilità .NET e nativa