Introduction à la programmation d'applications modulaires en C++

Introduction

Aujourd'hui, pratiquement toute application peut être étendue avec de nombreux types différents de greffons ou de plugins. Grâce à ceux-ci, nous pouvons écrire de nouvelles fonctions pour nos applications préférées sans les recompiler chaque fois que nous voulons les étendre ou les modifier. Je vais vous dire comment écrire une application modulaire en C++.

9 commentaires Donner une note à l'article (5)

Article lu   fois.

Les deux auteur et traducteur

Site personnel

Traducteur : Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Bibliothèques partagées et extensions

Souvent, les applications modernes offrent aux développeurs des interfaces de programmation (API) agréables ou encore des langages particuliers avec lesquels ils peuvent écrire des extensions pour des applications existantes. C'est un très bon moyen de donner la possibilité aux utilisateurs de personnaliser les programmes. Je vais vous montrer comment écrire une application qui va charger des extensions depuis des bibliothèques partagées.

Vous devez savoir que l'écriture d'applications modulaires est différente entre les systèmes Unix et Windows. Unix propose la bibliothèque dlfcn qui contient un ensemble de fonctions qui peuvent être utilisées pour charger des fonctions C depuis une bibliothèque compilée. Sous Windows, nous le faisons d'une autre manière. Dans cet article, nous allons apprendre à le faire sur Linux (ces solutions fonctionneront certainement sur BSD et probablement sur Mac OS).

II. Les outils

Pour faire tous les exemples de ce tutoriel, vous aurez besoin d'un système d'exploitation Linux (je recommande Debian) avec le compilateur g++ et votre éditeur de code préféré (pour moi, ce sera vim).

III. Deux approches aux applications modulaires sur Linux

Pour commencer, je vais vous montrer l'exemple le plus simple d'application qui utilise des modules. En raison du fait que les fonctions de l'en-tête Linux dlfcn.h ne permettent de charger que les fonctions en syntaxe C, nous pouvons réaliser les modules de deux façons :

  • écrire tout le code comme une fonction C ;
  • écrire une fonction de chargement, avec la syntaxe C, qui crée un objet d'une classe qui représente le module.

Dans cet article, je vais vous montrer les deux solutions.

III-A. Solution fonction module avec la syntaxe C

En préambule, j'aimerais vous dire quelque chose à propos de trois fonctions que nous allons utiliser dans notre application. Elles sont appelées : dlopen, dlsym et dlclose. Elles viennent toutes du fichier d'en-tête dlfcn.h et sont implémentées dans les bibliothèques du système Linux. La première d'entre elles est utilisée pour charger une bibliothèque partagée. Elle nécessite deux arguments : le nom du fichier de la bibliothèque partagée (const char*) et un drapeau (int). Il existe plusieurs drapeaux que nous pouvons utiliser, mais le plus commun est RTLD_LAZY. Il provoque le chargement des symboles lorsque ceux-ci sont utilisés pour la première fois. Nous allons utiliser ce drapeau dans nos premiers exemples. Vous pouvez en apprendre davantage sur les drapeaux dans les pages de man Linux ; (man dlopen). dlopen renvoie un pointeur void (void*) appelé gestionnaire.

La seconde (dlsym) est utilisée pour obtenir l'adresse de la fonction dont le symbole a été chargé et passé au gestionnaire par la fonction dlopen. dlsym retourne un pointeur (void*) sur la fonction trouvée dans la ressource partagée passée comme premier argument (gestionnaire reçu de dlopen). Le nom de la fonction à trouver est passé en second argument (char*). Si le système n'arrive pas à trouver cette fonction, il retourne un pointeur NULL (adresse zéro). En C++, nous pouvons transtyper le pointeur reçu vers le type de pointeur réel de la fonction.

La dernière fonction, dlclose, sera utilisée pour fermer la bibliothèque partagée ouverte par dlopen .

La connaissance de ces fonctions nous permet d'écrire notre première application modulaire.

III-A-1. Exemple de base

Tout d'abord, nous devons écrire une application qui sera capable de charger les modules et de lancer leurs fonctions.

Les noms des modules seront passés comme arguments depuis la ligne de commande :

./application module1 module2

Pour ce faire, nous allons utiliser les arguments argc et argv de la fonction main.

Le code de notre application ressemblera à ceci :

 
Sélectionnez
#include <dlfcn.h>
#include <iostream>
 
int main(int argc, char ** argv)
{
  if (argc == 1)
  {
    std::cerr << "Usage: " << argv[0] << " modules...\n";
    return 1;
  }
   
  for (int i = 1; i < argc; ++i)
  {
    void* shared_library = dlopen(argv[i], RTLD_LAZY);
    void (*module)() = reinterpret_cast<void (*)()>(dlsym(shared_library, "module"));
    if (module)
    {
      module();
      dlclose(shared_library);
    } else
    {
      std::cerr << "Error while loading: " << argv[i] << "\n";
    }
  }
  return 0;
}

III-A-2. Explication du code

Dans la boucle qui commence à la ligne 12, nous parcourons tous les arguments de la ligne de commande. Chacun d'entre eux (sauf pour le premier - indexé 0, qui est le nom du fichier binaire de l'application) est un nom de module qui doit être chargé.

Dans le corps de la boucle for, nous utilisons la fonction dlopen pour charger la bibliothèque partagée et la fonction dlsym pour charger la fonction appelée module (dans la syntaxe C !). Ensuite, nous appelons la fonction chargée et, finalement, nous fermons la bibliothèque partagée en utilisant dlclose .

Comme vous pouvez le voir, nous convertissons le pointeur void ( void* ) renvoyé par dlsym au pointeur de la fonction sans arguments renvoyant void ( void (*)() ). C'est nécessaire en C++ afin d'être en mesure d'appeler la fonction indiquée par ce pointeur.

Nous n'avons pas utilisé la fonction dlerror qui informe l'utilisateur sur les erreurs en raison du fait que c'est une application très simple et que son objectif était de montrer comment le chargement dynamique fonctionne. Dans la prochaine partie de cet article, nous allons écrire un outil avancé appelé chargeur de module qui utilisera dlerror .

III-A-3. Compilation

Pour compiler l'application ci-dessus, nous lancerons le compilateur g++ (pratiquement n'importe quelle version sera suffisante pour le faire) :

g++ app.cpp -o app -ldl

Nous utilisons l'option -ldl pour indiquer au compilateur qu'il doit lier notre application avec une bibliothèque dynamique.

III-B. Écrivons un module

Il est temps, maintenant, d'écrire un ou deux modules simples. Chaque module sera dans son propre fichier *.cpp. Un tel fichier doit comporter la définition d'au moins une fonction. Le nom de cette fonction doit être module. Comme je le disais avant, cette fonction doit être écrite avec la syntaxe C. Cela signifie que nous devons ajouter extern "C" avant sa déclaration.

Je vais créer deux modules qui vont écrire une ligne de texte sur la sortie standard. Voici le code du premier (module_start.cpp) :

 
Sélectionnez
#include <iostream>
 
extern "C" void module()
{
  std::cout << "Start module function!\n";
}

et celui du deuxième (module_other.cpp) :

 
Sélectionnez
#include <iostream>
 
extern "C" void module()
{
  std::cout << "Other module function!\n";
}

Maintenant, nous pouvons compiler ces deux modules en utilisant ces commandes :

g++ -fPIC -shared module_start.cpp -o module_start.so
g++ -fPIC -shared module_other.cpp -o module_other.so

Vous devez vous rappeler des options -shared et -fPIC. Le premier ne fait qu'indiquer que le code est celui d'une bibliothèque partagée. Le second est une abréviation pour code à position indépendante, qui signifie que les positions dans le code assembleur seront relatives au lieu d'être absolues. Grâce à cela, le code sera capable d'être chargé dynamiquement n'importe où dans la mémoire de l'application.

Si la compilation n'échoue pas, nous pouvons lancer l'application avec un ou les deux modules :

./app ./module_start.so
./app ./module_other.so
./app ./module_start.so ./module_other.so

Comme vous pouvez le voir, nous passons à l'application les chemins relatifs des bibliothèques partagées. Si le chemin n'est ni un chemin parent ni un chemin absolu, dlopen cherchera l'objet partagé dans ces localisations :

  1. Les chemins de LD_LIBRARY_PATH ;
  2. Dans la liste placée dans /etc/ld.so.cache ;
  3. /lib ;
  4. /usr/lib.

Sinon, dlopen utilisera le chemin relatif ou absolu fourni.

III-C. Exemple avec les classes C++

Le prochain exemple d'application modulaire sera très similaire, mais maintenant chaque module sera une classe qui dérive de la classe de base des modules. Ils auront également une fonction appelée chargeur qui allouera de la mémoire pour l'objet module de classe et le construira. Elle renverra un pointeur vers la mémoire allouée.

Afin de ne pas réécrire tout depuis le début, nous allons :

  1. Écrire la classe de base pour les modules ;
  2. Changer un peu le code de l'application ;
  3. Écrire de nouveaux modules avec chargeurs.

III-C-1. Classe de base des modules

La classe de base pour les modules doit être abstraite. En C++, pour rendre une classe abstraite, vous devez mettre au moins une fonction virtuelle pure. Cette dernière ressemble à ceci :

 
Sélectionnez
class SomeClass
{
    virtual void myFunction() = 0; // ceci est une fonction virtuelle pure
};

Elle ne peut pas être définie (car sa définition est 0). Nous devons la définir dans chacune des classes qui en hérite.

Dans notre exemple, nous allons créer la classe qui n'aura qu'une seule fonction appelée run. Son code est présenté sur le listing ci-dessous :

 
Sélectionnez
#ifndef MODULE_BASE_HPP
#define MODULE_BASE_HPP
 
class ModuleBase
{
  public:
    virtual void run() = 0;
};
 
#endif

III-C-2. Charger des modules

En raison du fait que nous ne pouvons pas charger toute la classe en utilisant la fonction dlsym, nous allons créer une fonction spéciale de chargement pour chaque module. Cette fonction créera l'objet à partir du module de classe et retournera un pointeur vers lui. Nous devons ajuster l'application pour l'adapter à notre construction des modules.

 
Sélectionnez
#include <dlfcn.h>
#include <iostream>
 
#include "ModuleBase.hpp"
 
int main(int argc, char ** argv)
{
  if (argc == 1)
  {
    std::cerr << "Usage: " << argv[0] << " modules...\n";
    return 1;
  }
   
  for (int i = 1; i < argc; ++i)
  {
    void* shared_library = dlopen(argv[i], RTLD_LAZY);
    ModuleBase* (*loader)() = reinterpret_cast<ModuleBase* (*)()>(dlsym(shared_library, "loader"));
    ModuleBase* module;
    if (loader)
    {
      module = loader();
      module->run();
    } else
    {
      std::cerr << "Erreur de chargement: " << argv[i] << "\n";
    }
  }
  return 0;
}

Maintenant, nous avons un pointeur vers la fonction de chargement et nous l'appelons. Nous recevons un pointeur vers le module. Ensuite, nous appelons la fonction run de l'objet du module. Le reste du code est le même que dans l'exemple précédent.

III-C-3. Écrire un module

Pour écrire un module, nous devons d'abord créer le fichier C++ avec l'en-tête ModuleBase.hpp inclus. Ensuite, nous devons écrire la classe qui dérive de ModuleBase . Nous devons déclarer et définir la méthode run dans la nouvelle classe. À la fin, nous avons une définition de fonction de chargement qui renvoie un pointeur vers ModuleBase (ModuleBase*).

Je vous présente mes deux modules ci-dessous :

 
Sélectionnez
#include "ModuleBase.hpp"
#include <iostream>
 
class ModuleStart :
  public ModuleBase
{
  public:
    void run()
    {
      std::cout << "Le module de démarrage tourne!\n";
    }
};
 
extern "C" ModuleBase* loader()
{
  ModuleBase* m = new ModuleStart;
  return m;
}
 
Sélectionnez
#include "ModuleBase.hpp"
#include <iostream>
 
class ModuleSoph :
  public ModuleBase
{
  public:
    void run()
    {
      std::cout << "Le module sophistiqué tourne!\n";
    }
};
 
extern "C" ModuleBase* loader()
{
  ModuleBase* m = new ModuleSoph;
  return m;
}

Après la compilation (nous utilisons les mêmes commandes que dans le premier exemple), nous pouvons lancer l'application avec ses modules de la même façon que précédemment. Je vous recommande d'écrire cet exemple vous-même pour vous entraîner à le faire.

III-D. Conception des applications modulaires

Les exemples que je vous ai indiqués ne sont ni très utiles ni flexibles. Ils montrent seulement l'aspect technique de l'écriture d'applications modulaires en C++. Pour écrire une bonne solution modulaire, vous devez la concevoir précisément. Dans la partie suivante de l'article, je vous présenterai des exemples plus complexes d'applications modulaires.

IV. Exercice

  • Essayez d'écrire une application qui peut faire deux opérations mathématiques de base : l'addition et la soustraction.
  • Autorisez les utilisateurs à ajouter leurs propres opérations en utilisant des modules.
  • Écrivez des modules pour les opérations de division, multiplication et modulo.

La solution de ce problème sera publiée dans la prochaine partie de l'article.

V. Remerciements

Cet article est une traduction autorisée de l'article paru sur le site de Kacper Kołodziej.

Merci à milkoseck pour sa relecture orthographique.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2015 Kacper Kołodziej. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.