|
e
Managed C++ (C++/CLI dans la version avenir du .Net Framework) permet
d'utiliser sur la plateforme .Net des composants ou librairies écris en C++
"natif". Cette approche est relativement alléchante lorsque l'on ne veut pas
tout réimplémenter !
Par ailleurs, il
existe peu
d'exemples sur Internet
excepté
l'utilisation d'un integer managé dans un
environnement non
managé. Pourtant, les APIs de composants ne se résument
pas qu'aux types de base. Nous allons ici présenter par un exemple simple
(mais pas simpliste :-))
la manière de porter un composant C++ natif sur la
plateforme .Net par le biais du Managed C++.
Description du Composant C++
Notre composant est constitué de 3 classes (API), une classe Source, une classe
Target et une logique de parsing implementée dans la classe Parser. Afin de
pouvoir changer les implementations de Source et Target, celles-ci sont
abstraites et ne definissent qu'une méthode abstraite read pour la Source et
write pour la target. Voila le header de chacune des classes.
class Source
{
public:
Source();
vitual ~Source();
public:
virtual void read()=0;
}
Source.h
class Target
{
public:
Target();
vitual ~Target();
public:
virtual void write()=0;
}
Target.h
class Parser
{
public:
Parser(Source* _s, Target* _t);
vitual ~Parser();
public:
void parse();
}
Parser.h
C'est ici que .Net
entre en scène, nous
avons passé beaucoup de temps à développer notre Parser
et nous ne voudrions
surtout pas tout perdre ! Pourquoi ne pas
encapsuler notre Composant en
managed C++ ?
Wrapper en Managed C++
Comment wrapper nos classes abstraites?
Une classe abstraite ne pouvant pas par nature être instanciée, il est donc
difficile de "wrapper", c'est-à-dire encapsuler une instance, on va donc
définir 2 interfaces
ISource et ITarget ayant la même signature que nos classes
abstraites. Il est vrai qu'il faudra maintenir les interfaces et les classes
abstraites synchrone. Voila donc leur prototype.
namespace Reader
{
public __gc __interface ISource
{
void read();
}
}
ISource.h
namespace Reader
{
public __gc __interface ITarget
{
void write();
}
}
ITarget.h
Reste à wrapper le Parser
Le Wrapping se fait de manière relativement simple, on
définit une classe
NParser qui va encapsuler une instance non managée (précédée du mot clé __nogc)
de la classe Parser.
namespace Reader
{
public __gc class NParser
{
private:
Parser __nogc* m_parser;
public:
NParser(ISource* _s, ITarget* _t);
virtual ~NParser();
void parse();
};
}
NParser.h
Jusqu'ici, pas de
difficulté. Mais si l'on regarde le constructeur de la
classe NParser, il prend en paramètre une
ISource et une ITarget, ce qui semble
relativement symétrique à notre API C++ native. Mais une
ISource et une ITarget
se situent du coté managé du code, comment faire pour récupérer une instance
native ?
On peut imaginer plusieurs scénarios:
1) On a déjà des
implémentations de Source et Target en C++.
On va donc wrapper chaque
classe de manière à pouvoir l'instancier en C#. On récupérera l'instance native
par une méthode
géniale de notre classe "wrappeuse" : void* getNativePtr(),
efficace mais peu esthétique.
2) On peut également vouloir
implémenter Source et
Target directement en C# (ou managed C++) pour
bénéficier des classes du
Framework.
Premier scénario : On wrappe nos implementations C++ en MC++
On dispose déjà d'une
implémentation de Source en C++ natif: la classe
ConcreteSource et une
implementation de Target : la classe
ConcreteTarget.
Voici comment l'on peut procéder pour wrapper ConcreteSource par exemple.
Wrapping de ConcreteSource
Le Wrapping se fait de manière classique en
encapsulant une instance non managée dans une classe managée. L'instance non
managée est précédée de __nogc dans sa déclaration pour préciser au Garbage
Collector "qu'il ne doit pas s'en occuper". Afin de pouvoir récupérer le
pointeur sur l'objet C++ encapsulé dans la classe
NConcreteSource, on
définit
une interface IAccessNative proposant une
méthode getNativePointer().
NConcreteSource implementara
IAccessNative et retournera donc le pointeur C++
sur l'instance de ConcreteSource.
namespace Reader
{
private __gc class NConcreteSource : public ISource,public IAccessNative
{
private:
ConcreteSource __nogc* m_s;
public:
NConcreteSource(void);
virtual ~NConcreteSource(void);
public:
void* getNativePointer();
void read();
};
}
NConcreteSource.h
Impact dans le constructeur de NParser
Le problème que l'on avait
résidait dans la "conversion" nos instances de
ISource* et
ITarget* en
Source* et Target*. Voici le code du constructeur de NParser
utilisant getNativePointer() .
NParser::NParser(ISource* _s, ITarget* _t)
{
IAccessNative* nativeSource = dynamic_cast< IAccessNative* > (_s);
IAccessNative* nativeTarget = dynamic_cast< IAccessNative* > (_t);
this->m_parser = new Parser((Source*)nativeSource->getNativePointer(),
(Target*)nativeTarget->getNativePointer());
}
Vue d'ensemble du wrapping
Voici le diagramme de classes du wrapping de notre composant. Les deux
factories SourceFactory et
TargetFactory permettent de rendre du côté C# des objets de type
ISource et cacher
ainsi aux utilisateurs C# la
méthode getNativePointer() qui n'a aucune application en C#.
Second scénario : On permet une implementation C#
Cette solution est certainement plus "propre" que la précédente mais elle pose
un problème : comment faire pour utiliser une instance de C# dans du code non
managé ? En effet en wrappant la classe
Parser dans
NParser, ceci permet
d'appeler un parser C++ depuis un
environnement managé. On a donc une
interopérabilité de C# vers C++. Si je veux maintenant
implémenter Source et/ou
Target en C# et les fournir à mon Parser C++, il nous faut donc une
interopérabilité C++ vers C#. Nous voulons donc
implémenter une classe
CsSource
en C#, elle implémentera l'interface
ISource et doit être utilisée par la
classe C++ Parser via
NParser. Pour ce faire, il suffit de wrapper une instance
de ISource dans du code non managé. Le framework met à disposition un template
"wrapper" nommée gcroot qui permet de "communiquer" entre le monde managé et
non managé. Voici le code de la classe WSource wrappant une instance de
ISource.
class WSource : public Source
{
gcroot< ISource* > m_handle;
public
WSource(ISource* _t);
virtual ~WSource(void);
void read();
};
WSource.h
WSource::WSource(ISource* _s)
:m_handle(_s)
{
}
WSource::~WSource(void)
{
}
void WSource::read()
{
m_handle->read();
}
WSource.cpp
WSource est derivée de Source afin de pouvoir être passée au constructeur de
Parser. Ainsi l'implementation du constructeur de NParser devient:
NParser::NParser(ISource* _s, ITarget* _t)
{
WSource* s = new WSource(_s);
WTarget* t = new WTarget(_t);
this->m_parser = new Parser(s,t);
}
NParser.cpp
Par cet artifice de double wrapping on peut avoir la double interopérabilité de
C# vers C++ et de C++ vers C#. Voila le diagramme UML de wrapping
pour conclure
et résumer de manière un peu plus formelle notre exemple.
Conclusion
On a vu deux exemples de "portage" de code C++ sur .Net via MC++. Si la
première solution est relativement simple malgré l'artifice de l'interface
IAccessNative, elle suppose que les implémentations ne varient pas;
c'est-à-dire que .Net ne sera que client de ce composant C++ et qu'aucune
implémentation C# des interfaces
Source ou Target ne sera
disponible. Par contre,
la deuxième solution permet d'implémenter
Source et Target en C#. Choisir l'une
ou l'autre est un pari sur l'avenir de notre composant. Si le portage est un
"patch" en attendant un nouvelle version
complètement ".Netisé" la première solution
est peut-être la moins coûteuse. La seconde demande de re-implémenter nos
Source et Target en C# mais laisse la porte ouverte à de futur développements
.Net sur la base de notre composant C++.
|