Accueil
Rechercher:
sur developpez.com sur les forums
Forums | Tutoriels | F.A.Q's | Participez | Hébergement | Contacts
Club Emploi Blogs   TV   Dév. Web PHP XML Python Autres 2D-3D-Jeux Sécurité Windows Linux PC Mac
Accueil Conception Java DotNET Visual Basic  C  C++ Delphi MS-Office SQL & SGBD Oracle  4D  Business Intelligence
FORUMS .NET FAQs .NET TUTORIELS .NET SOURCES .NET LIVRES .NET OUTILS .NET BLOG .NET DOTNET TV

Les DLL natives en .NET

Date de publication : 18/11/2004 , Date de mise a jour : 18/11/2004

Par LEBRUN Thomas (Autres Articles)
 

  

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


I. Introduction
II. Utilisation
A. Explications
B. Exemples
III. Conversion de type
IV. Passage d'arguments par pointeurs
a. Les risques
b. La solution
V. Passage de chaînes de caractères
VI. Conclusion
VI. Liens


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


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 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é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 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
// 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:

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 fournit 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 message 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 Equivalence 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/msdnmag/issues/03/07/NET/default.aspx?fig=true#fig2


IV. Passage d'arguments par pointeurs


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.


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") ] 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
// 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
// ** 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.


VI. Liens




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'à 3 ans de prison et jusqu'à 300 000 E de dommages et intérêts. Cette page est déposée à la SACD.

Responsables bénévoles de la rubrique DotNET : Jérôme Lambert (Cardi) et Louis-Guillaume Morand - Contacter par EMail :
Vos questions techniques : forum d'entraide DotNET - Publiez vos articles, tutoriels et cours
et rejoignez-nous dans l'équipe de rédaction du club d'entraide des développeurs francophones
Nous contacter - Copyright © 2000-2008 www.developpez.com - Legal informations.