Les DLL natives en .NET

Image non disponible  Image non disponible  Image non disponible

Dans cet article, vous apprendrez comment faire appel, dans vos applications .NET, à des fonctions dont le code est implémenté dans des DLL natives.

N'hésitez pas à commenter cet article ! Commentez Donner une note à l'article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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 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

É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).

Utilisation de DllImport
Sélectionnez
// 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

Utilisation de l'attribut EntryPoint
Sélectionnez
// 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éfinit dans System.Runtime.InteropServices.Marshal).
Voici un exemple pour illustrer cette propriété :

Utilisation de SetLastError et GetLastWin32Error
Sélectionnez
// 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é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 :

 
Sélectionnez
Int32 err = Marshal.GetLastWin32Error();
throw new Win32Exception(err);

par ceci :

 
Sélectionnez
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 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.

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-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.

 
Sélectionnez
// 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. 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
Sélectionnez
// 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);
	}

}
Passage de chaîne de caractères : Exemple avec un paramètre d'entrée et un paramètre de sortie
Sélectionnez
// ** 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.

VII. Liens

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2004 LEBRUN Thomas. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.