I. 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++.
I-A. 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).
I-B. 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).
I-C. 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.
I-C-1. 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.
I-C-1-a. 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 :
#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
;
}
I-C-1-b. 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 .
I-C-1-c. 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.
I-C-2. É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) :
#include
<iostream>
extern
"C"
void
module
()
{
std::
cout <<
"Start module function!
\n
"
;
}
et celui du deuxième (module_other.cpp) :
#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 :
- Les chemins de LD_LIBRARY_PATH ;
- Dans la liste placée dans /etc/ld.so.cache ;
- /lib ;
- /usr/lib.
Sinon, dlopen utilisera le chemin relatif ou absolu fourni.
I-C-3. 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 :
- Écrire la classe de base pour les modules ;
- Changer un peu le code de l'application ;
- Écrire de nouveaux modules avec chargeurs.
I-C-3-a. 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 :
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éritent.
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 :
#ifndef MODULE_BASE_HPP
#define MODULE_BASE_HPP
class
ModuleBase
{
public
:
virtual
void
run() =
0
;
}
;
#endif
I-C-3-b. 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.
#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.
I-C-3-c. É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 :
#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;
}
#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.
I-C-4. 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.
I-D. 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.
I-E. Remerciements▲
Cet article est une traduction autorisée de l'article paru sur le site de Kacper Kolodziej.
Merci à milkoseck pour sa relecture orthographique.