Portail / Sujets autour de la programmation / Programmation C/C++ sous Linux / Serveur d'application avec Qt 6 / Concepts généraux(Sommaire)

Instanciation de classes par voie externe.

Mise à jour du 07/07/2024.

C++ présente une régression par rapport à Java dans la mesure où ce langage ne dispose pas d'un mécanisme de réflexion. Grâce à ce mécanisme, il est assez simple en Java d’instancier une classe à partir de son nom sous forme d’une chaîne de caractères. Nous n’avons pas d’équivalent en C++. Commençons par décrire ce qui se passe en Java.

Le mécanisme de réflexion en Java.

En Java, on commence par invoquer la méthode statique Class.forName("nom_de_la_classe") pour requérir le descripteur de la classe à instancier.

Cette méthode recherche dans les librairies (.jar) et les fichiers (.class) de ses chemins de recherche la classe dont le nom correspond à celui transmis à cette méthode. Cela revient à dire que la classe doit exister physiquement dans un fichier avant de pouvoir être instanciée en mémoire.

Ensuite, on invoque la méthode newInstance() du descripteur (classe Class) trouvé pour créer un nouvel objet de cette classe.

Transposition du mécanisme au code C++.

Tentons alors de transposer ce mécanisme au C++. En C++, la définition d’une classe est accessible via un fichier d’en-têtes portant généralement l’extension (.h) ou (.hpp). Malheureusement cela concerne uniquement le code source. Après génération (compilation et édition de liens), les noms de classes se trouvent sous forme décorée dans les librairies et les exécutables.

Sous Linux, la commande nm -D -C permet d’obtenir la liste des symboles dont celui des noms de classe sous une forme non décorées. Malheureusement cette commande n’est pas portable et en outre rien ne permet de la limiter au seuls noms des classes ce qui rend son exploitation délicate. Cette approche n’est donc pas non plus la bonne et il n’existe pas de mécanisme "portable" permettant de lister les classes intégrées par un fichier binaire.

Pour disposer d’un mécanisme portable, on peut imaginer regrouper les classes à instancier par voie externe (que nous appelerons désormais "classes instaciables") par leur nom au sein d’une même librairie. Cette librairie définit elle-même une table des noms des classes instanciables. A chaque classe est associée une clef unique d'identfication (généralement le nom de la classe mais ce n'est pas obligatoire). La librairie obtenue doit ensuite être déployée dans un des chemins de recherche des binaires de l’exécutable. Par exemple sous Linux, on peut étendre ce chemin au moyen de la variable d’environnement LD_LIBRARY_PATH.

Si on doit modifier une classe instanciable ou le contenu de la table des classes instanciables, on se limite à régénérer cette librairie (de même qu'en Java, en pareil cas, il faut reconstruire le (.jar)).

Cette librairie peut être liée à l'exécutable qui l'utilise mais elle peut aussi être chargée dynamiquement sans liaison préalable. Pour rendre notre serveur d'application générique, c'est cette dernière possibilité qui présente le plus d'intérêt. En effet, il est alors possible de placer cette librairie n'importe où, d'en déclarer le chemin dans un fichier de configuration puis de la charger à la demande. Cela permet aussi de pouvoir déclarer plusieurs librairies de ce type.

Facilitatiion via l'infrastructure JMP.

La librairie jmp propose des classes de base pour réaliser des librairies de classes instanciables par voie externe. La classe principale est la classe ClassNames qui représente l'ensemble des fabriques de classes instantiables à la manière d'une table. Cette classe dispose de la méthode containsClassName("nom_de_la_classe")qui permet de savoir si une classe que l’on souhaite instancier par son nom existe ou non dans la table. Elle dispose également de la méthode newInstance("nom_de_la_classe") qui permet de créer un objet issu de la classe instanciable dont le nom est transmis en paramètre. Enfin, la méthode removeClassFactory("nom_de_la_classe") permet d'ôter une fabrique de classe instanciable de la table.

L'instanciation à proprement parler se fait au moyen d'une fabrique de classe spécifique contenue par la table pour chaque classe instanciable. Un pointeur intelligent sur chaque fabrique de classe est associé au nom de la classe instanciable dans la table. Les fabriques de classe doivent dériver de l'interface IClass. Cette interface dispose d'une méthode newInstance() qui réalise l'instanciation.

L'instanciation ne retourne pas directement un pointeur intelligent sur l'objet concret mais un pointeur sur son interface ancêtre. Il est cependant facile de transtyper ce pointeur intelligent vers celui sur la classe concrète.

Utilisation pratique.

La librairie jmp définit une variable globale CLASS_FACTORIES qui représente la table globale des classes instanciables pour une application donnée. Chaque librairie disposant de classes instanciables doit ajouter à cette table la liste des instances de fabriques de classe avec comme clef le nom de la classe instanciable (qui est différent de celui de la fabrique de classe).

L'ajout devrait se faire lors de l'initialisation de la librairie lors de son chargement. De même, lors du déchargement de la librairie, ces instances de fabriques de classe instanciable devraient être supprimé. En voici un exemple pour la librairie webapp :

/**
 * Fonction d'initialisation de la librairie lors de son chargement.
 * Cette fonction ajoute les fabriques des classes intanciables propres à la librairie.
 */
void __attribute__ ((constructor)) initLibrary(void) {
    // Initialisation du moteur de chiffrement
    QString factory = "WebSite";
    if (!CLASS_FACTORIES->containsClassName(factory))
        CLASS_FACTORIES->addClass(factory, make_shared<WebSiteFactory>());
    factory = "SecurityAgent";
    if (!CLASS_FACTORIES->containsClassName(factory))
        CLASS_FACTORIES->addClass(factory, make_shared<SecurityAgentFactory>());
}

/**
 * Libération des ressources de la librairie lors de son déchargement.
 * Cette fonction supprime les fabriques de classe de la librairie webapp.
 */
void __attribute__ ((destructor)) cleanUpLibrary(void) {
    shared_ptr<IClass> spFactory = CLASS_FACTORIES->removeClassFactory("WebSite");
    if (spFactory) {
        spFactory.reset();
        spFactory = nullptr;
    }
    spFactory = CLASS_FACTORIES->removeClassFactory("SecurityAgent");
    if (spFactory) {
        spFactory.reset();
        spFactory = nullptr;
    }
}

Réalisation d'un prototype.

Pour illuster tout ceci, nous allons créer une librairie appelée fornamequi permettra d'instancier par voie externe les classes Rouge, Bleu et Vert. Vous trouverez ci-dessous l'unique fichier source permettant la génération de cette librairie dynamique :

        #include "jmp.hpp"
        #include "forname_global.h"
        
        class ICouleur : public IInstance {
            public:
                ICouleur(const QString& name) : IInstance(name) {}
                virtual ~ICouleur() {}
                virtual QString toString() = 0;
        };
        
        class FORNAME_EXPORT Rouge : public ICouleur {
            public:
                Rouge() : ICouleur("Rouge") {}
                virtual ~Rouge() {}
                virtual QString toString() { return "rouge"; }
        };
        
        class RougeFactory : public IClass {
            public:
                RougeFactory() {}
                virtual ~RougeFactory() {}
                virtual shared_ptr<IInstance> newInstance() {
                    return make_shared<Rouge>();
                }
        };
        
        class FORNAME_EXPORT Bleu : public ICouleur {
            public:
                Bleu() : ICouleur("Bleu") {}
                virtual ~Bleu() {}
                virtual QString toString() { return "bleu"; }
        };
        
        class BleuFactory : public IClass {
            public:
                BleuFactory() {}
                virtual ~BleuFactory() {}
                virtual shared_ptr<IInstance> newInstance() {
                    return make_shared<Bleu>();
                }
        };
        
        class FORNAME_EXPORT Vert : public ICouleur {
            public:
                Vert() : ICouleur("Vert") {}
                virtual ~Vert() {}
                virtual QString toString() { return "vert"; }
        };
        
        class VertFactory : public IClass {
            public:
                VertFactory() {}
                virtual ~VertFactory() {}
                virtual shared_ptr<IInstance> newInstance() {
                    return make_shared<Vert>();
                }
        };
        
        class FORNAME_EXPORT ClassesTable : public ClassNames {
            public:
                ClassesTable() : ClassNames() {
                    addClass("Rouge", make_shared<RougeFactory>());
                    addClass("Bleu", make_shared<BleuFactory>());
                    addClass("Vert", make_shared<VertFactory>());
                }
                ~ClassesTable() {}
        };
        

Nous créons ensuite un programme auquel cette librairie est liée par construction et qui illustre l"instanciation par voie externe d'un objet de chacune de ces 3 classes :

#include <QCoreApplication>
#include <QIODevice>
#include "forname.h"

/**
 * @brief main Ce prototype permet d'instancier des classes se trouvant dans la librairie du projet instances.
 * @param argc Nombre de paeramètre de la ligne de commande.
 * @param argv Argumenst de la ligne de commande.
 * @return Statut de l'exécution.
 */
int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);
    QTextStream cout(stdout, QIODevice::WriteOnly);
    ClassesTable table;
    QStringList list = table.classes();
    cout << "Affichage de la liste des classes instanciables" << Qt::endl;
    cout << "-----------------------------------------------" << Qt::endl;
    for (auto p = list.begin(); p != list.end(); ++p)
        cout << *p << Qt::endl;
    cout << Qt::endl << "Instanciation des classes" << Qt::endl;
    cout << "-------------------------" << Qt::endl;
    // Instanciation de la classe Rouge
    shared_ptr<Rouge> spRouge = dynamic_pointer_cast<Rouge>(table.newInstance("Rouge"));
    cout << spRouge->toString() << ", nom de la classe = " << spRouge->name() << Qt::endl;
    // Instanciation de la classe Bleu
    shared_ptr<Bleu> spBleu = dynamic_pointer_cast<Bleu>(table.newInstance("Bleu"));
    cout << spBleu->toString() << ", nom de la classe = " << spBleu->name() << Qt::endl;
    // Instanciation de la classe Vert
    shared_ptr<Vert> spVert = dynamic_pointer_cast<Vert>(table.newInstance("Vert"));
    cout << spVert->toString() << ", nom de la classe = " << spVert->name() << Qt::endl;
    return 0;
}
    

Si on exécute ce programme on obtient l'affichge qui suit :

Affichage de la liste des classes instanciables
-----------------------------------------------
Bleu
Rouge
Vert

Instanciation des classes
-------------------------
rouge, nom de la classe = Rouge
bleu, nom de la classe = Bleu
vert, nom de la classe = Vert

Rédaction par Jean-Marie Piatte (1983-2021)