Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Julien Chable

He blogs, you blog, I blog ...

Archives

.NET Intéropérabilité code managé/non managé

Voici mon premier article sur .NET, j'espère que cela vous plaira même si ce n'est pas nouveau et que cela peut se trouver ailleurs sur le net. J'avais envie de faire une petite pause et je me suis dit que cela serait une bonne idée de faire découvrir aux personnes ne connaissant pas cette face cachée de .NET.

On peut complimenter Java d'être un langage portable, sûrement un peu plus que .NET, mais cette portabilité est aussi quelquefois une contrainte lorsque vous disposer de logiciels développer dans des langages hétérogènes. Certes il y a CORBA et consort, mais ce n'est pas pour faciliter le développement et les problèmes par la suite. Aujourd'hui on ne redéveloppe quasiment jamais un logiciel dans son intégralité, on doit faire avec l'existant. A notre époque où les technologies changent, on ne peut pas utiliser une technologie qui ne permet de faire un minimum d'intéropérabilité avec les autres technologies existantes.

La technologie .NET est sur ce point relativement avancé par rapport à Java. Java dispose des JNI (Java Native Interface) pour se débrouiller d'appeler des fonctions natives écrites en C/C++ majoritairement, ou alors d'embarquer une JVM dans une application C/C++. Mais le framework .NET permet de faire cela beaucoup plus simplement à l'aide des attributs et des services PInvoke (Platform Invocation Services), et surtout permet d'en faire un peu plus (beaucoup ?) ...

Remarque : attention plateforme Windows uniquement => je ne connais pas suffisamment l'initiative de Mono sur ce point pour dire que cela marche aussi bien, bien que cela devrait être le cas ...

Nous allons voir 2 points sur ce sujet :

  • Services PInvoke
  • Ecriture de code non sécurisé

Surement dés que je serais plus adroit avec COM, je ferais un post sur l'intéropérabilité avec COM mais c'est pas pour tout de suite ;-)

Services PInvoke

Ces services offrent la possibilité d'écrire du code.NET permettant d'accéder aux fonctions, structures et callbacks des DLL non gérées. Je ne vous donne même pas d'exemples d'utilisations tellement cela est énorme et indispensable pour du développement système ou autres.

Déclaration d'une fonction importée

La déclaration d'une fonction importée d'une DLL est la suivante :

[DllImport(Nom de la DLL)]
modificateur_accès static extern type_retour NomFonctionDLL(type_param1 param1, type_param2 para2, ...)

Les modificateurs d'accès static extern sont indispensables. L'espace de noms contenant l'attribut DllImport se trouve est System.Runtime.InteropServices.

Voici un exemple d'utilisation très simple (j'ai pas trouvé plus simple !):

using System;
using System.Runtime.InteropServices;

namespace Blogs.Developpeur.Org.Neodante
{
   class PInvokeMessageBox
   {
      [DllImport ("user32.dll")]
      static extern int MessageBoxA(int hWnd, string msg, string caption, int type);

      static void Main(string[] args)
      {
         MessageBoxA(0, "Hello from C# managé", "Intéropérabilité C#/code non managé", 0);
      }
   }
}

Cela aura pour résultat de vous afficher une jolie boîte de dialogue appellé via la DLL user32.

Dans cet exemple on reprenait le nom de la fonction importée, mais nous pouvons spécifier un nom différent pour C# en utilisant le paramètre EntryPoint de DllImport. Voici la déclaration :

...

[DllImport ("user32.dll", EntryPoint="MessageBoxA")]
static extern int MsgBoxDLL(int hWnd, string msg, string caption, int type);

static void Main(string[] args)
{
   MsgBoxDLL(0, "Hello from C# managé", "Intéropérabilité C#/code non managé", 0);
}

...

Allons encore un peu plus loin, et utilisons d'autres paramètres de DllImport. Le paramètre CharSet permet de spécifier le jeu de caractères employé par la DLL. Si nous reprenons notre exemple précédent, l'utilisation de la valeur CharSet.Ansi dans l'appel de MessageBox produira un appel à MessageBoxA, alors que CharSet.Unicode se traduira par un appel à MessageBoxW. CharSet.Auto décidera pour vous ...

...

[DllImport ("user32.dll", CharSet=CharSet.Ansi)]
static extern int MessageBox(int hWnd, string msg, string caption, int type);

static void Main(string[] args)
{
   MessageBox(0, "Hello from C# managé", "Intéropérabilité C#/code non managé", 0);
}

...

Info Java : pour pouvoir appeler une DLL à partir de Java, l'utilisation des JNI (Java Native Interface) est indispensable. Mais attention, qui dit JNI dit DLL créée spécialement pour JNI. C'est à dire qu'une DLL qui n'a pas été écrite d'une certaine façon pour être compatible avec JNI ne peut pas directement être exploitée. Si vous voulez appeler une DLL non compatible JNI, comme TOUTES les DLL qui ne sont pas écrites pour une application Java, il faut faire une DLL JNI qui appelle les fonctions d'une ou de plusieurs DLL. Donc l'utilisation de JNI est quelque peu fastidieuse, d'autant que pour faire une DLL JNI il faut pendant l'étape de développement passer par un petit utilitaire javah en plus de la compilation classique ...

Marshaling

Quand vous appeller une fonction importée à partir d'une DLL, .NET doit transformer les paramètres donnés dans le code managé en paramètres valides pour la fonction de la DLL et vice versa au retour de la fonction DLL. Cette opération est appelée marshaling ou marshalisation (du verbe marshaler provenant de l'argofrancoanglais apparemment :p). Si nous prenons l'exemple de la fonction MessageBox, le 2ème et 3ème paramètres que nous donnons est un type String, cependant le compilateur C# le transforme automatiquement en type Win32 LPSTR.

Pour connaître la correspondance de l'ensemble des types de données de la plateforme .NET, comment marshaler les classes/strutures/string/tableaux et avoir tout plein de renseignements plus important les uns que les autres, voici la page MSDN. Avec cette page, vous devriez pouvoir vous en sortir dans 99% des cas.

Néanmoins que faire si vous voulez garder le contrôle de cette transformation ? .NET possède un attribut (que je viens de découvrir !) MarshalAs défini dans l'espace de nom System.Runtime.InteropServices, à ne pas oublier d'inclure !

using System;
using System.Runtime.InteropServices;

namespace Blogs.Developpeur.Org.Neodante
{
  
class PInvokeMessageBox
   {
     
[DllImport ("user32.dll", CharSet=CharSet.Unicode)]
     
static extern int MessageBox(int hWnd,
         [MarshalAs(UnmanagedType.LPWStr)]
         string msg,
         [MarshalAs(UnmanagedType.LPWStr)]
         string caption, int type);

      static void Main(string[] args)
      {
         MessageBox(0, "Hello from C# managé", "Intéropérabilité C#/code non managé", 0);
      }
  
}
}

Remarque Java : En Java cela n'est pas nécessaire puisque les DLL JNI emploient obligatoirement des types Java définit dans jni.h qui sont de simple alias de type prédéfinis :
...
typedef
unsigned char
jboolean;
typedef unsigned short
jchar;
typedef short
jshort;
typedef float
jfloat;
typedef double
jdouble;
...

Néanmoins, la structure JNIEnv permet d'avoir certaines informations sur la JVM lors de l'appel d'une fonction d'une DLL, ce que ne peut pas donner la CLR ... Les deux approches sont très intéressantes mais celle de .NET est beaucoup plus simple, plus rapide et répond à beaucoup plus de besoins que les JNI de Java ...

Fonction de rappel

Certaines fonctions de DLL permettent de d'appeller une méthode en réponse à l'appel d'une méthode (pour .NET dispose des delegates pour ça), on appel cela couramment un callback ou méthode de rappel. Pour cela nous allons utiliser la combinaison de PInvoke et des délégués, le premier pour appeler la fonction et le second pour permettre de définir la fonction de rappel. Pour passer un string par référence nous utilisons un StringBuilder comme le préconise le tableau de mashaling d'une chaine de caractères.

L'exemple suivant très simple utilise les APIs EnumWindows et GetWindowText, on passe à la première fonction un pointeur vers une fonction qui sera appelée pour chaque fenêtre trouvée. Dans la fonction ainsi appelée, nous utilisons l'API nous permettant de connaître le titre de chaque fenêtre appelée :

...
[DllImport("user32.dll")]
static extern int GetWindowText(int hWnd, StringBuilder text, int count);
[DllImport("user32.dll")]
static extern int EnumWindows(CallbackFunc callback, int lParam);

delegate bool CallbackFunc(int hWnd, int lParam);

static bool InfoWindowOutput(int hWnd, int lParam)
{
   StringBuilder sb =
new StringBuilder(255);
   GetWindowText(hWnd, sb, 255);
   System.Console.WriteLine("Window title : " + sb);
  
return true;
}

static void Main()
{
   EnumWindows(
new CallbackFunc(InfoWindowOutput), 0);
}
...

Code non sécurisé

On entend par code non sécurisé, du code pour lequel le Runtime ne gère pas lui -même l'allocation et la libération de la mémoire. L'utilisation du code non sécurisé est particulièrement intéressant lorsque vous utilisez des appels à des DLL C/C++ ou alors à des fins de performance (nous y reviendrons dans les tutorials sur Mobile Direct3D :p).

L'utilisation de deux mots clé permet cela : unsafe et fixed.

Le mot clé unsafe spécifie d'un bloc de code, d'une méthode (constructeur compris), d'une propriété qu'il sera exécuté dans un environnement non géré (Garbage Collector à la niche ! Bon chien bon chien !)

Le mot clé fixed permet de dire au GC de ne pas déplacer l'objet lors de ses optimisations de mémoire (défragmentation). Ainsi le pointeur sur un objet fixed ne deviendra pas invalide. Ce mot clé va de paire avec l'utilisation des pointeurs, sans lequel cela ne serait évidemment pas possible dans certains cas. De toute manière, le compilateur C# ne vous permettra pas de créer un pointeur vers une variable gérée sans l'utilisation d'un bloc fixed.

Remarque : le mot clé fixed donnant des contraintes à la Garbage Collector, il va de soi que son utilisation doit rester exceptionnelle.

Les pointeurs en C# ne peuvent être que sur des types valeur, des chaines de caractères et des tableaux.

Remarque : le premier élément d'un tableau doit être un type valeur, car C# retourne l'adresse du premier élément. Pour avoir loupé cette info, j'ai passé 2 heures à débogué lors de ma première utilisation de code unsafe ... :(

class UnsafeClass
{

   public static unsafe void Swap(int* a, int* b)
   {
     
int tmp = *a;
      *a = *b;
      *b = tmp;
   }

   public static void Main()
   {
     
int a = 10;
     
int b = 20;

      System.Console.WriteLine("Values : a={0} b={1}", a, b);

      unsafe
     
{
        
Swap(&a,  &b);
      }

      System.Console.WriteLine("Values : a={0} b={1}", a, b);
   }
}

Voici la syntaxe pour utiliser le mot clé fixed : fixed (type* pointeur = expression) { instructions }

Le Garbage Collector ne s'occupe pas alors de la variable spécifié qui peut être de type void ou non managé. Si vous ne spécifiez pas le mot clé fixed, vous aurez le droit à un message d'erreur : "Vous ne pouvez prendre l'adresse d'une expression non fixed qu'à l'intérieur d'un initialiseur d'instruction fixed". Voici un exemple relativement simple :

class Toto
{
  
public int a, b;
}

class FixedTest
{
  
static unsafe void Swap(int* a, int* b)
   {
     
int tmp = *a;
      *a = *b;
      *b = tmp;
   }

   unsafe public static void Main()
   {
      Toto toto =
new Toto();
      toto.a = 10;
      toto.b = 20;

      Console.WriteLine ("a={0} b={1}", toto.a, toto.b);

      fixed (int* finta = &toto.a, fintb = &toto.b)
      {
         Swap (finta, fintb);
      }

      Console.WriteLine ("a={0} b={1}", toto.a, toto.b);
   }
}

Liens :

Tutorial PInvoke sur MSDN : http://msdn.microsoft.com/library/default.asp?url=/library/en-us/csref/html/vcwlkplatforminvoketutorial.asp
C# Keyword unsafe : http://msdn.microsoft.com/library/default.asp?url=/library/en-us/csref/html/vclrfunsafe.asp
C# Keyword fixed : http://msdn.microsoft.com/library/default.asp?url=/library/en-us/csref/html/vclrffixed.asp

Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :
Posted: vendredi 29 juillet 2005 12:04 par neodante

Commentaires

neodante a dit :

Salut, très bon résumé sur PInvoke, je me permet de rajouter que pour de meilleurs performances on peut utiliser l'attribut [ SuppressUnmanagedCodeSecurity ] qu'on peut aussi allouer de la mémoire non managé avec les fonctions Marshal.AllocCoTaskMem et Marshal.FreeCoTaskMem et qu'en mode unsafe on peut utiliser l'opérateur '->' pour acceder aux membres. J'espere qu'il y'aura une suite avec les techniques avancées de PInvoke..
# août 12, 2005 17:52
Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- Office 365: Modifier les jeux de couleur dans les Thèmes des pages classiques de SharePoint Online par Blog Technique de Romelard Fabrice le 08-08-2018, 17:27

- Office 365: Modifier les jeux de couleur dans les Thèmes des pages modernes de SharePoint Online par Blog Technique de Romelard Fabrice le 07-04-2018, 13:26

- Office 365: Script PowerShell pour fixer le Quota Warning de toutes les collections d’un tenant par Blog Technique de Romelard Fabrice le 07-03-2018, 14:16

- MVP Award 2018-2019 par Blog de Jérémy Jeanson le 07-02-2018, 20:39

- Reprise des articles de 2014 à aujourd’hui par Blog de Jérémy Jeanson le 06-20-2018, 13:00

- Office 365: Comment créer un sous-plan dans Office 365 Planner par Blog Technique de Romelard Fabrice le 06-14-2018, 17:19

- Office 365: Script PowerShell de création de sous-sites basés sur CSOM ou PnP par Blog Technique de Romelard Fabrice le 06-12-2018, 14:58

- Office 365: Comment exporter tous les comptes Azure Active Directory ayant une license via PowerShell par Blog Technique de Romelard Fabrice le 05-17-2018, 13:46

- PowerShell: Comment avoir le Country Name depuis un Country Code par Blog Technique de Romelard Fabrice le 05-17-2018, 13:20

- Office 365: Comment supprimer un compte externe d’un site SharePoint Online en mode Extranet par Blog Technique de Romelard Fabrice le 05-11-2018, 17:00