Introduction à l'interopérabilité - comment utiliser ses dll natives en .net

Nico-pyright(c)

Cet article a pour but de présenter une introduction à l'interopérabilité, de (ré)utiliser des bibliothèques natives dans un programme .Net (C++/CLI, C#) et présentera les détails de la création d'un wrapper. ♪

- Titre : Introduction à l'interopérabilité - comment utiliser ses dll natives en .net

- Auteur : Nico-pyright(c)

- Parution : 16 janvier 2007

- Mise à jour : 16 janvier 2007

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2007 Nico-pyright(c). 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. Droits de diffusion permanents accordés à Developpez LLC.

Sommaire

- 1.Introduction

- 2.Mixer du C/C++ avec le C++/CLI

- 3.Interopérabilité C et C++/CLI

- 3.1.Créons une petite bibliothèque

- 3.2.Interopérabilité d'une bibliothèque C avec du C++/CLI

- 3.3.Interopérabilité d'une API Win32 avec du C++/CLI

- 3.Pinvoke et dllimport

- 3.1.Exportation des méthodes

- 3.2.Interopérabilité d'une API Win32 avec du C#

- 3.3.Interopérabilité d'une bibliothèque C avec du C#

- 3.4.Interopérabilité d'une classe C++ avec du C#

- 3.5.Introduction au Marshalling : exemple String managé / string natif

- 4.Wrapper C++/CLI

- 4.1.Un exemple de wrapper

- 4.2.Marshall et conversions

- 4.3.Téléchargements

- 5.Performances

- 6.Conclusion

- Remerciements

- Contact

1.Introduction

Vous êtes un développeur .Net (C# par exemple) et on vous a demandé d'utiliser des bibliothèques natives dans votre application ?
Ou alors vous êtes un développeur C ou C++ qui souhaite réutiliser ses bibliothèques natives dans le cadre d'un projet en .Net ? Ou simplement, vous souhaitez migrer vos applications natives sans avoir à réécrire toutes vos bibliothèques ?
Alors ce tutoriel d'introduction à l'interopérabilité est pour vous. Vous trouverez dans cet article comment mixer du code natif avec du code managé en C++/CLI. Vous apprendrez aussi à utiliser des dll natives en C#. Enfin, vous y apprendrez comment créer un wrapper en C++/CLI.
On parlera alors d'interopérabilité, ou d'interop, de C++ interop, native interop ou autres p-invoke et DllImport.

Tout au long de ce tutoriel, nous allons utiliser Visual C++ 2005 et Visual C# 2005.

2.Mixer du C/C++ avec le C++/CLI

Une des grandes forces du C++/CLI est sa capacité à mixer du code natif avec du code .Net. Cette fonctionnalité s'appelle IJW (It Just Works).

On peut donc très facilement intégrer des classes C++ ou des méthodes C dans une application .Net développée en C++/CLI.

Imaginons que je dispose d'une bibliothèque, façon C, très complexe, que voici :

fichier libC.h

#include <stdio.h>

void affiche(char * phrase)
{
printf("%s\n", phrase);
}

et que je dispose d'une classe C++ CPersonne que voici :

fichier Personne.h

#include <iostream>
#include <string>
using namespace std;

class CPersonne
{
private:
string leNom;
string lePrenom;
public:
CPersonne(string nom, string prenom);
void afficheNomPrenom();
public:
~CPersonne(void);
};

fichier Personne.cpp

#include "Personne.h"

CPersonne::CPersonne(string nom, string prenom)
{
leNom = nom;
lePrenom = prenom;
}

CPersonne::~CPersonne(void)
{
}

void CPersonne::afficheNomPrenom()
{
cout << "Je m'appelle : " << lePrenom << " " << leNom << endl;
}

Je veux pouvoir créer une application C++/CLI qui capitalise ces acquis sans avoir à réécrire tout le code. Et je veux logiquement pouvoir intégrer mes sources dans mon application. Rien de plus simple. Je crée une application CLR console et j'ajoute ces fichiers en réglant le mode de compilation à mixte (/clr). Voici mon programme final :

#include "Personne.h"
#include "libC.h"

using namespace System;

int main(array<System::String ^> ^args)
{
Console::WriteLine(L"Hello World C++/CLI");
CPersonne moi("pyright", "nico");
moi.afficheNomPrenom();
affiche("Hello C");
return 0;
}

Ainsi, si on a la possibilité d'inclure ses classes existantes dans sa nouvelle application C++/CLI, il devient très facile de mixer son code existant avec du C++/CLI.
Pour plus d'informations, n'hésitez pas à consulter :
- Utiliser le C++ managé et le framework.net 2.0 dans des applications Win32 et MFC
- Introduction au monde du C++/CLI avec Visual C++ 2005
- Mixer du C++/CLI avec du code Win32 ou MFC dans la FAQ C++/CLI

Vous pouvez télécharger cet exemple (classe C++, bibliothèque C et programme exemple en C++/CLI) à cette adresse : Télécharger ici (28 ko)

3.Interopérabilité C et C++/CLI

On n'a pas toujours la possibilité ou l'envie d'intégrer ses sources (méthodes ou classes) dans notre nouveau projet en C++/CLI.
Aussi, il est possible d'utiliser IJW pour utiliser des méthodes depuis des dll existantes.

3.1.Créons une petite bibliothèque

Tout d'abord, créons une petite bibliothèque, façon C.
Faisons un nouveau projet Win32, que nous allons appeler libC. On choisit bien sur DLL comme type d'application, et on choisit de faire un projet vide.
Ensuite, ajoutons un fichier cpp, dans lequel nous allons créer une fonction, que nous allons exporter.
Ajoutons le code suivant :

extern "C" __declspec(dllexport) int addition(int a,int b)
{
return a+b;
}

On commence par faire simple, une addition de deux entiers, qui retourne un entier. L'utilisation d'extern « C » permettra d'avoir un nom de fonction non décoré, ce qui sera important pour la suite.
Après compilation et édition de liens, on obtient un fichier libC.lib et un fichier libC.dll.

Vous pouvez télécharger la bibliothèque C indépendante à cette adresse : Télécharger ici (7 ko)

3.2.Interopérabilité d'une bibliothèque C avec du C++/CLI

Nous allons maintenant utiliser cette DLL depuis un programme .Net écrit en C++/CLI.
Créons un nouveau projet CLR console application, que j'appelle cppCliUseLibC. On copie le .lib de la bibliothèque précédemment écrite dans le même répertoire que les sources du projet que l'on est en train de créer.
Il faut désormais lier la bibliothèque au projet, pour cela deux façons :
- bouton droit sur le projet => properties => configuration properties => linker => input => additional depedencies, et ici, on rajoute le nom du .lib.

- ou alors avec le pragma suivant :

#pragma comment (lib, "libc.lib")

Rajoutons ensuite en entête le prototype de la fonction exportée :

extern "C" __declspec(dllexport) int addition(int a,int b);

Nous pouvons alors utiliser la fonction dans notre programme :

using namespace System;

#pragma comment (lib, "libc.lib")
extern "C" __declspec(dllexport) int addition(int a,int b);

int main(array<System::String ^> ^args)
{
Console::WriteLine(addition(10,5));
return 0;
}

N'oubliez pas d'aller copier la dll précédemment générée dans le répertoire de votre exe (debug ou release). Exécutez, et … It Just Works !

Vous pouvez télécharger cet exemple à cette adresse : Télécharger ici (22 ko)

3.3.Interopérabilité d'une API Win32 avec du C++/CLI

C'est le même principe lorsque l'on veut utiliser les Api Win32. Les méthodes sont stockées dans les dll de windows (user32.dll par exemple). Il suffit de la même manière de lier le .lib correspondant et d'inclure le bon fichier d'entête (l'information est dans msdn). Exemple pour l'API GetSystemMetrics :.
Msdn nous informe qu'il faut inclure Windows.h et lier User32.lib.
Écrivons donc notre programme qui utilise une API Win32

#include <windows.h>

using namespace System;

#pragma comment (lib, "user32.lib")

int main(arraylt;System::String ^> ^args)
{
int x = GetSystemMetrics(SM_CXFULLSCREEN);
int y = GetSystemMetrics(SM_CYFULLSCREEN);
Console::WriteLine(L"La résolution de l'écran est {0}x{1}", x,y );
return 0;
}

Encore une fois, It Just Works, sans avoir rien d'autre à faire. C'est IJW qui fait tout le travail pour nous.

Vous pouvez télécharger cet exemple à cette adresse : Télécharger ici (21 ko)

3.Pinvoke et dllimport

P/Invoke est le raccourci de Platform Invoke, qui représente ce qu'on appelle une transition dans le monde natif. Ce procédé est non sécurisé (unsafe) et ne respecte pas les normes CLS.
L'appel de P/Invoke est un service qui permet à du code managé d'appeler des fonctions non managées implémentées dans des bibliothèques de liaison dynamique (DLL), comme celles figurant dans l'interface API Win32. Elle localise et appelle une fonction exportée et marshale ses arguments (entiers, chaînes, tableaux, structures) sur les limites d'interopération si nécessaire.
Le marshalling regroupe tout ce qui concerne la conversion de paramètres entre .Net et d'autres langages (notamment le monde natif). Convertir des données est un procédé complexe qui n'est pas toujours automatisable et qui peut nécessiter une intervention complexe du développeur.
On utilise DLLImport, qui est un attribut .Net qui nous permet de définir un point d'entrée dans les bibliothèques natives.
Vous pourrez retrouver plus d'informations dans le tutoriel de Thomas Lebrun.

3.1.Exportation des méthodes

Le grand inconvénient de P/Invoke est qu'on ne peut pas exporter des types, mais uniquement des fonctions.
Cela va nous obliger par moment à utiliser des pirouettes de programmation, et des rajouts d'informations.

3.2.Interopérabilité d'une API Win32 avec du C#

Pour appeler une méthode de l'API Win32, on va donc utiliser P/Invoke.
Il faut alors utiliser DllImport et référencer l'assembly correspondante :

using System.Runtime.InteropServices;

La méthode que l'on va utiliser est toujours GetSystemMetrics, accessible dans user32.dll. On va donc la déclarer avec DllImport conformément à sa définition dans MSDN :

extrait de MSDN

int GetSystemMetrics(
int nIndex
);

Ce qui donne :

[DllImport("User32.dll")]
public static extern int GetSystemMetrics(int nIndex);

N'oublions pas non plus de définir les constantes SM_CXSCREEN et SM_CYSCREEN. Ce qui nous donne le programme complet ci-dessous :

using System;
using System.Runtime.InteropServices;

namespace useWin32ApiInCSharp
{
class Program
{
[DllImport("User32.dll")]
public static extern int GetSystemMetrics(int nIndex);
static int SM_CXSCREEN = 0;
static int SM_CYSCREEN = 1;

static void Main(string[] args)
{
Console.WriteLine("la résolution de l'écran est {0}x{1}", GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN));
}
}
}

Ici, l'interopérabilité est assurée grâce à DllImport qui va permettre d'accéder directement à la méthode de la DLL, cependant c'est à nous de fournir la déclaration et les types utilisés, ce qui peut s'avérer fastidieux.

NB : l'attribut DllImport existe aussi en C++/CLI, mais généralement, on aura tendance à ne pas l'utiliser explicitement et à laisser IJW l'utiliser automatiquement.

Vous pouvez télécharger cet exemple en C# à cette adresse : Télécharger ici (7 ko)

3.3.Interopérabilité d'une bibliothèque C avec du C#

Le même principe est employé ici pour utiliser notre DLL C depuis notre programme C#.
Seuls changent ici le nom de la DLL et le nom de la fonction à appeler. Ce qui nous donne :

using System;
using System.Runtime.InteropServices;

namespace useWin32ApiInCSharp
{
class Program
{
[DllImport("libC.dll")]
public static extern int addition(int a, int b);

static void Main(string[] args)
{
Console.WriteLine(addition(10, 15));
}
}
}

N'oubliez pas bien sûr de copier la DLL libC.dll dans le répertoire avec l'exécutable C#.
Notez ici l'importance de l'emploi de extern « C » comme expliqué précédemment, le nom de la fonction n'est pas décoré et est facilement utilisable.

Vous pouvez télécharger cet exemple en C# à cette adresse : Télécharger ici (10 ko)

3.4.Interopérabilité d'une classe C++ avec du C#

Nous avons vu jusqu'à présent comment utiliser des méthodes définies dans des dll dans des applications .Net, mais comment utiliser des classes complètes ?
Sachant qu'on ne peut importer que des méthodes, comment faire ?
C'est ce que je vais montrer ici.
Tout d'abord, il nous faut une instance d'objet et il faut pouvoir la libérer. Nous allons donc exporter des méthodes new et delete.

Ajoutons donc :

extern "C" __declspec(dllexport) CPersonne* CPersonne_New();
extern "C" __declspec(dllexport) void CPersonne_Delete(CPersonne* cp);

On ajoute donc deux fonctions qui ne doivent pas être membre de la classe afin de wrapper les constructeurs et destructeurs de la classe.

extern "C" __declspec(dllexport) CPersonne* CPersonne_New()
{
return new CPersonne("pyright", "nico"); // fait exprès, voir plus loin
}
extern "C" __declspec(dllexport) void CPersonne_Delete(CPersonne* cp)
{
delete cp;
}

Ne pas oublier bien sûr d'exporter la classe :

class __declspec(dllexport) CPersonne

Voici le code complet :

fichier Personne.h

#pragma once
#include <iostream>
#include <string>
using namespace std;

class __declspec(dllexport) CPersonne
{
private:
string leNom;
string lePrenom;
public:
CPersonne(string nom, string prenom);
void afficheNomPrenom();
public:
~CPersonne(void);
};

extern "C" __declspec(dllexport) CPersonne* CPersonne_New();
extern "C" __declspec(dllexport) void CPersonne_Delete(CPersonne* cp);

fichier Personne.cpp

#include "Personne.h"

CPersonne::CPersonne(string nom, string prenom)
{
leNom = nom;
lePrenom = prenom;
}

CPersonne::~CPersonne(void)
{
}

void CPersonne::afficheNomPrenom()
{
cout << "Je m'appelle : " << lePrenom << " " << leNom << endl;
}

extern "C" __declspec(dllexport) CPersonne* CPersonne_New()
{
return new CPersonne("pyright", "nico");
}
extern "C" __declspec(dllexport) void CPersonne_Delete(CPersonne* cp)
{
delete cp;
}

Les attentifs auront remarqué que j'instancie le constructeur en dur avec les noms et prénom « pyright » et « nico ». Les non-attentifs n'auront qu'à revenir un peu en arrière. C'est évidemment fait exprès dans un premier temps…
Car la question qui se pose maintenant est : comment faire pour passer des paramètres depuis C# au constructeur ? Nous verrons cela plus tard.

Occupons-nous désormais du programme C#. Il faut donc déclarer via DllImport les deux fonctions exportées qui feront office de constructeurs et destructeurs.

[DllImport("libCppForInterop.dll", EntryPoint = "CPersonne_New")]
public static extern IntPtr NewCPersonne();

[DllImport("libCppForInterop.dll", EntryPoint = "CPersonne_Delete")]
public static extern void DeleteCPersonne(IntPtr cp);

Pas de difficultés jusque là. Cependant, cela se corse maintenant, car le compilateur va mutiler le nom des méthodes de CPersonne tant est si bien que le CLR sera incapable d'identifier un point d'entrée correct pour la fonction afficheNomPrenom.
Heureusement, vient à la rescousse un petit utilitaire bien pratique dumpbin.exe que vous trouverez avec Visual C++. Lancez en ligne de commande :

dumpbin.exe /exports libCppForInterop.dll

Ce qui nous intéresse se trouve ici :

5    4 000110EB ?afficheNomPrenom@CPersonne@@QAEXXZ = @ILT+230(?afficheNomPrenom@CPersonne@@QAEXXZ)
6    5 00011203 CPersonne_Delete = @ILT+510(_CPersonne_Delete)
7    6 0001112C CPersonne_New = @ILT+295(_CPersonne_New)

On constate bien que les méthodes CPersonne_New et CPersonne_Delete ne sont pas décorées, cependant ce n'est pas le cas de affichePrenom. Qu'à cela ne tienne, nous allons utiliser ce nom puisque nous l'avons.

[DllImport("libCppForInterop.dll", EntryPoint = "?afficheNomPrenom@CPersonne@@QAEXXZ",
CharSet = CharSet.Unicode, CallingConvention = CallingConvention.ThisCall)]
public static extern void afficheNomPrenom(IntPtr thisptr);

Ici j'introduis deux paramètres supplémentaires à DllImport : Charset et CallingConvention. Charset permet de préciser le type d'encodage voulu, on ne s'en sert pas encore pour l'instant. CallingConvention est une méthode d'instance qui contient un argument implicite qui représente le pointeur this. Dans notre cas, il s'agit du pointeur natif que nous devons passer en argument.
Voici le programme complet :

using System;
using System.Runtime.InteropServices;

namespace useLibCppWithCSharp
{
class Program
{

[DllImport("libCppForInterop.dll", EntryPoint = "CPersonne_New")]
public static extern IntPtr NewCPersonne();

[DllImport("libCppForInterop.dll", EntryPoint = "CPersonne_Delete")]
public static extern void DeleteCPersonne(IntPtr cp);

[DllImport("libCppForInterop.dll", EntryPoint = "?afficheNomPrenom@CPersonne@@QAEXXZ",
CharSet = CharSet.Unicode, CallingConvention = CallingConvention.ThisCall)]
public static extern void afficheNomPrenom(IntPtr thisptr);

static void Main(string[] args)
{
IntPtr cp = NewCPersonne();
afficheNomPrenom(cp);
DeleteCPersonne(cp);
}
}
}

Pas si compliqué que ça finalement … mais il reste le problème des paramètres du constructeur.

Vous pouvez télécharger la bibliothèque C++ à cette adresse : Télécharger ici (10 ko)
Vous pouvez télécharger le programme de test C# à cette adresse : Télécharger ici (12 ko)

3.5.Introduction au Marshalling : exemple String managé / string natif

Comment réussir alors à communiquer entre un programme C# qui connait des String et autres StringBuilder avec une classe C++ qui réclame un std::string ?

C'est tout l'épineux problème du Marshalling et nous allons petit à petit nous y attaquer.
On peut dès à présent imaginer une première solution qui serait de réécrire la bibliothèque en proposant un type plus facilement manipulable que std::string, comme wchar_t par exemple. Nous allons réécrire un bout de la classe dans un premier temps, notamment les accès aux chaînes et le prototype du constructeur.

fichier Personne.h

#pragma once
#include <wchar.h>

class __declspec(dllexport) CPersonne
{
private:
wchar_t leNom[100];
wchar_t lePrenom[100];
public:
CPersonne(wchar_t *nom, wchar_t *prenom);
void afficheNomPrenom();
public:
~CPersonne(void);
};

extern "C" __declspec(dllexport) CPersonne* CPersonne_New(wchar_t *nom, wchar_t *prenom);
extern "C" __declspec(dllexport) void CPersonne_Delete(CPersonne* cp);

fichier Personne.cpp

#include "Personne.h"

CPersonne::CPersonne(wchar_t *nom, wchar_t *prenom)
{
wcscpy_s(leNom,nom);
wcscpy_s(lePrenom,prenom);
}

CPersonne::~CPersonne(void)
{
}

void CPersonne::afficheNomPrenom()
{
wprintf(L"Je m'appelle : %s %s\n", lePrenom, leNom);
}

extern "C" __declspec(dllexport) CPersonne* CPersonne_New(wchar_t *nom, wchar_t *prenom)
{
return new CPersonne(nom, prenom);
}
extern "C" __declspec(dllexport) void CPersonne_Delete(CPersonne* cp)
{
delete cp;
}

On changera aussi la déclaration DllImport pour le constructeur :

using System;
using System.Runtime.InteropServices;

namespace useLibCppWithCSharp2
{
class Program
{
[DllImport("libCppForInterop2.dll", EntryPoint = "CPersonne_New", CharSet = CharSet.Unicode)]
public static extern IntPtr NewCPersonne(String nom, String prenom);
[DllImport("libCppForInterop2.dll", EntryPoint = "CPersonne_Delete", CharSet = CharSet.Unicode)]
public static extern void DeleteCPersonne(IntPtr cp);
[DllImport("libCppForInterop2.dll", EntryPoint = "?afficheNomPrenom@CPersonne@@QAEXXZ",
CharSet = CharSet.Unicode, CallingConvention = CallingConvention.ThisCall)]
public static extern void afficheNomPrenom(IntPtr thisptr);

static void Main(string[] args)
{
IntPtr cp = NewCPersonne("pyright", "nico");
afficheNomPrenom(cp);
DeleteCPersonne(cp);
}
}
}

Bien sûr, nous avons réutilisé l'outil dumpbin.exe pour trouver le nom de la fonction et nous avons rajouté le charset unicode.
Et nous pouvons constater dans ce cas que la cohabitation se fait bien entre le type String et le type wchar_t. C'est un cas d'école, ici, le Marshalleur prend en charge la conversion, en général cela s'avère beaucoup plus compliqué quand on cherche à faire communiquer un tableau de conteneur de pointeurs de tableau de bytes (ou autre…).

Pour certains types (Byte, Int16, IntPtr, etc.), il n'y aucun besoin de conversion. On appelle les types qui ont une représentation commune à la fois dans le monde managé et natif, des types blittables. Vous pourrez retrouver à cette adresse, une correspondance dans les types C et les classes .Net.

Vous pouvez télécharger la bibliothèque réarrangée C++ à cette adresse : Télécharger ici (10 ko)
Vous pouvez télécharger le nouveau programme de test C# à cette adresse : Télécharger ici (10 ko)

4.Wrapper C++/CLI

Nous avons donc vu qu'il était possible d'interagir assez simplement avec une classe C++ en remplaçant les std::string par des wchar_t qui ont une correspondance aisée avec les String de .Net.
Mais que faire si on ne peut pas modifier les sources de la bibliothèque ou si on souhaite faire passer des valeurs ésotériques qui n'ont aucune chance d'avoir un équivalent en .Net ?
Une des solutions, la plus adaptée à mon avis, est de faire un wrapper en C++/CLI.
Il y a plusieurs avantages à cela :
- ne pas toucher à la bibliothèque existante (on ne peut pas toujours, surtout si elle est fournie par un tiers) ;
- créer une assembly en C++/CLI qui englobe la bibliothèque existante, et qui permet son utilisation dans n'importe quel langage .Net, sans avoir à se soucier d'interopérabilité ultérieurement ;
- le C++/CLI est le langage idéal pour gérer l'interopérabilité, plus facile et plus rapide, comme nous allons le voir ;
- cela permet d'avoir une interface unique en .net d'accès à la bibliothèque, et on peut même envisager de créer une abstraction objet supplémentaire.

4.1.Un exemple de wrapper

Reprenons notre classe C++ qui utilise le type std::string et créons une nouvelle version avec le constructeur qui prend en paramètres 2 std::string, puis comme précédemment, créons un .lib et une dll.
Dans le Personne.h, ne change que cette ligne (on ajoute deux paramètres au constructeur):

extern "C" __declspec(dllexport) CPersonne* CPersonne_New(string nom, string prenom);

Et dans le .cpp, l'implémentation change de cette façon :

extern "C" __declspec(dllexport) CPersonne* CPersonne_New(string nom, string prenom)
{
return new CPersonne(nom, prenom);
}

Rien d'extraordinaire…
Créons ensuite un projet CLR class library afin d'avoir notre assembly wrapper. Puis créons une classe ref CliWrapperCPersonne.
Le but de cette classe managée est donc d'utiliser la dll et de proposer une interface aisée d'utilisation.
En tant que bibliothèque, notre dernière version fournit le .h avec les entêtes, le .lib et la dll. Nous allons donc les utiliser dans le wrapper, en incluant le .h et en liant le .lib.

#include "personne.h"

#pragma comment (lib, "libCppForInterop3.lib")

Je pourrais envisager de proposer une nouvelle interface, mais celle-ci me convient, donc je reprends l'idée générale. La structure générale aura donc l'allure ci-dessous :

namespace cliWrapper {

public ref class CliWrapperCPersonne
{
private:
CPersonne * cp;
public:
CliWrapperCPersonne(String ^nom, String ^prenom)
{
cp = CPersonne_New(nom, prenom); // ne compilera pas, il manque évidement une conversion
}
~CliWrapperCPersonne()
{
CPersonne_Delete(cp);
}
protected:
!CliWrapperCPersonne()
{
CPersonne_Delete(cp);
}
public:
System::Void Affiche()
{
cp->afficheNomPrenom();
}
};
}

On a donc un membre privé qui contient un pointeur vers notre objet natif.
Un constructeur qui appelle la fonction qui sert de constructeur dans notre dll.
Un destructeur et un finalizer qui appellent la fonction qui sert de destructeur dans notre dll.
Et enfin, une méthode Affiche qui appelle la méthode afficheNomPrenom de notre classe.

N. B. Notez qu'on ne peut pas utiliser autre chose qu'un pointeur pour accéder à l'objet CPersonne, déjà parce que notre librairie est construite ainsi et ensuite parce que pour être une classe conforme .Net, afin que C# ou d'autres langages .Net puissent l'instancier, on ne peut pas encapsuler de types natifs, mais uniquement des pointeurs. Le compilateur nous aurait avertis par l'erreur suivante :

error C4368: cannot define 'cp' as a member of managed 'cliWrapper::CliWrapperCPersonne': mixed types are not supported

Il ne nous reste plus qu'à convertir les chaînes .net String en chaîne natives string de la STL et on pourra utiliser alors notre wrapper.
On va s'inspirer de la FAQ C++/CLI pour effectuer notre conversion. Ci-dessous le code complet du wrapper.

// cliWrapper.h

#pragma once

#include "personne.h"
#include <windows.h>
#include <vcclr.h>

using namespace System;

#pragma comment (lib, "libCppForInterop3.lib")

namespace conversion{
static string convertStringToStlString (String ^ chaine)
{
char * chaineChar;
pin_ptr<const wchar_t> wch = PtrToStringChars(chaine);
int taille = (chaine->Length+1) * 2;
chaineChar = new char[taille];
int t = WideCharToMultiByte(CP_ACP, 0, wch, taille, NULL, 0, NULL, NULL);
WideCharToMultiByte(CP_ACP, 0, wch, taille, chaineChar, t, NULL, NULL);
std::string chaineSTL = chaineChar;
delete chaineChar;
return chaineSTL;
}
}

namespace cliWrapper {

public ref class CliWrapperCPersonne
{
private:
CPersonne * cp;
public:
CliWrapperCPersonne(String ^nom, String ^prenom)
{
string nomStd = conversion::convertStringToStlString(nom);
string prenomStd = conversion::convertStringToStlString(prenom);
cp = CPersonne_New(nomStd, prenomStd);
}
~CliWrapperCPersonne()
{
CPersonne_Delete(cp);
}
protected:
!CliWrapperCPersonne()
{
CPersonne_Delete(cp);
}
public:
System::Void Affiche()
{
cp->afficheNomPrenom();
}
};
}

Il ne nous reste plus qu'à créer un projet de test, que l'on compile en /clr:pure pour montrer qu'on pourra l'utiliser dans n'importe quel langage .Net, comme on le ferra plus loin en C#.
Ne pas oublier de référencer l'assembly, comme décrit dans la FAQ C++/CLI. Ne pas oublier non plus la bibliothèque dans le même répertoire que l'assembly.

using namespace cliWrapper;

int main(array<System::String ^> ^args)
{
CliWrapperCPersonne ^cliWrapperCPersonne = gcnew CliWrapperCPersonne("Pyright", "Nico");
cliWrapperCPersonne->Affiche();
return 0;
}

Et le tour est joué.
Faisons la même chose en C# pour montrer la facilité d'utilisation et la propreté du wrapper C++/CLI.

using System;
using cliWrapper;

namespace usingCliWrapperWithCSharp
{
class Program
{
static void Main(string[] args)
{
CliWrapperCPersonne cp = new CliWrapperCPersonne("Pyright", "Nico");
cp.Affiche();
}
}
}

N.B.
- La classe wrapper doit absolument être publique pour que les autres langages .net puissent l'utiliser, conformément aux normes CLS.
- Il aurait pu être judicieux de lever une exception dans le wrapper si on ne trouve pas la bibliothèque, cette exception .Net pouvant ensuite être récupérée depuis C#.
- C'est le garbage collector qui s'occupera d'appeler le finalizer ou le destructeur, et permettra ainsi la libération des ressources utilisées.

4.2.Marshall et conversions

Nous avons vu comment convertir une String managée en string native de la STL. Retrouvez d'autres conversions dans la FAQ C++/CLI

4.3.Téléchargements

Vous pouvez télécharger la dernière version de la bibliothèque C++ à cette adresse : Télécharger ici (10 ko)
Vous pouvez télécharger le wrapper C++/CLI à cette adresse : Télécharger ici (31 ko)
Vous pouvez télécharger le programme de test en C++/CLI à cette adresse : Télécharger ici (78 ko)
Vous pouvez télécharger le programme de test en C# à cette adresse : Télécharger ici (40 ko)

5.Performances

Le C++/CLI est le langage le plus puissant et le plus efficace pour l'interopérabilité, cependant il y a quand même des choses à savoir.
Chaque transition est coûteuse entre le monde managé et le monde non managé. En effet, il s'agit de transitions spéciales du compilateur/linker appelées Thunk en anglais. Elles sont appelées à chaque fois que l'on passe du monde managé à un monde non managé.
D'où, on préférera limiter les transitions et regrouper un maximum d'appels lors de ces transitions.
On évitera par exemple ce type d'appel :

FaitUnePremiereChoseDansMaDllNative();
FaitUneDeuxiemeChoseDansMaDllNative();
FaitUneTroisiemeChoseDansMaDllNative();
FaitUneQuatriemeChoseDansMaDllNative();
FaitUneCinquiemeChoseDansMaDllNative();

Et on préféra ce type d'appel :

FaitCinqChosesDansMaDllNative();

Comme dit l'expression, chunky is better than chatty.
Il faudra aussi faire attention à ne pas trop bousculer le garbage collector, en évitant les pointeurs épingles. On utilisera plutôt le destructeur (Dispose) que le finalizeur.
De même, le mécanisme de boxing et le marshalling ralentissent considérablement les performances. On aura à cœur d'utiliser au maximum le même encodage (unicode par exemple) des deux côtés. On préférera bien évidemment des types blittables pour le marshalling.

6.Conclusion

Voilà, voici les bases de l'interopérabilité. Vous devriez avoir compris grâce à ce tutoriel les méthodes qui permettent d'utiliser des bibliothèques ou du code natif depuis une application .Net.
Je vous ai également montré comment Marshaller certaines données simples et comment implémenter un wrapper en C++/CLI.

Il existe encore un autre moyen de faire de l'interopérabilité, il s'agit de l'interopérabilité avec les objets COM. C'est une méthode que je n'ai pas décrite ici, car elle est assez simple à utiliser et à mettre en place. N'hésitez pas à consulter le tutoriel de Jean Marc Rabilloud.

Après tout ça, vous devez, j'imagine, vous poser la question suivante : quelle est la meilleure solution d'interopérabilité ?

La réponse est bien sûr dépendante de ce que l'on fait actuellement (C#, C++/CLI, MFC, Win32, etc. …) et de ce que l'on a besoin d'utiliser (Assembly, bibliothèque C native, API Win32, etc. …).
La meilleure solution est à mon avis la toute première décrite, à savoir le mixage de sources natives et de C++/CLI. Il faut cependant disposer des sources natives, ce qui n'est pas toujours le cas. Cette solution est la plus rapide à mettre en place, on a en effet juste besoin de compiler avec les bons modes de compilation. Elle est aussi la plus rapide en termes de performances. Enfin, elle permet d'avoir une très grande souplesse dans l'utilisation des ressources natives et des interactions avec le monde managé.

Pour un développeur C#, la solution la plus performante est bien entendu l'utilisation de DllImport, mais a l'inconvénient d'alourdir le code et peut poser des difficultés de marshalling. La solution la plus simple pour un développeur C# étant à base de COM interop, le wrapper étant généré automatiquement.
Enfin, si on est un développeur C++/CLI et qu'on a pas la possibilité de compiler des sources natives, la création d'un wrapper reste une très bonne approche, souple et homogène. Au niveau des performances, elle est aussi très acceptable et permet de mieux maîtriser son interopérabilité.

J'espère que ce tutoriel vous aura permis d'appréhender les concepts de l'interopérabilité et vous aura fait vous rendre compte que le C++/CLI est le langage par excellence pour gérer efficacement l'interopérabilité.

Remerciements

Je remercie toute l'équipe C++, particulièrement Farscape, et l'équipe Dotnet, notamment Ditch, pour leur relecture attentive du document.

Contact

Si vous constatez une erreur dans le tutoriel, dans le source, dans la programmation ou pour toutes informations, n'hésitez pas à me contacter par mail, ou par le forum.