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 en savoir plus, consultez :

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 case activée pour 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’utilisation PtrToStringChars 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émentaires ToPointer .

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 Booléen
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^ String
LPWSTR wchar_t * String ^ [in], StringBuilder ^ [in, out] String ^ [in], StringBuilder ^ [in, out]
LPCWSTR const wchar_t* String^ String
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, To Str, 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.

Voir aussi

Interopérabilité native et .NET