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 à ¶m, 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
\a
uto_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.