Tutoriel : Introduction à l'interopérabilité (partie 2) - C++ Interop

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 mécanismes de COM Interop et du Hosting de CLR. ?

- Titre : Tutoriel : Introduction à l'interopérabilité (partie 2) - C++ Interop

- Auteur : Nico-pyright(c)

- Parution : 29 octobre 2007

- Mise à jour : 29 octobre 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.Interopérabilité C avec C# (c++ interop)

- 2.1.Mapper une structure native d'entiers (C++ -> C#)

- 2.2.Mapper une structure native complexe (chaine et sous structure) (C++ -> C#)

- 2.2.1.Marshaler les IntPtr

- 2.2.2.MarshalAs

- 2.3.Mise à jour d'une structure native (C# -> C++)

- 2.4.Utilisation de tableaux (c++ interop)

- 2.5.Utiliser un tableau dans une structure, le problème des tableaux à plusieurs dimensions

- 2.6.Mapper des événements managés dans une classe native

- 2.7.Marshaller des pointeurs de fonction natifs

- 3.Interopérabilité avec COM (COM Interop)

- 3.1.Démarrer avec COM Interop

- 3.2.Passage de paramètres à une méthode

- 3.3.Passage de paramètres au constructeur, wrapper de classes pour COM Interop

- 3.4.Wrapper de classes pour COM Interop, simplification et utilisation d'interfaces

- 3.5.Conserver la signature des méthodes

- 4.Wrapping inverse

- 5.Hosting CLR

- 6.Intercepter des exceptions managées depuis du code natif

- 9.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 de pouvoir faire du COM Interop ? 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 connaitre des mécanismes avancés d'interopérabilité ?
Alors ce tutoriel est pour vous.

Vous apprendrez dans ce cours comment utiliser des structures C plus ou moins complexes dans un programme C#. Vous apprendrez aussi comment fonctionne COM Interop ainsi que le hosting de CLR afin d'utiliser des objets .Net dans une application native.

Cet article est la deuxième partie du tutoriel d'introduction à l'interopérabilité. N'hésitez pas à aller consulter la première partie en préambule.

2.Interopérabilité C avec C# (c++ interop)

En préambule, je tiens à faire remarquer l'importance de la libération de ressources qui doit en général se faire du même côté que son allocation. Par exemple, si dans une méthode native est renvoyée une zone de mémoire allouée, il ne faudra pas oublier de proposer du côté natif une méthode qui permette de libérer la mémoire. Une méthode Free() personnelle par exemple …

2.1.Mapper une structure native d'entiers (C++ -> C#)

Beaucoup de bibliothèques C utilisent des structures pour regrouper des données sémantiquement.
Nous allons voir ici comment réutiliser des structures d'une bibliothèque C en C# avec DllImport.

Tout d'abord, créons une bibliothèque C : (nouveau projet WIN32, application type dll, empty project).

Créons un nouveau fichier .cpp et créons une structure simple :

typedef struct {
int telfixe;
int telPort;
} MASTRUCTURETEL;

Et ensuite créons une fonction qui retourne un pointeur sur une structure remplie, n'oublions pas l'exportation et le extern « C » pour éviter la décoration de la fonction.

extern "C" __declspec(dllexport) MASTRUCTURETEL * GetUneStructure()
{
MASTRUCTURETEL *maStruct = new MASTRUCTURETEL;
maStruct->telfixe = 123;
maStruct->telPort = 456;
return maStruct;
}

Le compilateur nous génère un .lib et un .dll.

Créons maintenant un projet C# qui va utiliser cette dll.

Tout d'abord on a besoin du namespace System.Runtime.InteropServices pour utiliser DllImport.
Pour mapper la structure maintenant, on va créer une classe :

[StructLayout(LayoutKind.Sequential)]
public class MaStructTelCSharp
{
public Int32 telFixe;
public Int32 telMobile;
}

Pour mapper un entier, on utilise bien sûr un Int32.
Pour mapper correctement la structure, on va avoir besoin de préciser l'attribut StructLayout à Sequential pour préserver l'alignement mémoire.
Exportons ensuite la fonction :

[DllImport("libCStruct.dll")]
public static extern IntPtr GetUneStructure();

Dans le main, maintenant, appelons cette fonction et récupérons un pointeur natif vers notre structure :

IntPtr maStructureCUnmanaged = GetUneStructure();

Puis marshallons ces données vers notre classe « structure » créée pour l'occasion grâce à Marshal.PtrToStructure :

MaStructTelCSharp maStructureCSharp = new MaStructTelCSharp();
Marshal.PtrToStructure(maStructureCUnmanaged, maStructureCSharp);

Maintenant, on peut utiliser la structure pour afficher les informations, par exemple :

Console.WriteLine(maStructureCSharp.telFixe);
Console.WriteLine(maStructreCSharp.telMobile);

N'oubliez pas de copier le fichier dll dans le répertoire debug du projet C#.
Exécutez, on récupère bien les informations.

2.2.Mapper une structure native complexe (chaine et sous structure) (C++ -> C#)

2.2.1.Marshaler les IntPtr

On va maintenant compliquer un peu la structure pour voir différents scénarios d'interopérabilité.

Prenons les structures suivantes :

typedef struct {
int telfixe;
int telPort;
} MASTRUCTURETEL;

typedef struct {
int monAge;
int maTaille;
char *nom;
MASTRUCTURETEL structTel;
wchar_t *prenom;
} MASTRUCTURE;

On a donc une structure qui contient des entiers, une chaine (char *), une autre chaine (wchar_t *) et une structure qui contient elle-même des entiers.

Et maintenant, la fonction qui remplit la structure et retourne un pointeur.

extern "C" __declspec(dllexport) MASTRUCTURE * GetUneStructure()
{
MASTRUCTURE *maStruct = new MASTRUCTURE;
maStruct->maTaille = 185;
maStruct->monAge = 27;
maStruct->nom = new char[20];
maStruct->prenom = new wchar_t[20];
strcpy_s(maStruct->nom, 20, "pyright");
wcscpy_s(maStruct->prenom, 20, L"nico");
maStruct->structTel.telfixe = 123;
maStruct->structTel.telPort = 456;
return maStruct;
}

Reste maintenant à mapper cette structure un peu plus complexe en C# :

[StructLayout(LayoutKind.Sequential)]
public class MaStructCSharp
{
public Int32 monAge;
public Int32 maTaille;
public IntPtr nom;
public MaStructCSharpTel maStructTel;
public IntPtr prenom;
}
[StructLayout(LayoutKind.Sequential)]
public class MaStructCSharpTel
{
public Int32 telFixe;
public Int32 telPort;
}

Finalement, ce n'est pas si compliqué, les entiers deviennent des Int32, les chaines deviennent des IntPtr et la sous-structure une classe.
On reprend le même principe pour remplir la classe avec le pointeur sur la structure :

IntPtr maStructureCUnmanaged = GetUneStructure();
MaStructCSharp maStructureCSharp = new MaStructCSharp();
Marshal.PtrToStructure(maStructureCUnmanaged, maStructureCSharp);
Console.WriteLine(maStructureCSharp.monAge);
Console.WriteLine(maStructureCSharp.maTaille);
Console.WriteLine(maStructureCSharp.maStructTel.telFixe);
Console.WriteLine(maStructureCSharp.maStructTel.telPort);

Notez que pour la sous-structure, il n'y a rien de particulier à faire.
Viennent maintenant les chaines, on va utiliser des méthodes de la classe Marshal pour les convertir :

string nom = Marshal.PtrToStringAnsi(maStructureCSharp.nom);
Console.WriteLine(nom);
string prenom = Marshal.PtrToStringUni(maStructureCSharp.prenom);
Console.WriteLine(prenom);

PtrToStringAnsi pour convertir le char * et PtrToStringUni pour convertir le wchar_t *.
Assez simple globalement, mais les conversions de chaines sont assez lourdes. On est obligé de passer par un pointeur puis de le convertir, en utilisant une autre variable.

2.2.2.MarshalAs

N'y aurait-il pas un moyen d'avoir directement des chaines C# dans notre structure plutôt que des pointeurs et d'être obligé de convertir ?
Bien sûr que si, la classe de marshal propose des attributs pour effectuer directement des opérations de marshaling sur des types qui ont un équivalent simple en C#.
Par exemple pour des chaines wchar_t *, on aura la structure C :

typedef struct {
wchar_t *nom;
wchar_t *prenom;
} MASTRUCTURE;

dont l'équivalent sera :

[StructLayout(LayoutKind.Sequential)]
public class MaStructCSharp
{
[MarshalAs(UnmanagedType.LPWStr)]
public string nom;
[MarshalAs(UnmanagedType.LPWStr)]
public string prenom;
}

On déclare directement des string et on utilise l'attribut MarshalAs et on précise le type, ici on va utiliser le paramètre UnmanagedType.LPWStr.
On pourra désormais récupérer les noms et prénoms ainsi :

IntPtr maStructureCUnmanaged = GetUneStructure();
MaStructCSharp maStructureCSharp = new MaStructCSharp();
Marshal.PtrToStructure(maStructureCUnmanaged, maStructureCSharp);
Console.WriteLine(maStructureCSharp.nom);
Console.WriteLine(maStructureCSharp.prenom);

Le code s'en trouve clarifié.

2.3.Mise à jour d'une structure native (C# -> C++)

Maintenant nous allons voir comment utiliser une dll pour mettre à jour une structure native préalablement construite en C#.
Soit la structure C suivante :

typedef struct {
int age;
int taille;
} MASTRUCTURE;

et la fonction suivante qui ajoute 1 :

extern "C" __declspec(dllexport) void Inc(MASTRUCTURE * s)
{
s->age++;
s->taille++;
}

L'équivalent C# est :

[StructLayout(LayoutKind.Sequential)]
public class MaStructCSharp
{
public int age;
public int taille;}

Et la déclaration DllImport :

[DllImport("libCStruct.dll")]
public static extern void Inc(IntPtr s);

Nous allons donc instancier depuis C# une nouvelle structure C# :

MaStructCSharp s = new MaStructCSharp();
s.age = 27;
s.taille = 185;

Le but maintenant est d'obtenir un pointeur natif sur cette structure C#, commençons par allouer une zone mémoire native de la taille de la structure :

IntPtr sI = Marshal.AllocHGlobal(Marshal.SizeOf(s));

Puis on va convertir la structure managée en pointeur natif :

Marshal.StructureToPtr(s, sI, true);

Il ne nous reste plus qu'à appeler la méthode d'incrémentation et à reconvertir le résultat dans notre structure C#, en utilisant PtrToStructure :

Inc(sI);
Marshal.PtrToStructure(sI, s);
Console.WriteLine(s.age);
Console.WriteLine(s.taille);

Maintenant que nous avons vu cette façon de faire, je me dois de vous présenter une autre façon de traiter l'interopérabilité des structures. On peut avoir le même résultat que ci-dessus en déclarant notre structure comme ceci, en utilisant le mot clé struct :

[StructLayout(LayoutKind.Sequential)]
public struct MaStructCSharp
{
public int age;
public int taille;
}

Si on déclare le DllImport avec une référence :

[DllImport("libCStruct.dll")]
public static extern void Inc(ref MaStructCSharp s);

On pourra faire notre incrémentation avec ce code :

MaStructCSharp s = new MaStructCSharp();
s.age = 27;
s.taille = 185;
Inc(ref s);
Console.WriteLine(s.age);
Console.WriteLine(s.taille);

2.4.Utilisation de tableaux (c++ interop)

En C, on manipule beaucoup de tableaux. Comment interopérer des tableaux entre une bibliothèque C et un programme C# ?
Imaginons que j'ai une dll C qui ait des méthodes de tri de tableaux, et particulièrement une méthode qui me trie un tableau d'entier.
J'ai un programme C# qui (ô hasard) utilise un tableau d'entier et (comble de malchance) je ne connais pas la méthode Sort de la classe Array du framework Dotnet.
Je vais donc utiliser ma bibliothèque C pour trier mon tableau d'entier C#. Considérons cette fonction de ma bibliothèque C :

extern "C" __declspec(dllexport) void triCpp(int *t, int longueur)
{
int i, inversion;
do
{
inversion = 0;
for(i=0; i < longueur - 1 ; i++)
{
if (t[i]>t[i+1])
{
int temp;
temp = t[i];
t[i] = t[i+1];
t[i+1] = temp;
inversion = 1;
}
}
longueur--;
}
while (inversion);
}

Cette fonction prend un pointeur sur un tableau d'entier en paramètre, ainsi que la longueur du tableau (nombre d'éléments). Il s'agit d'un tri classique par ordre croissant.
En C#, je déclare la fonction :

[DllImport("libCStruct.dll")]
public static extern void triCpp(IntPtr t, int longueur);

Je passe donc un pointeur (IntPtr) et un entier.
Ensuite, je crée un tableau d'entier non trié, que j'affiche sur la console :

int[] tableau = { 10, 2, 8, 4, 3, 15, 0, 1 };
Console.WriteLine("-------------");
int size = tableau.Length;
for (int i = 0; i < size; i++)
Console.WriteLine(tableau[i]);

J'alloue (avec AllocHGlobal) ensuite un espace mémoire correspondant à la taille d'un élément du tableau multiplié par le nombre d'éléments du tableau :

IntPtr tabUnmanaged = Marshal.AllocHGlobal(Marshal.SizeOf(tableau[0]) * size);

J'utilise la fonction Marshal.Copy pour remplir cet espace mémoire avec les données de mon tableau managé :

Marshal.Copy(tableau, 0, tabUnmanaged, size);

Puis j'appelle ma fonction de tri qui met à jour le tableau natif :

triCpp(tabUnmanaged, size);

Il ne me reste plus qu'à faire l'opération inverse, c'est-à-dire copier le tableau natif dans mon tableau C# pour effectuer la mise à jour :

Marshal.Copy(tabUnmanaged, tableau, 0, size);
Console.WriteLine("-------------");
for (int i = 0; i < size; i++)
Console.WriteLine(tableau[i]);

Sur la console s'affiche désormais mon tableau trié.

2.5.Utiliser un tableau dans une structure, le problème des tableaux à plusieurs dimensions

Imaginons maintenant que je veux utiliser la structure suivante en C dans mon programme C# :

typedef struct
{
int val;
int telfixe[10][2];
} MASTRUCTURETEL;

Nous avons cette fois-ci un tableau à deux dimensions. Comment faire ?
Comme auparavant, je vais créer une dll qui va exporter une fonction qui me retourne une structure :

typedef struct
{
int val;
int telfixe[10][2];
} MASTRUCTURETEL;

extern "C" __declspec(dllexport) MASTRUCTURETEL * GetUneStructure()
{
MASTRUCTURETEL *maStruct = new MASTRUCTURETEL;
for (int i = 0 ; i < 10 ; i++)
for (int j = 0; j < 2 ; j++)
maStruct->telfixe[i][j] = (i+1)*(j+1);
maStruct->val = 10;
return maStruct;
}

J'initialise mon tableau avec un peu n'importe quoi, mais bon voilà, c'est exactement le tableau que je voulais avec les valeurs que je voulais.
Maintenant, je veux pouvoir l'utiliser dans mon programme C#.
Il faut savoir que le marshaleur ne sait pas marshaler des tableaux multidimensionnels, et donc que fatalement, nous allons récupérer un tableau à une seule dimension. Pour ce faire, on crée la structure C# ainsi :

[StructLayout(LayoutKind.Sequential)]
public class MaStructTelCSharp
{
public int val;
[MarshalAs(UnmanagedType.ByValArray, SizeConst=10*2, ArraySubType=UnmanagedType.SysInt)]
public int[] telFixe;
}

L'important est de noter la taille du tableau dans le paramètre SizeConst de l'attribut MarshalAs. On lui précise aussi qu'on veut récupérer des entiers.
Puis, on récupère la structure ainsi :

[DllImport("testLib.dll")]
public static extern IntPtr GetUneStructure();

...
IntPtr maStructureCUnmanaged = GetUneStructure();
MaStructTelCSharp maStructureCSharp = new MaStructTelCSharp();
Marshal.PtrToStructure(maStructureCUnmanaged, maStructureCSharp);

Finalement donc, on se retrouve avec une structure qui contient un tableau à une dimension au lieu d'un tableau à deux dimensions.
C'est triste, mais c'est ainsi. Rien ne vous empêche de reconstruire éventuellement un tableau à deux dimensions à partir de notre tableau à une dimension.
Comme ici, on crée une propriété qui construit un tableau à deux dimensions à partir du tableau à une dimension :

[StructLayout(LayoutKind.Sequential)]
public class MaStructTelCSharp
{
public int val;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10 * 2, ArraySubType = UnmanagedType.SysInt)]
public int[] telFixe;
public int[][] TelFixe
{
get { return Get2DimensionalArrayFromOneDimensionArray(telFixe, 10, 2); }
}
private static int[][] Get2DimensionalArrayFromOneDimensionArray(int[] array, int length, int dimension)
{
int[][] tab = new int[length][];
for (int i = 0; i < length; i++)
{
tab[i] = new int[dimension];
for (int j = 0; j < dimension; j++)
tab[i][j] = array[i * dimension + j];
}
return tab;
}
}

[DllImport("testLib.dll")]
public static extern IntPtr GetUneStructure();

static void Main()
{
IntPtr maStructureCUnmanaged = GetUneStructure();
MaStructTelCSharp maStructureCSharp = new MaStructTelCSharp();
Marshal.PtrToStructure(maStructureCUnmanaged, maStructureCSharp);
int[][] tab = maStructureCSharp.TelFixe;
for (int i = 0; i < tab.Length; i++)
{
for (int j = 0; j < tab[i].Length; j++)
Console.Write(" {0}", tab[i][j]);
Console.WriteLine("");
}
}

2.6.Mapper des événements managés dans une classe native

De la même façon qu'on peut s'abonner à des événements managés depuis une classe C++/CLI (voir dans la faq C++/CLI), il est possible de s'abonner à des événements CLI depuis une classe native.
Pour ce faire, nous avons besoin d'utiliser un des templates gcroot ou auto_gcroot et d'une macro un peu particulière : MAKE_DELEGATE.
Voici un exemple complet :

#include <msclr/auto_gcroot.h>
#include <msclr/event.h>

using namespace System;

delegate void MaMethodeHandler(String ^s, EventArgs ^arg);

ref class MaClasse
{
public:
event MaMethodeHandler ^OnMaMethode;
void Run(String^ s, EventArgs ^arg)
{
OnMaMethode(s, arg);
}
};

class TestMaClaseUnmanaged
{
public:
TestMaClaseUnmanaged(MaClasse ^_m)
{
m = _m;
m->OnMaMethode+=MAKE_DELEGATE(MaMethodeHandler, DoStuff);
}
BEGIN_DELEGATE_MAP(TestMaClaseUnmanaged)
EVENT_DELEGATE_ENTRY(DoStuff, String^, EventArgs^)
END_DELEGATE_MAP()
private:
msclr::auto_gcroot<MaClasse^> m;
void DoStuff(String ^ s, EventArgs ^arg)
{
pin_ptr<const wchar_t> pinptrStr = PtrToStringChars(s);
wprintf(L"%s\n", pinptrStr);
}
};

int main()
{
MaClasse ^m = gcnew MaClasse();
TestMaClaseUnmanaged* t = new TestMaClaseUnmanaged(m);
m->Run("truc", nullptr);
}

On a donc notre classe native TestMaClaseUnmanaged qui wrappe la classe managée grâce à auto_gcroot :

msclr::auto_gcroot<MaClasse^> m;

L'abonnement a l'événement se fait de la manière suivante :

m->OnMaMethode+=MAKE_DELEGATE(MaMethodeHandler, DoStuff);

On lui fournit le delegate ainsi que la méthode qui sera appelée lors du déclenchement de l'événement (qui doit bien sûr avoir la même signature que le delegate).
Enfin, il ne reste plus qu'à faire l'association avec les paramètres de l'événement.

BEGIN_DELEGATE_MAP(TestMaClaseUnmanaged)
EVENT_DELEGATE_ENTRY(DoStuff, String^, EventArgs^)
END_DELEGATE_MAP()

Et voilà.
Un domaine d'application par exemple serait l'intégration d'un contrôle .Net dans une application MFC (plus de précisions dans la faq C++/CLI)

2.7.Marshaller des pointeurs de fonction natifs

Il peut être également utile de pouvoir utiliser des pointeurs de fonction natif à partir d'une application managée. Imaginons que nous ayons cette classe native : (j'utilise ici les pragma unmanaged et managed pour simuler une bibliothèque native) :

#pragma unmanaged
namespace Natif
{
typedef void (__stdcall * pFonction)(int i);

class MaClasse
{
pFonction _pfn;
public:
MaClasse(pFonction pfn)
{
_pfn = pfn;
}
void MaFonction(int val)
{
_pfn(val);
}
};
}

void __stdcall FaitQqchoseDeNatif(int i)
{
std::cout << "Valeur : " << i << std::endl;
}

#pragma managed

On pourra utiliser en natif cette classe et notre fonction de cette façon par exemple :

Natif::MaClasse* c = new Natif::MaClasse(FaitQqchoseDeNatif);
c->MaFonction(5);
delete c;

qui affichera bien sûr :

Valeur : 5

Maintenant, nous voulons faire la même chose en .Net. L'équivalent des pointeurs de fonctions natifs est bien sûr le delegate de .Net.

[UnmanagedFunctionPointer(CallingConvention::StdCall)]
public delegate void FonctionDelegate(int i);

void FaitQqchoseDeManaged(int i)
{
Console::WriteLine("Valeur : {0}", i);
}

et pour utiliser la classe dans du code C++/CLI, nous ferons :

FonctionDelegate^ monDelegate = gcnew FonctionDelegate(FaitQqchoseDeManaged);
IntPtr p = Marshal::GetFunctionPointerForDelegate(monDelegate);
Natif::MaClasse* c1 = new Natif::MaClasse((Natif::pFonction)p.ToPointer());
c1->MaFonction(10);
delete c1;

On voit ici la construction du delegate qui est précédée de l'attribut UnmanagedFunctionPointer qui va permettre de contrôler le marshalling de la signature du delegate.
Nous utilisons ensuite la méthode GetFunctionPointerForDelegate pour récupérer un pointeur de fonction à partir du delegate.
Nous aurons donc en sortie :

Valeur : 10

N. B. Attention, j'ai ici défini explicitement la convention d'appel de ma callback à __stdcall, par défaut dans un programme natif, elle est positionnée à __cdecl. Il faudra dans ce cas-là utiliser cette convention d'appel pour l'attribut UnmanagedFunctionPointer .

3.Interopérabilité avec COM (COM Interop)

Le modèle COM (Component Object Model) est une plateforme qui permet la communication interprocess et la création d'objet dynamique et ceci, peu importe le langage de développement employé qui supporte la technologie.
COM est beaucoup utilisé sous Windows encore à l'heure actuelle même s'il tend à être remplacé par .Net.

3.1.Démarrer avec COM Interop

Il peut être parfois utile de pouvoir appeler du code managé depuis un programme complètement natif.
Pour cela, plusieurs solutions dont une assez pratique à mettre en œuvre : COM INTEROP.
On utilise CCW (COM Callable Wrappers) qui est un mécanisme de .Net pour permettre à un client COM d'accéder à des objets managés à travers un proxy COM qui encapsule l'assembly managée.
On peut voir ça comme un mécanisme de génération automatique de wrapper qui expose des interfaces COM et qui s'occupe de toutes les tâches de conversions / marshalling.
Nous allons donc voir cela à travers un exemple extrêmement simpliste, soit la classe managée suivante :

namespace assembly {

public ref class MaClasse
{
private:
String ^c;
public:
MaClasse()
{
c = "hello world";
}
void Print()
{
Console::WriteLine(c);
}
};
}

J'ai donc créé une assembly (class library) qui s'appelle assembly et qui expose une classe MaClasse, avec un constructeur par défaut, et une méthode Print qui affiche une chaine. Très basique. (je l'ai faite en C++/CLI, mais on pourrait très bien la faire en C#, le principe est naturellement le même).

Nous allons donc pouvoir procéder à la génération de notre wrapper. Pour ceci, il faut respecter les étapes suivantes :

- 1) signer l'assembly

Pour rappel, on utilise l'utilitaire sn.exe pour générer une paire clé publique, clé privée :

sn.exe -k maCle.snk

Ensuite, il faut utiliser ce fichier.
Clic droit sur le projet -> propriétés -> linker -> advanced -> Key file -> et ici mettre le chemin d'accès vers le fichier maCle.snk.

Au prochain build, l'assembly sera signée ;

- 2) Marquer l'assembly visible pour com

Pour ce faire, ouvrez le fichier AssemblyInfo.cpp, par défaut il a été généré :

[assembly:ComVisible(false)];

changez le en

[assembly:ComVisible(true)];

- 3) Enregistrer l'assembly pour com interop

en C#, il s'agit d'une option à cocher, en C++/CLI on va utiliser l'utilitaire regasm.exe.

regasm.exe assembly.dll /tlb:assembly.tlb /codebase

Cette ligne de commande va faire plusieurs choses : insérer des informations dans la base de registre (faites une recherche sur assembly.MaClasse vous verrez de quoi je parle, un prog-id et un clsid) ; générer un tlb et faire un lien vers le path de l'assembly (ce qui évite d'avoir à la mettre dans le GAC).

Vous voilà prêt à écrire le code natif en C++ pour accéder au wrapper qui accèdera à notre objet CLI.
Soit ce programme :

#include <windows.h>

int _tmain(int argc, _TCHAR* argv[])
{
DISPID dispid;
IDispatch *pDisp;
OLECHAR *methodName = L"Print";
CLSID clsID;
DISPPARAMS param;
param.cArgs=0;
param.rgvarg=NULL;
param.cNamedArgs=0;
param.rgdispidNamedArgs=NULL;

CoInitialize(NULL);
if (SUCCEEDED(::CLSIDFromProgID(L"assembly.MaClasse", &clsID)))
if (SUCCEEDED(CoCreateInstance(clsID, NULL, CLSCTX_ALL, IID_IDispatch, (void**)&pDisp)))
if (SUCCEEDED(pDisp->GetIDsOfNames(IID_NULL, &methodName,1, GetUserDefaultLCID(), &dispid)))
if (SUCCEEDED(pDisp->Invoke(dispid,
IID_NULL, GetUserDefaultLCID(), DISPATCH_METHOD, &param, NULL, NULL, NULL)))
pDisp->Release();
CoUninitialize();
return 0;
}

On commence bien sûr par appeler CoInitialize. Ensuite, on récupère le CLSID à partir du nom qui a été enregistré dans la base de registre par regasm.
Une fois qu'on a récupéré ce CLSID, on appelle CoCreateInstance pour obtenir un IDispatch et appeler le constructeur par défaut de notre classe.
On appelle ensuite GetIDsOfNames en lui passant en paramètre le nom de la méthode que l'on souhaite appeler.

Il ne reste plus qu'à invoquer la méthode en lui passant les paramètres adéquats. Je passe ici NULL en dernier et avant dernier paramètre, car je n'ai pas besoin de récupérer d'exception ou d'erreur de paramètres. Je lui passe également NULL en avant avant dernière position, car il s'agit du retour de la méthode, et comme il s'agit d'un void, il n'y a pas de retour.

Quant à &param, il s'agit de la construction des paramètres de la méthode ; il n'y en a pas non plus, je dois construire mon DISPPARAMS en lui disant qu'il n'y a pas de paramètres (cArgs=0) et en mettant le reste à null.
Enfin, on libère avec CoUninitialize();

Exécutez le programme, vous verrez sur la sortie console la phrase tant attendue : hello world.

3.2.Passage de paramètres à une méthode

Maintenant, nous allons voir comment passer des paramètres à une méthode.
soit la classe CLI suivante :

namespace assembly {

public ref class MaClasse
{
public:
MaClasse()
{
}
String ^ Print(String ^c, int val)
{
Console::WriteLine("entier passé : {0}", val);
return c + " " + DateTime::Now.ToString();
}
};
}

La méthode Print attend en paramètre une String ^ et un entier. La méthode affiche l'entier et renvoie la chaine passée en paramètre concaténée à la date du jour sous la forme d'une String ^.
Enregistrez l'assembly pour com interop comme expliqué dans le chapitre précédent. Je conseille personnellement de rajouter dans les post build events, la ligne de commande qui enregistre la dll, soit :

"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\RegAsm.exe" "$(OutDir)\assembly.dll" /tlb:"$(OutDir)\assembly.tlb" /codebase

Voici le code C++ natif qui va permettre d'appeler l'objet COM Wrapper :

#include <windows.h>
#include <iostream>

void BSTRtoASC (BSTR str, char * &strRet)
{
if ( str != NULL )
{
unsigned long length = WideCharToMultiByte (CP_ACP, 0, str, SysStringLen(str),
NULL, 0, NULL, NULL);
strRet = new char[length];
length = WideCharToMultiByte (CP_ACP, 0, str, SysStringLen(str),
reinterpret_cast <char *>(strRet), length, NULL, NULL);
strRet[length] = '\0';
}
}

int _tmain(int argc, _TCHAR* argv[])
{
DISPID dispid;
IDispatch *pDisp;
OLECHAR *methodName = L"Print";
CLSID clsID;
VARIANT vntArgs[2], vntResult;
DISPPARAMS param;
param.cArgs = 2;
param.rgvarg = vntArgs;
param.cNamedArgs = 0;
param.rgdispidNamedArgs = NULL;
vntArgs[0].vt = VT_INT;
vntArgs[0].intVal = 10;
vntArgs[1].vt = VT_BSTR;
vntArgs[1].bstrVal = SysAllocString(L"Nous sommes le");
vntResult.vt = VT_BSTR;

CoInitialize(NULL);
if (SUCCEEDED(::CLSIDFromProgID(L"assembly.MaClasse", &clsID)))
if (SUCCEEDED(CoCreateInstance(clsID, NULL, CLSCTX_ALL, IID_IDispatch, (void**)&pDisp)))
if (SUCCEEDED(pDisp->GetIDsOfNames(IID_NULL, &methodName,1, GetUserDefaultLCID(), &dispid)))
if (SUCCEEDED(pDisp->Invoke(dispid, IID_NULL,
GetUserDefaultLCID(), DISPATCH_METHOD, &param, &vntResult, NULL, NULL)))
{
char * retour;
BSTRtoASC(vntResult.bstrVal, retour);
std::cout << retour << std::endl;
pDisp->Release();
}
CoUninitialize();
return 0;
}

La partie importante est dans la construction des paramètres et dans l'utilisation du paramètre de retour.
On construit un tableau de VARIANT à deux dimensions VARIANT vntArgs[2] qui va recevoir les deux paramètres de la fonction.

Attention ! Les paramètres doivent être passés dans le sens inverse !

Pour mapper une String ^, on va utiliser un BSTR. C'est ce qu'on indique grâce à cette ligne :

vntArgs[1].vt = VT_BSTR;

La valeur passée sera :

vntArgs[1].bstrVal = SysAllocString(L"Nous sommes le");

Notez qu'on utilise le couple {VT_BSTR, bstrVal} pour faire passer une chaine.
Pour l'entier, c'est le même principe, on va utiliser le couple {VT_INT, intVal}.
Vous pouvez retrouver plus d'infos sur les VARIANT à cette adresse.
Pour le retour de la méthode, on utilise un autre variant vntResult, qu'on indique comme étant un BSTR.
Une fois l'appel à la méthode effectué, il ne reste plus qu'à traiter le retour. On utilise la méthode de conversion de la faq VC pour avoir un char * et l'afficher.

3.3.Passage de paramètres au constructeur, wrapper de classes pour COM Interop

Si la question est : « Peut-on passer des paramètres au constructeur d'un objet COM qui fait le wrapper d'une classe managée ? »
La réponse est non !

Alors, comment faire si je veux utiliser la classe System::IO::StreamReader (par exemple) depuis mon programme natif ?

Et bien, la solution est de décaler la construction dans une méthode en ayant préalablement passé les paramètres par méthode.
Et pour ce faire, il suffit d'encapsuler l'utilisation de la classe dans un wrapper qui respecte cette construction.
soit la classe ref suivante :

using namespace System;
using namespace System::IO;

namespace assembly
{
public ref class WrapStreamReader
{
private:
String ^_filePath;
StreamReader ^sr;
protected:
!WrapStreamReader()
{
if (sr)
sr->Close();
}
public:
WrapStreamReader() {}
~WrapStreamReader()
{
this->!WrapStreamReader();
}
void SetFile(String ^filePath)
{
_filePath = filePath;
}
bool Create()
{
if (!String::IsNullOrEmpty(_filePath))
{
try
{
sr = gcnew StreamReader(_filePath);
return true;
}
catch (Exception ^)
{
sr = nullptr;
return false;
}
}
return false;
}
String^ ReadLine()
{
if (sr)
return sr->ReadLine();
return nullptr;
}
};
}

C'est un exemple tout simple qui encapsule la méthode ReadLine de StreamReader.
Une utilisation en C++/CLI pourrait être la suivante :

int main(array<System::String ^> ^args)
{
WrapStreamReader^w = gcnew WrapStreamReader();
w->SetFile("c:\\test.txt");
if (w->Create())
{
String ^s = w->ReadLine();
while (!String::IsNullOrEmpty(s))
{
Console::WriteLine(s);
s = w->ReadLine();
}
}
return 0;
}

C'est cet exemple que nous allons transformer en COM.
Le code pourrait être le suivant :

#include <windows.h>
#include <iostream>

void BSTRtoASC (BSTR str, char * &strRet)
{
if ( str != NULL )
{
unsigned long length = WideCharToMultiByte (CP_ACP, 0, str, SysStringLen(str),
NULL, 0, NULL, NULL);
strRet = new char[length];
length = WideCharToMultiByte (CP_ACP, 0, str, SysStringLen(str),
reinterpret_cast <char *>(strRet), length, NULL, NULL);
strRet[length] = '\0';
}
}

int _tmain(int argc, _TCHAR* argv[])
{
DISPID dispid;
IDispatch *pDisp;
OLECHAR *methodSetFile = L"SetFile";
CLSID clsID;
VARIANT vntArgs[1], vntResult;
DISPPARAMS param;
param.cArgs = 1;
param.rgvarg = vntArgs;
param.cNamedArgs = 0;
param.rgdispidNamedArgs = NULL;
vntArgs[0].vt = VT_BSTR;
vntArgs[0].bstrVal = SysAllocString(L"c:\\test.txt");

CoInitialize(NULL);
if (SUCCEEDED(::CLSIDFromProgID(L"assembly.WrapStreamReader", &clsID)))
if (SUCCEEDED(CoCreateInstance(clsID, NULL, CLSCTX_ALL, IID_IDispatch, (void**)&pDisp)))
if (SUCCEEDED(pDisp->GetIDsOfNames(IID_NULL, &methodSetFile,1, GetUserDefaultLCID(), &dispid)))
if (SUCCEEDED(pDisp->Invoke(dispid, IID_NULL,
GetUserDefaultLCID(), DISPATCH_METHOD, &param, NULL, NULL, NULL)))
{
OLECHAR *methodCreate = L"Create";
param.cArgs = 0;
param.rgvarg = NULL;
param.cNamedArgs = 0;
param.rgdispidNamedArgs = NULL;
vntResult.vt = VT_BOOL;
if (SUCCEEDED(pDisp->GetIDsOfNames(IID_NULL, &methodCreate,1, GetUserDefaultLCID(), &dispid)))
if (SUCCEEDED(pDisp->Invoke(dispid, IID_NULL,
GetUserDefaultLCID(), DISPATCH_METHOD, &param, &vntResult, NULL, NULL)))
{
if (vntResult.boolVal != 0)
{
OLECHAR *methodReadLine = L"ReadLine";
vntResult.vt = VT_BSTR;
if (SUCCEEDED(pDisp->GetIDsOfNames(IID_NULL, &methodReadLine,1, GetUserDefaultLCID(), &dispid)))
if (SUCCEEDED(pDisp->Invoke(dispid, IID_NULL,
GetUserDefaultLCID(), DISPATCH_METHOD, &param, &vntResult, NULL, NULL)))
{
char * retour;
BSTRtoASC(vntResult.bstrVal, retour);
while (strlen(retour) > 0 && vntResult.bstrVal)
{
std::cout << retour << std::endl;
if (FAILED(pDisp->Invoke(dispid, IID_NULL,
GetUserDefaultLCID(), DISPATCH_METHOD, &param, &vntResult, NULL, NULL)))
break;
BSTRtoASC(vntResult.bstrVal, retour);
}
pDisp->Release();
}
}
}
}
CoUninitialize();
return 0;
}

Donc le principe est très simple, on appelle l'une après l'autre les méthodes SetFile, Create et ReadLine.
Je vous l'accorde, la syntaxe est un peu compliquée, et ça devient vite touffu.
Mais heureusement pour nous, il y a un moyen d'arriver à une syntaxe plus élégante grâce à visual studio.

3.4.Wrapper de classes pour COM Interop, simplification et utilisation d'interfaces

Pour arriver à une syntaxe plus simple, on va utiliser la capacité de visual studio à incorporer les tlb grâce à #import et à le convertir en classes C++.
Nous transformons notre précédente classe managée qui wrappe de manière simpliste le StreamReader pour faire en sorte que la classe implémente une interface :

using namespace System;
using namespace System::IO;

namespace MonStreamReader
{
public interface class IClassWrap
{
void SetFile(String ^fileName);
bool Create();
String^ ReadLine();
};

public ref class ClassWrap : IClassWrap
{
private:
String ^_fileName;
StreamReader ^sr;
protected:
!ClassWrap()
{
if (sr)
sr->Close();
}
public:
ClassWrap() {}
~ClassWrap()
{
this->!ClassWrap();
}
virtual void SetFile(String ^fileName)
{
_fileName = fileName;
}
virtual bool Create()
{
if (!String::IsNullOrEmpty(_fileName))
{
try
{
sr = gcnew StreamReader(_fileName);
return true;
}
catch (Exception ^)
{
sr = nullptr;
return false;
}
}
return false;
}
virtual String^ ReadLine()
{
if (sr)
return sr->ReadLine();
return nullptr;
}  };
}

On génère le tlb, et on l'importe dans notre programme natif. Visual C++ va générer des classes et notamment une qui nous intéresse IClassWrapPtr. C'est un smart pointer qui permet d'accéder à l'objet COM.
Il ne reste plus qu'à utiliser ce smart pointer pour appeler directement les méthodes de notre classe.
Notez que les fonctions ont été relookées à la mode COM. On récupère un HRESULT et le retour de fonction se fait par un paramètre en sortie [out].

#include <atlsafe.h>
#include <iostream>

#import "..\debug\MonStreamReader.tlb" raw_interfaces_only

int _tmain(int argc, _TCHAR* argv[])
{
CoInitialize(NULL);
MonStreamReader::IClassWrapPtr pClass(__uuidof(MonStreamReader::ClassWrap));
CComBSTR filePath = "c:\\test.txt";
if (SUCCEEDED(pClass->SetFile(filePath)))
{
unsigned char resultBool;
if (SUCCEEDED(pClass->Create(&resultBool)))
{
if (resultBool)
{
CComBSTR line;
if (SUCCEEDED(pClass->ReadLine(&line)))
{
while (line)
{
_bstr_t line_(line);
std::cout << line_ << std::endl;
if (FAILED(pClass->ReadLine(&line)))
break;
}
}
}
}
}
return 0;
}

C'est quand même plus digeste !!
Notez l'utilisation de CComBstr pour passer des chaines qui seront marshalées en String^ .net. Par contre, mon retour booléen donne un unsigned char.
Pour l'affichage, j'utilise également la classe _bstr_t qui dispose d'une surcharge de char *.
Vous pouvez télécharger le projet exemple (387 Ko).

n'oubliez pas d'exporter votre tlb avec la commande :

RegAsm.exe MonStreamReader.dll /tlb:MonStreamReader.tlb

3.5.Conserver la signature des méthodes

La contrainte de l'exemple précédent est que l'import transforme la signature des méthodes à la sauce COM.
Ainsi, une méthode :

String^ ReadLine();

sera wrappée en :

HRESULT ReadLine(/*[out,retval]*/ BSTR);

Ce qui suggère l'utilisation de la macro SUCCEEDED pour tester la bonne marche de la fonction.
Mais pouvons-nous encore simplifier et se passer de ce type de signature ? On aimerait pouvoir avoir une signature du genre :

BSTR ReadLine();

Bref, conserver la signature de la méthode, aux types près …
Et bien c'est possible, grâce à un attribut disponible dans le namespace System::Runtime::CompilerServices.
Notre classe managée ne change pas d'écriture, mais l'interface sera écrite ainsi :

using namespace System::Runtime::CompilerServices;

namespace MonStreamReader
{
public interface class IClassWrap
{
[MethodImpl(MethodImplOptions::PreserveSig)]
void SetFile(String ^fileName);
[MethodImpl(MethodImplOptions::PreserveSig)]
bool Create();
[MethodImpl(MethodImplOptions::PreserveSig)]
String^ ReadLine();
};

public ref class ClassWrap : IClassWrap
{
.........
}
}

L'attribut PreserveSig, comme son nom le suggère un peu, permet de préserver la signature ; et finalement de simplifier encore un brin le programme natif.

int _tmain(int argc, _TCHAR* argv[])
{
CoInitialize(NULL);
MonStreamReader::IClassWrapPtr pClass(__uuidof(MonStreamReader::ClassWrap));
CComBSTR filePath = "c:\\test.txt";
pClass->SetFile(filePath);
if (pClass->Create())
{
CComBSTR line;
line = pClass->ReadLine();
while (line)
{
_bstr_t line_(line);
std::cout << line_ << std::endl;
line = pClass->ReadLine();
}
}
return 0;
}

Dingue ça, on dirait presque du C++/CLI , à part un ou deux BSTR qui trainent…
PS : au cours de mes utilisations, j'ai appris à ne pas faire trop confiance à l'intellisense, il se débrouille pas trop mal, mais suite à quelques compilations, enregistrements… dès fois, il perd un peu le nord. Heureusement que nous savons exactement ce que nous voulons faire.

4.Wrapping inverse

Dans l'exemple de COM interop, on a voulu utiliser la classe StreamReader. Il y a une autre façon de faire, très simple.
On encapsule la classe dans une dll native qui exporte des méthodes.
créer un projet win32 dll.
Et activez l'option de compilation à /clr.

#include <msclr\auto_gcroot.h>

using namespace System;
using namespace System::IO;

using namespace msclr;

class WrapperNatifStreamReader
{
private:
auto_gcroot<StreamReader^> sr;
public:
__declspec(dllexport) WrapperNatifStreamReader(wchar_t * fileName)
{
sr = gcnew StreamReader(gcnew String(fileName));
}
__declspec(dllexport) ~WrapperNatifStreamReader()
{
sr->Close();
}
__declspec(dllexport) wchar_t * ReadLine()
{
pin_ptr<const wchar_t> line =  PtrToStringChars(sr->ReadLine());
return (wchar_t*)line;
}
};

On utilise le template auto_gcroot pour encapsuler la classe StreamReader.
Dans le cadre de notre utilisation, on n'a encapsulé que les constructeurs et destructeurs, ainsi que la méthode ReadLine.
Une fois compilée, nous obtenons un .lib et un .dll.
Et on peut l'utiliser dans un projet de test entièrement natif.

#include "stdafx.h"
#include <iostream>

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

class WrapperNatifStreamReader
{
private:
intptr_t sr;
public:
__declspec(dllexport) WrapperNatifStreamReader(wchar_t * fileName);
__declspec(dllexport) ~WrapperNatifStreamReader();
__declspec(dllexport) wchar_t * ReadLine();
};

int _tmain(int argc, _TCHAR* argv[])
{
WrapperNatifStreamReader *wnf = new WrapperNatifStreamReader(L"c:\\test.txt");
wchar_t * line = wnf->ReadLine();
while (line)
{
std::wcout << line << std::endl;
line = wnf->ReadLine();
}
delete wnf;
return 0;
}

N. B.  Pour la lisibilité, j'ai choisi d'implémenter directement les méthodes de la classe. Mais on aurait très bien pu séparer ce code dans un .h et un .cpp. Ainsi, nous aurions pu éviter de recopier la définition de la classe dans le projet de test en incluant simplement le .h. Notez quand même qu'il y a une différence entre les deux définitions de la classe. Dans l'une, on a un membre privé auto_gcroot et dans l'autre, on a un membre privé intptr_t.
Pour que le .h soit utilisable dans les deux cas, il suffit de le transformer en utilisant la macro #ifdef _MANAGED. Ainsi, notre .h ressemblerait à :

class WrapperNatifStreamReader
{
private:
#ifdef _MANAGED
auto_gcroot<StreamReader^> sr;
#else
intptr_t sr;
#endif
public:
__declspec(dllexport) WrapperNatifStreamReader(wchar_t * fileName);
__declspec(dllexport) ~WrapperNatifStreamReader();
__declspec(dllexport) wchar_t * ReadLine();
};

téléchargez WrapperNatifStreamReader.rar (640 Ko) et UseWrapper.rar (677 Ko)

5.Hosting CLR

Une autre solution pour utiliser des assemblys depuis un programme natif est d'embarquer le CLR dans notre application. On appelle ça, le hosting de CLR.
Cela se fait notamment grâce à l'API ClrCreateManagedInstance.

CComPtr<IDispatch> pDisp;
HRESULT hr = S_OK;
if (SUCCEEDED(ClrCreateManagedInstance(TEXT("MonStreamReader.ClassWrap, MonStreamReader"), IID_IDispatch, (void**)&pDisp)))
{
OLECHAR FAR* setFile = L"SetFile";
DISPID dispid;

VARIANT vntArgs[1];
DISPPARAMS param;
param.cArgs = 1;
param.rgvarg = vntArgs;
param.cNamedArgs = 0;
param.rgdispidNamedArgs = NULL;
vntArgs[0].vt = VT_BSTR;
vntArgs[0].bstrVal = SysAllocString(L"c:\\test.txt");

if (SUCCEEDED(pDisp->GetIDsOfNames (IID_NULL, &setFile, 1, LOCALE_SYSTEM_DEFAULT, &dispid)))
{
if (SUCCEEDED(pDisp->Invoke(dispid, IID_NULL,
LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &param, NULL, NULL, NULL)))
{
DISPPARAMS dispparamsNoArgs = {NULL, NULL, 0, 0};
OLECHAR FAR* create = L"Create";
if (SUCCEEDED(pDisp->GetIDsOfNames (IID_NULL, &create, 1, LOCALE_SYSTEM_DEFAULT, &dispid)))
{
VARIANT vntResult;
vntResult.vt = VT_UI1;
if (SUCCEEDED(pDisp->Invoke(dispid, IID_NULL,
LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &dispparamsNoArgs, &vntResult, NULL, NULL)))
{
if (vntResult.bVal != 0)
{
vntResult.vt = VT_BSTR;
OLECHAR FAR* readLine = L"ReadLine";
if (SUCCEEDED(pDisp->GetIDsOfNames (IID_NULL, &readLine, 1, LOCALE_SYSTEM_DEFAULT, &dispid)))
{
if (SUCCEEDED(pDisp->Invoke(dispid, IID_NULL,
LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &dispparamsNoArgs, &vntResult, NULL, NULL)))
{
CComBSTR line = vntResult.bstrVal;
while (line)
{
wcout << (BSTR)line << endl;
if (FAILED(pDisp->Invoke(dispid, IID_NULL,
LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &dispparamsNoArgs, &vntResult, NULL, NULL)))
break;
line = vntResult.bstrVal;
}
}
}
}
}
}
}
}
}

On remarque ensuite la même méthode pour invoquer les fonctions à partir des objets COM et l'utilisation des DISPPARAMS et autres VARIANT pour gérer les paramètres.
Cependant, on accèdera directement à l'assembly (sans utiliser de TLB) en utilisant son nom complet (MonStreamReader.ClassWrap, MonStreamReader) qui correspond au nom du namespace et de la classe.
N. B. L’assembly devra être située dans le même répertoire que l'exécutable ou bien être enregistrée dans le GAC.

Attention, msdn nous dit par rapport à l'api ClrCreateManagedInstance que si le CLR n'est pas chargé dans le process, alors il va charger automatiquement la version 1.0.3705. Si ce chargement échoue, alors il tentera de charger la dernière version du CLR.

Plusieurs versions du CLR peuvent cohabiter sur la même machine, comme on peut le constater en allant voir dans le répertoire %windir%\microsoftr.net\framework
Pour charger soit même le CLR (et éventuellement, le paramétrer selon sa convenance), on utilise CorBindToRuntimeEx :
Lorsque le premier paramètre vaut NULL, on aura donc la version la plus récente disponible du CLR qui sera choisie
On pourra rajouter au début du main

ICorRuntimeHost *pRuntimeHost = NULL;
if (FAILED(CorBindToRuntimeEx(NULL, NULL, 0, CLSID_CorRuntimeHost, IID_ICorRuntimeHost, (LPVOID*)&pRuntimeHost)))
{
cout << "Impossible de démarrer la dernière version du CLR" << endl;
}
else
{
pRuntimeHost->Start();
}

et à la fin :

if (pRuntimeHost != NULL)
{
pRuntimeHost->Stop();
pRuntimeHost->Release();
}

Vous pouvez télécharger le programme exemple : HostClr.rar (24 Ko)

6.Intercepter des exceptions managées depuis du code natif

Des fois, il peut être utile de pouvoir intercepter une exception managée à partir d'un code natif.
Soit le programme suivant qui simule, grâce aux pragma unmanaged et managed, une interaction entre un code natif et un code managé :

using namespace System;

void fonctionManagee()
{
throw gcnew Exception("Exception managée");
}

#pragma unmanaged
void fonctionNative()
{
fonctionManagee();
}
#pragma managed

int main()
{
fonctionNative();
return 0;
}

Si on exécute cet exemple, on obtient un crash de l'application à cause de la levée d'exception dans la méthode managée.
Le premier réflexe est d'utiliser un try catch … mais … quel type d'exception attraper ? Une solution pourrait être de tout attraper, avec une construction de ce genre :

void fonctionNative()
{
try
{
fonctionManagee();
}
catch (...)
{
std::cout << "N'importe quelle exception est attrapee, meme managee" << std::endl;
}
}

Cette construction a l'avantage de permettre d'intercepter l'exception levée dans la fonction managée depuis le code natif, mais a l'inconvénient de tout intercepter, à cause de la construction « … » suivant le catch.
La solution est d'utiliser un block try except, comme ci-dessous :

void fonctionNative()
{
__try
{
fonctionManagee();
}
__except(GetExceptionCode() == 0xE0434F4D ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
std::cout << "Uniquement les exceptions managees sont attrapees" << std::endl;
}
}

0xE0434F4D est le code d'erreur SEH correspond. (pour l'anecdote, 0xE0434F4D correspond à 0xE0+« COM » (= 'àCOM'), car à l'origine, le CLR a été appelé COM+ 2.0. Le nom du projet a changé, mais pas les codes d'exception)
L'inconvénient est qu'on ne peut pas avoir d'information sur l'exception, comme dans notre cas, le message d'exception levé.

9.Conclusion

Avec ce tutoriel nous avons pu continuer notre apprentissage des mécanismes d'interopérabilité entre le monde natif et le monde .Net. Je rappelle que la première partie du tutoriel est consultable ici.
Nous avons vu comment utiliser des structures et des tableaux dans nos programmes C et C#. De même, nous avons pu appréhender les concepts de COM Interop et les mécanismes mis en œuvre pour interagir entre COM et .Net.
Nous avons également vu les fondements du hosting de CLR.
J'espère que vous comprenez mieux à présent comment utiliser correctement les mécanismes d'interopérabilité et appréciez à sa juste valeur la formidable capacité du C++/CLI dans ce domaine.

Remerciements

Je remercie toute l'équipe C++ et l'équipe Dotnet 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.