Appel à des fonctions natives à partir de code managé
Le Common Language Runtime fournit des services d’appel de plateforme, ou PInvoke, qui permet au code managé d’appeler des fonctions de style C dans des bibliothèques dynamiques natives liées (DLL). Le même marshaling de données est utilisé pour l’interopérabilité COM avec le runtime et pour le mécanisme « It Just Works », ou IJW.
Pour plus d’informations, consultez l’article suivant :
Les exemples de cette section illustrent simplement comment PInvoke
être utilisés. PInvoke
peut simplifier le marshaling de données personnalisé, car vous fournissez des informations de marshaling de manière déclarative dans les attributs au lieu d’écrire du code de marshaling procédural.
Remarque
La bibliothèque de marshaling offre une autre façon de marshaler les données entre les environnements natifs et managés de manière optimisée. Pour plus d’informations sur la bibliothèque de marshaling, consultez Vue d’ensemble du marshaling en C++ . La bibliothèque de marshaling est utilisable uniquement pour les données, et non pour les fonctions.
PInvoke et l’attribut DllImport
L’exemple suivant montre l’utilisation dans PInvoke
un programme Visual C++. La fonction native place est définie dans msvcrt.dll. L’attribut DllImport est utilisé pour la déclaration de mets.
// 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’exemple suivant équivaut à l’exemple précédent, mais utilise 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);
}
Avantages de l’IJW
Il n’est pas nécessaire d’écrire
DLLImport
des déclarations d’attribut pour les API non managées utilisées par le programme. Incluez simplement le fichier d’en-tête et le lien avec la bibliothèque d’importation.Le mécanisme IJW est légèrement plus rapide (par exemple, les stubs IJW n’ont pas besoin de vérifier la nécessité d’épingler ou de copier des éléments de données, car cela est fait explicitement par le développeur).
Il illustre clairement les problèmes de performances. Dans ce cas, le fait que vous traduisez d’une chaîne Unicode en chaîne ANSI et que vous disposez d’une allocation et d’une désallocation de mémoire standard. Dans ce cas, un développeur écrivant le code à l’aide de IJW se rend compte que l’appel
_putws
et l’utilisationPtrToStringChars
seraient mieux adaptés aux performances.Si vous appelez de nombreuses API non managées à l’aide des mêmes données, le marshaling une fois et le passage de la copie marshalée est beaucoup plus efficace que le re marshaling à chaque fois.
Inconvénients de l’IJW
Le marshaling doit être spécifié explicitement dans le code au lieu d’attributs (qui ont souvent des valeurs par défaut appropriées).
Le code de marshaling est inline, où il est plus envahissant dans le flux de la logique d’application.
Étant donné que les API de marshaling explicites retournent des types de portabilité
IntPtr
32 bits à 64 bits, vous devez utiliser des appels supplémentairesToPointer
.
La méthode spécifique exposée par C++ est la méthode plus efficace, explicite, au coût d’une complexité supplémentaire.
Si l’application utilise principalement des types de données non managés ou si elle appelle plus d’API non managées que les API .NET Framework, nous vous recommandons d’utiliser la fonctionnalité IJW. Pour appeler une API occasionnelle non managée dans une application principalement managée, le choix est plus subtil.
PInvoke avec les API Windows
PInvoke est pratique pour appeler des fonctions dans Windows.
Dans cet exemple, un programme Visual C++ interopére avec la fonction MessageBox qui fait partie de l’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);
}
La sortie est une boîte de message qui a le titre PInvoke Test et contient le texte Hello World !.
Les informations de marshaling sont également utilisées par PInvoke pour rechercher des fonctions dans la DLL. Dans user32.dll il n’existe en fait aucune fonction MessageBox, mais CharSet=CharSet ::Ansi permet à PInvoke d’utiliser MessageBoxA, la version ANSI, au lieu de MessageBoxW, qui est la version Unicode. En général, nous vous recommandons d’utiliser des versions Unicode d’API non managées, car cela élimine la surcharge de traduction du format Unicode natif des objets de chaîne .NET Framework vers ANSI.
Quand ne pas utiliser PInvoke
L’utilisation de PInvoke n’est pas appropriée pour toutes les fonctions de style C dans les DLL. Par exemple, supposons qu’il existe une fonction MakeSpecial dans mylib.dll déclarée comme suit :
char * MakeSpecial(char * pszString);
Si nous utilisons PInvoke dans une application Visual C++, nous pouvons écrire quelque chose de similaire à ce qui suit :
[DllImport("mylib")]
extern "C" String * MakeSpecial([MarshalAs(UnmanagedType::LPStr)] String ^);
La difficulté ici est que nous ne pouvons pas supprimer la mémoire de la chaîne non managée retournée par MakeSpecial. D’autres fonctions appelées via PInvoke retournent un pointeur vers une mémoire tampon interne qui n’a pas besoin d’être libérée par l’utilisateur. Dans ce cas, l’utilisation de la fonctionnalité IJW est le choix évident.
Limitations de PInvoke
Vous ne pouvez pas retourner le même pointeur exact à partir d’une fonction native que vous avez prise en tant que paramètre. Si une fonction native retourne le pointeur qui a été marshalé par PInvoke, la corruption de la mémoire et les exceptions peuvent entraîner.
__declspec(dllexport)
char* fstringA(char* param) {
return param;
}
L’exemple suivant présente ce problème et même si le programme peut sembler donner la sortie correcte, la sortie provient de la mémoire libérée.
// 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);
}
Marshaling Arguments
Avec PInvoke
, aucun marshaling n’est nécessaire entre les types primitifs natifs managés et C++ avec le même formulaire. Par exemple, aucun marshaling n’est requis entre Int32 et int, ou entre Double et double.
Toutefois, vous devez marshaler les types qui n’ont pas le même formulaire. Cela inclut les types char, chaîne et struct. Le tableau suivant présente les mappages utilisés par le marshaleur pour différents types :
wtypes.h | Visual C++ | Visual C++ avec /clr | Common Language Runtime |
---|---|---|---|
HANDLE | void* | void* | IntPtr, UIntPtr |
BYTE | unsigned char | unsigned char | Byte |
SHORT | short | short | Int16 |
WORD | unsigned short | unsigned short | UInt16 |
INT | int | int | Int32 |
UINT | nombre entier non signé | nombre entier non signé | UInt32 |
LONG | long | long | Int32 |
BOOL | long | bool | Boolean |
DWORD | unsigned long | unsigned long | UInt32 |
ULONG | unsigned long | unsigned long | UInt32 |
CHAR | char | char | Char |
LPSTR | char * | String ^ [in], StringBuilder ^ [in, out] | String ^ [in], StringBuilder ^ [in, out] |
LPCSTR | const char* | String^ | Chaîne |
LPWSTR | wchar_t * | String ^ [in], StringBuilder ^ [in, out] | String ^ [in], StringBuilder ^ [in, out] |
LPCWSTR | const wchar_t* | String^ | Chaîne |
FLOAT | virgule flottante | virgule flottante | Unique |
DOUBLE | double | double | Double |
Le marshaleur épingle automatiquement la mémoire allouée sur le tas d’exécution si son adresse est passée à une fonction non managée. L’épinglage empêche le garbage collector de déplacer le bloc de mémoire alloué pendant le compactage.
Dans l’exemple présenté précédemment dans cette rubrique, le paramètre CharSet de DllImport spécifie la façon dont les chaînes managées doivent être marshalées ; dans ce cas, ils doivent être marshalés en chaînes ANSI pour le côté natif.
Vous pouvez spécifier des informations de marshaling pour des arguments individuels d’une fonction native à l’aide de l’attribut MarshalAs. Il existe plusieurs choix pour marshaler un argument String * : BStr, ANSIBStr, TBStr, LPStr, LPWStr et LPTStr. La valeur par défaut est LPStr.
Dans cet exemple, la chaîne est marshalée en tant que chaîne de caractères Unicode double octet, LPWStr. La sortie est la première lettre de Hello World ! étant donné que le deuxième octet de la chaîne marshalée est null et le place comme marqueur de fin de chaîne.
// 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’attribut MarshalAs se trouve dans l’espace de noms System ::Runtime ::InteropServices. L’attribut peut être utilisé avec d’autres types de données tels que des tableaux.
Comme mentionné précédemment dans la rubrique, la bibliothèque de marshaling fournit une nouvelle méthode optimisée de marshaling des données entre des environnements natifs et managés. Pour plus d’informations, consultez Vue d’ensemble du marshaling en C++.
Considérations relatives aux performances
PInvoke a une surcharge comprise entre 10 et 30 instructions x86 par appel. En plus de ce coût fixe, le marshaling crée une surcharge supplémentaire. Il n’existe aucun coût de marshaling entre les types blittables qui ont la même représentation dans le code managé et non managé. Par exemple, il n’y a aucun coût à traduire entre int et Int32.
Pour de meilleures performances, vous avez moins d’appels PInvoke qui marshalent autant de données que possible, au lieu d’autres appels qui marshalent moins de données par appel.