Managed C++ par l'exemple Sébastien Andreo
Le Managed C++ (C++/CLI dans la version à venir du .Net Framework) permet d'utiliser des composants ou librairies écris en C++ "natif" sur la plateforme .Net. Cette approche est relativement alléchante lorsqu'on ne veut pas tout réimplémenter. Nous allons ici présenter la démarche permettant de porter un composant C++ natif sur la plateforme .Net par le biais du Managed C++.

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++.


 
Postez vos commentaires ici : http://www.dotnetguru.org/modules.php?op=modload&name=News&file=article&sid=597&mode=thread&order=0&thold=0