Dans le cadre de vos développements .NET, vous êtes amené à utiliser les méthodes que le Framework met à votre disposition. Mais, lors de réalisations plus complexes,
vous pouvez être amené à utiliser des méthodes qui se trouvent dans des DLL (Dynamic Link Library) qui n'ont pas été écrites en .NET. Pour pallier à ce problème, on utilise le mécanisme de : P/Invoke (Platform Invoke).
Bien que créé pour utiliser des DLL standards, vous pouvez tout à fait utiliser P/Invoke pour appeler les méthodes de vos propres DLL.
P/Invoke permet aussi d'écrire du code non managé afin d'empêcher la réflexion du code IL.
II. Utilisation
A. Explications
Toutes les classes qui vous permettent d'utiliser P/Invoke se trouvent dans l'espace de nom System.Runtime.InteropServices (Vous devez donc penser à faire un
using ou Import, selon le langage de programmation que vous utilisez, pour pouvoir utiliser ces classes).
Ensuite, vous devez déclarer, dans votre classe, la fonction que vous souhaitez utiliser. Cette déclaration doit suivre une certaine méthodologie:
La déclaration de la fonction doit être précédée de l'attribut DllImport, qui indique le nom de le DLL,
Les mots-clés static et extern (pour indiquer que la méthode à utiliser provient d'une source extérieure) doivent être utilisés,
Le nom de la fonction doit être le même que celui spécifié dans la DLL
Chaque argument doit avoir un nom
Vous devez connaître le prototype de la méthode que vous désirez implémenter
Etant donné que cela peut paraître abstrait, la section suivante vous montrera un exemple.
B. Exemples
Le programme suivant vous montrera un exemple d'utilisation de l'attribut DllImport, pour utiliser une méthode contenue dans le fichier user32.dll.
La méthode que nous allons appeler est la méthode FlashWindow, dont le but est de faire clignoter une fenêtre (de la même façon qu'une fenêtre MSN Messenger par exemple).
Utilisation de DllImport
// Déclaration des usingusing System;
using System.Runtime.InteropServices;
class FenetreClignotante
{
// Déclaration de la méthode à utiliser
[ DllImport("user32") ]
staticexternint FlashWindow
(
int hwnd,
int bInvert
);
privateint hWND;
// Point d'entrée de l'applicationstaticvoid Main(string[] args)
{
hWND = (int)this.Handle;
// Utilisation de la méthode
FlashWindow(this.hWND, 1);
}
}
Il est parfois possible que vous ne puissiez pas spécifier le nom de la méthode définie, car l'une de vos fonctions porte déjà ce même nom.
Dans ce cas, vous avez deux possibilités:
Changer le nom de votre méthode, et remplacer tous les appels de votre méthodes avec le nouveau nom (ce qui peut parfois s'avérer long....)
Changer le nom de la fonction implémentée dans la DLL native
Mettre votre classe dans un de vos namespace
Dans le deuxième cas, vous devrez utiliser la propriété optionnelle EntryPoint (Point d'entrée) pour indiquer le nom de la méthode à charger.
Un exemple étant toujours plus parlant que des mots, voici de quoi je veux parler
Utilisation de l'attribut EntryPoint
// Déclaration des usingusing System;
using System.Runtime.InteropServices;
// Déclaration de la méthode à utiliser
[ DllImport("user32.dll", EntryPoint="FlashWindow") ]
staticexternint MonClignotement
(
int hwnd,
int bInvert
);
privateint hWND;
// Point d'entrée de l'applicationstaticvoid Main(string[] args)
{
hWND = (int)this.Handle;
// Utilisation de la méthode
MonClignotement(this.hWND, 1);
}
Une autre propriété qu'il est intéressant d'utiliser est SetLastError. En effet, lorsque vous programmez, il est important de penser aux différentes
erreurs qui peuvent survenir (la plupart des DLL étant écrites en C, elles ne génèrent pas d'exceptions mais renvoient des codes d'erreurs). Pour pouvoir intercepter ces erreurs lorsque vous travaillez avec P/Invoke, vous devez mettre à true (vrai) cette propriété, afin de pouvoir
récupérer l'éventuelle exception qui pourrait survenir, au moyen de la méthode GetLastWin32Error (GetLastWin32Error est définit dans System.Runtime.InteropServices.Marshal).
Voici un exemple pour illustrer cette propriété:
Utilisation de SetLastError et GetLastWin32Error
// Nous utiliserons ici la méthode Beep de la DLL kernel32.dll
// Déclaration des usingusing System;
using System.Runtime.InteropServices;
class FenetreClignotante
{
// Déclaration de la méthode à utiliser
[ DllImport("kernel32", SetLastError=true) ]
staticexternbool Beep
(
uint iFreq,
uint iDuration
);
// Point d'entrée de l'applicationstaticvoid Main(string[] args)
{
if ( !Beep(100, 100) )
{
Int32 err = Marshal.GetLastWin32Error();
thrownew Win32Exception(err);
}
}
}
GetLastWin32Error() nous permet donc de récupérer le code d'erreur de l'exception. Vous pouvez voir à quoi correspond chaque code d'erreur à cette adresse: Liste des codes d'erreur.
Néanmoins, il existe deux techniques pour pouvoir récupérer directement la description correspondant au code d'erreur:
Vous pouvez utiliser l'API FormatMessage, ce qui est assez fastidieux (vous devrez utiliser une API pour comprendre l'erreur générer par une autre API...)
Vous pouvez également utiliser une méthode que j'ai trouvée ici
La deuxième méthode est simple: dans votre code, remplacez:
string errorMessage = new Win32Exception(Marshal.GetLastWin32Error()).Message;
errorMessage contient alors le message d'erreur, plutôt que le code d'erreur.
Si vous tenez tout de même à utiliser l'API FormatMessage voici une classe fournit par abelman et
qui vous permet de récupérer le message d'une DLL à partir du code d'erreur:
Dans nos exemples, le prototype de FlashWindow est relativement simple, car les types utilisés sont les mêmes en .NET et en Win32. Celui de Beep par contre est un peu plus complexe,
car les types des arguments de la méthode n'existent pas en .NET. Ce sont des types Win32. Si vous essayez de passer en paramètre un entier, alors que la méthode attend un LPSTR ou un Short,
vous aurez une erreur à la compilation. Pour éviter cela, il existe un tableau de conversion des types Win32 vers les types .NET:
Utiliser le passage d'arguments par pointeurs peut représenter un gros risque si vous n'avez pas bien saisi le fonctionnement du Garbage Collector (Ramasse Miettes).
En effet, ce ramasse miette est "l'outil" du framework .NET qui est chargé de libérer les objets qui ne sont plus nécessaires dans votre application: il est donc chargé de déplacer des objets en mémoire.
Etant donné que son passage est aléatoire, vous prenez le risque, si vous passez l'adresse d'un objet par l'intermédiaire d'un pointeur, que le Garbage Collector déplace cet objet en mémoire.
Votre pointeur sera alors invalide car il ne pointera plus vers l'objet. Ce résultat entraînera alors des bugs ou bien des plantages de votre application.
b. La solution
Pour éviter que cela ne se produise, vous devez fixer les objets en mémoire. Cela peut être fait au moyen du mot-clé fixed. Le Garbage Collector saura alors qu'il
ne doit pas bouger, en mémoire, les instances de ces objets.
Un exemple de code vous permettra de mieux comprendre de quoi il retourne: nous utiliserons la méthode ReadFile, qui doit être utilisée après un appel à la méthode CreateFile.
Pour information, ces méthodes peuvent être utilisées pour envoyer et lire des données sur les ports COM.
// Fonction ReadFile
[ DllImport("kernel32") ]
publicstaticexternbool ReadFile
(
IntPtr hFile,
void *lpBuffer,
uint NbOctevtALire,
uint *lpNbOctetsLus,
IntPtr lpOverlapped
);
...
int handle = ... // Appel à la fonction CreateFileuint NbOctectsLus = 0;
byte [] Buffer = new Byte[1024];
fixed( byte* pBuffer = Buffer )
{
bool b = ReadFile(handle, pBuffer, 1024, &NbOctectsLus, 0);
}
.....
Notez que la fonction ReadFile renvoie un booléen, qui indique si la méthode s'est bien déroulée ou non.
Une petite astuce pour savoir si un argument doit être passé par valeur ou par référence: si, dans son prototype, le type d'un paramètre commence par la lettre P
ou par les lettres LP, alors vous devez passer ce paramètre par référence.
V. Passage de chaînes de caractères
Travailler avec des données textuelles est chose commune. Dans ce cas, la première idée à laquelle on pourrait penser serait d'utiliser une instance de la classe
String pour travailler sur les paramètres de la méthode.
Néanmoins, les choses ne sont pas si simples: en effet, les instances de la classe String étant immuables, vous devez d'abord déterminer si le paramètre est en entrée ou en sortie.
Si le paramètre est en sortie (ou entrée et sortie), vous devez utiliser la classe StringBuilder.
Afin de déterminer le "sens" du paramètre texte, vous avez deux possibilités:
Premièrement, vous pouvez essayer de comprendre le but du paramètre. En effet, si vous avez un paramètre ayant par exemple le nom de SetTexte, vous pouvez supposer que ce paramètre en en entrée uniquement. A l'inverse, un paramètre du nom de GetTexte fait plutôt penser à un pramètre de sortie. Bien que fonctionnant, cette technique n'est pas sur à 100% (que faire par exemple si le paramètre porte le nom de Toto ?), et n'est pas très "professionnelle"
L'autre méthode consiste à regarder le nom du type de paramètre. Si celui-ci contient un C (par exemple LTCTSTR), le paramètre sera en entrée. Dans le cas contraire, le paramètre sera en sortie.
Vous avez également la possibilité d'utiliser l'énumération System.Runtime.InteropServices.CharSet, afin d'indiquer le type d'encodage utilisé par la chaîne de caractères.
Il existe deux types d'encodage:
Unicode
ANSI
La plupart des paramètres étant de l'un ou l'autre de ces deux types, le framework .NET vous offre la possibilité de déterminer automatiquement l'encodage utilisé,
si vous utilisez CharSet.Auto.
Voici un exemple de code, afin de vous faire comprendre un peu mieux ces principes:
Passage de chaîne de caractères : Exemple avec un paramètre de sortie
// Déclaration des using
using System;
using System.Runtime.InteropServices;
class FenetreClignotante
{
// Déclaration de la méthode à utiliser
[ DllImport("kernel32", CharSet=CharSet.Auto) ]
publicstaticexternuint GetCurrentDirectory
(
uint Taille,
StringBuilder sTmp
);
// Point d'entrée de l'applicationstaticvoid Main(string[] args)
{
uint Taille = 255;
StringBuilder sTmp = new StringBuilder((int)Taille);
uint i = GetCurrentDirectory(Taille, sTmp);
Console.WriteLine(sTmp);
}
}
Passage de chaîne de caractères : Exemple avec un paramètre d'entrée et un paramètre de sortie
// ** Documentation for Win32 GetShortPathName() API Function
// DWORD GetShortPathName(
// LPCTSTR lpszLongPath, // file for which to get short path
// LPTSTR lpszShortPath, // short path name (output)
// DWORD cchBuffer // size of output buffer
// );
[ DllImport("Kernel32", CharSet = CharSet.Auto) ]
staticextern Int32 GetShortPathName
(
String path, // input string
StringBuilder shortPath, // output string
Int32 shortPathLength); // StringBuilder.Capacity
)
VI. Conclusion
Cet article vous a donc fourni une bonne approche des Dll natives et de leurs utilisations dans vos applications .NET. Il ne vous reste plus qu'à tenter vous-même l'expérience ;)
Et j'ajouterai juste un dernier mot, pour dire aux développeurs d'applications mobiles, qu'utiliser P/Invoke sur les périphériques embarqués (Pocket PC, SmartPhone, etc..) est tout à fait possible,
comme le démontre cette source : Contrôler la LED de votre Pocket PC.