Intégrer C++ et C++.NET

Cet article se base sur des idées et des exemples développés dans d'autres articles sur ce site. Je vous invite entre autres à consulter l'article sur les objets autonomes de même que l'article sur les mutex et les autoverrous pour vous familiariser avec la stratégie servant d'exemple pour ce programme. Notez que ce qui suit est désuet; je l'ai laissé ici à titre de référence historique.

Un étudiant d'informatique industrielle, session 5 à l'automne 2005, Pascal Maheux, m'a indiqué que vous rencontrerez des ennuis à réaliser l'activité ci-dessous à partir d'un projet .NET vide si vous n'ajoutez pas la directive #include <tchar.h> au fichier d'en-tête de la fenêtre .NET.

Si vous travaillez à partir d'un modèle de projet Application Windows Forms (.NET), cette inclusion sera faite par défaut et vous n'aurez pas à vous en préoccuper.

Nombreuses et nombreux sont celles et ceux qui souhaitent utiliser les classes de l'infrastructure .NET lorsque celles-ci paraissent utiles et utiliser un langage plus complet comme C++ lorsque le besoin s'en fait sentir. L'une des raisons (très plausibles par ailleurs) pour vouloir agir ainsi serait l'absence d'une bibliothèque standard en C++ pour les applications à multifenêtrage graphique: on pourrait vouloir utiliser l'infrastructure de .NET pour avoir accès aux classes et contrôles graphiques et utiliser C++ pour donner du punch au programme, profiter des facilités de STL, utiliser des objets constants et ainsi de suite.

La question que ces gens risquent de se poser, peu importe leur origine et leur outil de prédilection, est comment y arriver?

Pour illustrer une stratégie possible, je vous propose l'interface ci-dessus. Nous présumerons que vous utilisez C++ .NET et que vous êtes en mesure de construire une telle interface vous-même (avec Windows.Forms, probablement).

Sur cette interface, on déposera une étiquette (nommée lblTexte) et un bouton de commande (nommé btnGo).

Lorsque l'usager sélectionnera btnGo, le texte sur le bouton deviendra Arrêter et un objet Autonome (instance d'une classe nommée Gosseux, mais je conviens que ça pourrait être moins familier) sera lancé. Cet objet gardera en tant qu'attribut un pointeur sur lblTexte et modifiera chaque seconde le texte sur cette étiquette. Une sélection subséquente de btnGo détruira l'objet Autonome en question et fera en sorte de remettre le texte Lancer sur btnGo.

Puisque le CLR de .NET assure la collecte automatique des objets pris en charge mais pas celle des objets indigènes, et puisqu'il est possible qu'un usager ferme le programme alors qu'un Autonome s'exécute encore, nous ajouterons au modèle un singleton d'une classe nommée NettoyeurAutonomes dont le rôle sera de s'assurer de la destruction de tous les objets autonomes encore en mémoire à la fin du programme.

La classe Autonome (rappel)

La classe Autonome est un schéma de conception servant en tant qu'abstraction OO du concept de thread. La classe Autonome est abstraite, et la méthode que doivent surcharger ses dérivés pour réaliser une tâche dans un thread qui leur est propre se nomme agir().

Une implémentation correcte (pour nos fins) de Autonome serait :

Autonome.h Autonome.cpp
#ifndef AUTONOME_H
#define AUTONOME_H
class Autonome
{
   HANDLE h_;
   bool meurs_;
   long dodo_;
protected:
   Autonome(long dodo = {});
      : meurs_{}, dodo_{dodo}
   {
   }
public:
   virtual void agir() = 0;
   void dodo();
   bool dois_mourir() const { return meurs_; }
   void meurs() { meurs_ = true; }
   void demarrer();
   void arreter();
   virtual ~Autonome();
};
#endif
#include "Autonome.h"
#include "NettoyeurAutonomes.h"

unsigned long __stdcall generique (void *p)
{
   Autonome &pA = *static_cast<Autonome *>(p);
   while (!pA.dois_mourir())
   {
      pA.agir();
      pA.dodo();
   }
   return 0L;
}

void Autonome::demarrer()
{
   DWORD thId;
   h_ = CreateThread(0, 0, generique, this, 0, &thId);
   NettoyeurAutonomes::get().abonner(this);
}

void Autonome::arreter()
{
   meurs();
   WaitForSingleObject(h_, INFINITE);
   CloseHandle(h_);
   NettoyeurAutonomes::get().desabonner(this);
}

void Autonome::dodo()
{
  Sleep(dodo_);
}

Autonome::~Autonome()
{
   arreter();
}

Rappelons que la classe Autonome n'est pas (du tout!) nécessaire pour réaliser une interaction entre code indigène et code pris en charge. Ce n'est, simplement, qu'une partie de notre exemple.

Pour en savoir plus sur le concept de classe Autonome, voir cet article.

Singleton NettoyeurAutonomes

Si un objet pris en charge par le CLR de .NET est détruit (par exemple à la fin de l'exécution d'un programme), il se peut qu'on veuille assurer la bonne destruction de toutes ses ressources, en particulier celles qui ne sont pas prises en charge, et plus spécifiquement les objets autonomes qui n'ont pas encore été détruits, pour éviter que des threads sauvages ne poursuivent leur exécution une fois leur durée de vie utile dépassée.

Pour ce faire, un bon truc est de faire en sorte que tout autonome s'abonne dès son démarrage à un service de nettoyage et se désabonne dès son interruption du même service. Ainsi, un singleton peut être chargé du nettoyage des autonomes en fin de programme et assurer la bonne fin de tous les autonomes n'ayant pas été arrêtés au préalable par d'autres mécanismes.

NettoyeurAutonomes.h NettoyeurAutonomes.cpp
#ifndef NETTOYEUR_AUTONOMES_H
#define NETTOYEUR_AUTONOMES_H

#include "Autonome.h"
#include "Mutex.h"
#include <vector>

class NettoyeurAutonomes
{
   std::vector<Autonome *> abonnes_;
   static NettoyeurAutonomes singleton;
   NettoyeurAutonomes(const NettoyeurAutonomes&) = delete;
   NettoyeurAutonomes& operator=(const NettoyeurAutonomes&) = delete;
   Mutex m_;
public:
   static NettoyeurAutonomes &get()
   {
      return singleton;
   }
   void abonner(Autonome *);
   void desabonner(Autonome *);
   ~NettoyeurAutonomes();
};

#endif
#include "Autonome.h"
#include "Mutex.h"
#include "NettoyeurAutonomes.h"
#include <vector>
#include <algorithm>
using namespace std;
NettoyeurAutonomes NettoyeurAutonomes::singleton;
void NettoyeurAutonomes::abonner(Autonome *p)
{
   Autoverrou av{m_};
   if (find(begin(abonnes_), end(abonnes_), p) == end(abonnes_))
      abonnes_.push_back(p);
}

void NettoyeurAutonomes::desabonner(Autonome *p)
{
   Autoverrou av{m_};
   auto itt = find(begin(abonnes_), end(abonnes_), p);
   if (itt != end(abonnes_))
      abonnes_.erase (itt);
}

NettoyeurAutonomes::~NettoyeurAutonomes()
{
   for(auto p : abonnes_) delete p;
}

Encore une fois, à strictement parler, NettoyeurAutonomes n'est pas nécessaire pour faire interagir code indigène et code pris en charge, mais peut être pratique en situation susceptible de laisser des instance de Autonome survivre à la fin d'un programme, comme c'est le cas s'il y a interaction avec du code pris en charge.

Pour en savoir plus sur les mutex et les autoverrous, voir cet article.

La classe Gosseux

La classe Gosseux décrit la catégorie d'objet autonome qui interagira avec notre programme .NET. Son rôle sera de mettre à jour chaque seconde une étiquette en y indiquant le nombre de secondes depuis le démarrage de l'objet autonome. Ne craignez rien : il ne s'agit que d'un exemple de démonstration, et le code de agir() dans cet objet pourrait être aussi raffiné que souhaité.

Gosseux.h Gosseux.cpp
#ifndef GOSSEUX_H
#define GOSSEUX_H

#include "Autonome.h"
#include <vcclr.h>

class Gosseux : public Autonome
{
   gcroot<System::Windows::Forms::Label *>label_;
   Gosseux (System::Windows::Forms::Label*);
   int nitt_;
public:
   static Gosseux* creer(System::Windows::Forms::Label *);
   void agir()
   {
      label_->Text = System::Convert::ToString(++nitt_);
   }
};

#endif
#include "Gosseux.h"

Gosseux::Gosseux(System::Windows::Forms::Label* label)
   : Autonome{1000}, label_{label}, nitt_{}
{
}

Gosseux *Gosseux::creer(System::Windows::Forms::Label *label)
{
   auto p = new Gosseux(label);
   p->demarrer();
   return p;
}

Le secret de la sauce est dans le template nommé gcroot, inclus de vcclr.h et, plus précisément, de gcroot.h (deux fichiers qui ne sont évidemment pas des fichiers du standard C++). Cette classe a pour rôle d'enrober un objet pris en charge de manière à ce qu'il soit utilisable dans un contexte indigène.

Vous remarquerez que l'emploi de gcroot sur un objet pris en charge rend celui-ci tout à fait utilisable en situation normale, et ce sans que cela n'implique un effort de programmation supplémentaire.

Les ajouts à faire à la classe .NET représentant l'interface personne/ machine

private:
   Gosseux *gosseux_;

Comment faire en sorte, maintenant, que la sélection d'un bouton dans un objet pris en charge crée (ou détruise) un objet Autonome indigène? Dans le cas de notre exemple, il importe que le pointeur sur l'autonome créé soit connu de la fenêtre puisqu'elle doit pouvoir arrêter cet autonome une fois ce dernier lancé.

public:
   frmDemo()
      : gosseux_{}
   {
      InitializeComponent();
   }

Ainsi, il faut inclure Gosseux.h dans le fichier d'en-tête de la fenêtre, puis ajouter l'attribut d'instance gosseux_ à la fenêtre:

Remarquez que Gosseux est un type indigène et que la fenêtre (code pris en charge) possède donc un attribut dont le type est pointeur sur un type indigène. C'est là quelque chose de tout à fait légal.

Le constructeur devrait ensuite initialiser gosseux_ à 0 (ce que le débogueur .NET considérera comme un pointeur non initialisé), ce qui est le plus simplement fait dans le constructeur. Par exemple, présumant que la fenêtre se nomme frmDemo, on aura le constructeur par défaut visible à droite.

private:
   System::Void btnGo_Click(System::Object *sender, System::EventArgs *e)
   {
      if (gosseux_)
      {
         delete gosseux_;
         gosseux_ = 0;
         btnGo->Text = S"Lancer";
      }
      else
      {
         btnGo->Text = S"Arrêter";
         gosseux_ = Gosseux :: Creer (lblTexte);
      }
   }

Enfin, il ne reste plus qu'à insérer le code requis pour réagir correctement à la sélection du bouton de commande btnGo (donc à modifier la définition de la méthode btnGo_Click()):

Encore une fois, remarquez que les opérations sur une instance d'une classe indigène sont très semblables, du point de vue de la classe prise en charge, à des opérations sur des instances de classes prises en charge, à ceci près qu'on se permet d'appliquer l'opération delete sur l'autonome indigène au moment opportun.

En résumé

En résumé, le secret de la sauce est :


Valid XHTML 1.0 Transitional

CSS Valide !