I. Introduction▲
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▲
II-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 la 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.
Étant donné que cela peut paraître abstrait, la section suivante vous montrera un exemple.
II-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).
// Déclaration des using
using
System;
using
System.
Runtime.
InteropServices;
class
FenetreClignotante
{
// Déclaration de la méthode à utiliser
[ DllImport(
"user32"
) ]
static
extern
int
FlashWindow
(
int
hwnd,
int
bInvert
);
private
int
hWND;
// Point d'entrée de l'application
static
void
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éthode 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
// Déclaration des using
using
System;
using
System.
Runtime.
InteropServices;
// Déclaration de la méthode à utiliser
[ DllImport(
"user32.dll"
, EntryPoint=
"FlashWindow"
) ]
static
extern
int
MonClignotement
(
int
hwnd,
int
bInvert
);
private
int
hWND;
// Point d'entrée de l'application
static
void
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éfini dans System.Runtime.InteropServices.Marshal).
Voici un exemple pour illustrer cette propriété :
// Nous utiliserons ici la méthode Beep de la DLL kernel32.dll
// Déclaration des using
using
System;
using
System.
Runtime.
InteropServices;
class
FenetreClignotante
{
// Déclaration de la méthode à utiliser
[ DllImport(
"kernel32"
, SetLastError=true) ]
static
extern
bool
Beep
(
uint
iFreq,
uint
iDuration
);
// Point d'entrée de l'application
static
void
Main
(
string
[]
args)
{
if
(
!
Beep
(
100
,
100
) )
{
Int32 err =
Marshal.
GetLastWin32Error
(
);
throw
new
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érée 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 :
Int32 err =
Marshal.
GetLastWin32Error
(
);
throw
new
Win32Exception
(
err);
par ceci :
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 fournie par : abelman et qui vous permet de récupérer le message d'une DLL à partir du code d'erreur : Classe permettant la récupération des messages d'erreur d'une DLL depuis le code d'erreur.
III. Conversion de type▲
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 :
Type Win32 |
Type .NET |
Équivalence en C#/VB.NET |
---|---|---|
HWND, HANDLE, HINSTANCE, HMODULE |
System.IntPtr |
System.IntPtr |
LPSTR, LPCSTR, LPWSTR, LPCWSTR |
System.String, System.StringBuilder |
string |
BYTE |
System.Byte |
Byte |
SHORT |
System.Int16 |
Short |
WORD |
System.UInt16 |
ushort |
DWORD, UINT, ULONG |
System.Int32 |
uint |
INT, LONG |
System.UInt32 |
uint |
BOOL |
System.Bool |
bool |
CHAR |
System.Char |
char |
FLOAT |
System.Single |
float |
DOUBLE |
System.Double |
double |
Ce tableau peut être retrouvé dans la MSDN de Microsoft, à cette adresse : http://msdn.microsoft.com/en-us/magazine/cc164123.aspx#S6
IV. Passage d'arguments par pointeurs▲
IV-A. Les risques▲
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-miette).
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. Étant 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.
IV-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 clef 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"
) ]
public
static
extern
bool
ReadFile
(
IntPtr hFile,
void
*
lpBuffer,
uint
NbOctevtALire,
uint
*
lpNbOctetsLus,
IntPtr lpOverlapped
);
...
int
handle =
...
// Appel à la fonction CreateFile
uint
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. À l'inverse, un paramètre du nom de GetTexte fait plutôt penser à un paramètre de sortie. Bien que fonctionnant, cette technique n'est pas sure à 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 :
// Déclaration des using
using
System;
using
System.
Runtime.
InteropServices;
class
FenetreClignotante
{
// Déclaration de la méthode à utiliser
[ DllImport(
"kernel32"
, CharSet=CharSet.Auto) ]
public
static
extern
uint
GetCurrentDirectory
(
uint
Taille,
StringBuilder sTmp
);
// Point d'entrée de l'application
static
void
Main
(
string
[]
args)
{
uint
Taille =
255
;
StringBuilder sTmp =
new
StringBuilder
((
int
)Taille);
uint
i =
GetCurrentDirectory
(
Taille,
sTmp);
Console.
WriteLine
(
sTmp);
}
}
// ** 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) ]
static
extern
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.