II. Première partie▲
Dans cette partie, je vais vous montrer des applications plus sophistiquées qui utilisent également des modules. Les exemples de cette partie seront orientés objet et présenteront une approche plus professionnelle de la programmation d'applications modulaires.
Dans cette partie, je vais vous montrer deux exemples. Le premier est la solution de l'exercice que je vous ai donné à la fin du dernier article. On vous demandait d'écrire une calculatrice qui pouvait additionner des nombres, les soustraire et charger des modules pour effectuer d'autres opérations. Le deuxième exemple, que je vais écrire pour vous, est un interpréteur système (également appelé ligne de commande).
II-A. Calculatrice▲
Pour simplifier le problème (et illustrer notre but), nous supposerons que nous ajoutons seulement des fonctions qui œuvrent avec deux nombres réels (addition, soustraction, multiplication, division, modulo). Nous ne pouvons pas ajouter la fonction factorielle (un argument entier et non pas deux arguments réels). Dans le second exemple, vous serez capable d'apprendre à écrire des applications plus flexibles.
Pour commencer : nous allons écrire une classe appelée ModuleManager qui conservera les fonctions des modules. Son en-tête pourrait ressembler à ceci :
#ifndef MODULE_MANAGER_HPP
#define MODULE_MANAGER_HPP
#include
<map>
#include
<string>
#include
<vector>
class
ModuleManager
{
public
:
typedef
double
(*
CalcFunc)(double
, double
);
private
:
std::
map<
std::
string, CalcFunc>
loaded_functions_;
public
:
std::
vector<
std::
string>
getFunctionsList();
CalcFunc getFunction(std::
string);
bool
addFunction(std::
string, CalcFunc);
bool
loadModule(std::
string);
}
;
#endif
Pour stocker les fonctions, nous utiliserons la fonction map de la bibliothèque standard. Celle-ci conserve chaque élément sous forme d'une paire composée d'une clé et de cet élément. Les éléments sont triés par clé. Dans notre cas, la clé est une chaîne (également de la bibliothèque standard). Cette fonction conserve les noms des fonctions. Le second élément de la paire conserve un pointeur vers la fonction. Nous obtenons ce pointeur à l'aide de la fonction dlsym (j'ai parlé de toutes ces fonctions dans l'introduction).
La fonction getFunctionsList renvoie un vecteur de chaînes qui contient les noms de toutes les fonctions chargées. Nous utilisons getFunction pour obtenir un pointeur de la fonction rattachée au nom passé en argument. La fonction addFunction ajoute une fonction. Si la fonction, dont la clé passée en argument, existe, addFunction renvoie FAUX. Sinon, elle renvoie VRAI. La fonction loadModule prend le chemin vers le fichier binaire contenant le module compilé et charge les deux fonctions contenues dans ce fichier : getName ainsi que la fonction dont le nom a été renvoyé par getName.
Voici, l'implémentation de cette classe décrite ci-dessus :
#include
"module_manager.hpp"
#include
<dlfcn.h>
std::
vector<
std::
string>
ModuleManager::
getFunctionsList()
{
std::
vector<
std::
string>
list;
for
(auto
&
pair : loaded_functions_)
{
list.push_back(pair.first);
}
return
list;
}
ModuleManager::
CalcFunc ModuleManager::
getFunction(std::
string key)
{
auto
it =
loaded_functions_.find(key);
if
(it ==
loaded_functions_.end())
{
return
nullptr
;
}
return
it->
second;
}
bool
ModuleManager::
addFunction(std::
string key, ModuleManager::
CalcFunc func)
{
auto
addition =
loaded_functions_.insert(std::
make_pair(key, func));
return
addition.second;
}
bool
ModuleManager::
loadModule(std::
string fname)
{
void
*
module
=
dlopen(fname.data(), RTLD_LAZY);
if
(!
module
)
{
return
false
;
}
const
char
*
(*
name)() =
reinterpret_cast
<
const
char
*
(*
)()>
(dlsym(module
, "getName"
));
if
(!
name)
{
return
false
;
}
double
(*
func)(double
, double
) =
reinterpret_cast
<
double
(*
)(double
, double
)>
(dlsym(module
, name()));
if
(func)
{
return
addFunction(std::
string(name()), func);
}
return
false
;
}
II-A-1. L'application▲
Maintenant que nous savons à quoi ressemble ModuleManager, nous pouvons écrire une application qui l'utilise. J'ai décrit l'en-tête de la bibliothèque dans l'introduction Le reste du code devrait être clair.
Nous pouvons donc écrire l'application. L'application nous permettra de :
- charger un nouveau module ;
- appeler une fonction existante ;
- quitter l'application.
Chaque fonctionnalité sera appelée en utilisant une commande dédiée :
#include
<iostream>
#include
<cmath>
#include
<string>
#include
<vector>
#include
<sstream>
#include
"module_manager.hpp"
double
add(double
, double
);
double
subtract(double
, double
);
int
main(int
argc, char
**
argv)
{
ModuleManager manager;
manager.addFunction("addition"
, add);
manager.addFunction("soustraction"
, subtract);
std::
string selection;
do
{
auto
names =
manager.getFunctionsList();
for
(auto
&
name : names)
{
std::
cout <<
name <<
"
\n
"
;
}
std::
cout <<
"
\\
load - charge un nouveau module
\n
"
;
std::
cout <<
"
\\
quit -- quitte l'application
\n
"
;
std::
cin >>
selection;
if
(selection ==
"
\\
load"
)
{
std::
string path;
std::
cout <<
"module path: "
;
std::
cin >>
path;
if
(manager.loadModule(path))
{
std::
cout <<
"++ module chargé!
\n
"
;
}
else
{
std::
cerr <<
"-- une erreur s'est produite pendant le chargement du module!
\n
"
;
}
}
else
if
(selection !=
"
\\
quit"
)
{
auto
func =
manager.getFunction(selection);
if
(func ==
nullptr
)
{
std::
cerr <<
"-- la fonction n'existe pas!
\n
"
;
}
else
{
double
a, b;
std::
cout <<
"arguments (2 doubles): "
;
std::
cin >>
a >>
b;
std::
cout <<
"args: "
<<
a <<
" "
<<
b <<
"
\n
"
;
std::
cout <<
"++ résultat de "
<<
selection <<
"("
<<
a <<
", "
<<
b <<
") = "
<<
func(a,b) <<
"
\n
"
;
}
}
}
while
(selection !=
"
\\
quit"
);
return
0
;
}
double
add(double
a, double
b)
{
return
a +
b;
}
double
subtract(double
a, double
b)
{
return
a -
b;
}
Je vais vous expliquer, maintenant, comment tout ça fonctionne. À la ligne 14, nous créons un objet ModuleManager et nous ajoutons deux fonctions : l'addition et la soustraction. Nous le faisons sans charger de code externe, car elles sont incluses dans l'application. Ensuite, nous montrons toutes les fonctions chargées ainsi que les commandes additionnelles : \load et \quit qui pourront être utilisées pour charger de nouveaux modules et quitter l'application. L'utilisateur effectue son choix à la ligne 28. Puis, nous effectuons ce qui correspond au choix de l'utilisateur. S'il demande de charger un nouveau module, nous lui demandons le chemin vers le fichier binaire contenant le module. S'il choisit quelque chose de différent de la commande \quit, nous le traitons comme un appel à une fonction chargée. Nous la recherchons dans le contrôleur et nous l'appelons à l'aide des arguments reçus après les avoir demandés à l'utilisateur en ligne 52.
II-A-2. Les modules de multiplication et division▲
Maintenant, place aux modules. Ils sont très simples. La multiplication se trouve dans multiply_module.cpp :
extern
"C"
double
multiply(double
a, double
b)
{
return
a *
b;
}
extern
"C"
const
char
*
getName()
{
return
"multiply"
;
}
et la division dans divide_module.cpp:
extern
"C"
double
divide(double
a, double
b)
{
return
a /
b;
}
extern
"C"
const
char
*
getName()
{
return
"divide"
;
}
Comme vous pouvez le voir, nous avons deux fonctions dans nos modules. La première est la fonction appropriée qui effectue l'opération mathématique. La seconde (getName) renvoie le nom de la première. Il est utilisé pour charger la fonction appropriée utilisée par la calculatrice et pour obtenir les noms des fonctions pour le ModuleManager.
II-A-3. La compilation▲
Vous pouvez compiler la totalité du programme en utilisant ces commandes :
g++ -std=c++11 module_manager.cpp -c -DDEBUG -o module_manager.o
g++ -std=c++11 calc.cpp module_manager.o -DDEBUG -ldl -o calc
g++ -fPIC -shared multiply_module.cpp -DDEBUG -o multiply_module.so
g++ -fPIC -shared divide_module.cpp -DDEBUG -o divide_module.so
C'est une façon simple (mais peut-être pas la plus simple) d'effectuer l'exercice que je vous avais donné dans la partie précédente de l'article. Nous allons, maintenant, programmer quelque chose de plus raffiné !
II-B. Votre propre interpréteur !▲
Je vais vous montrer comment écrire votre propre ligne de commande. Cela sera un peu plus difficile à coder, mais sa flexibilité et ses capacités seront plus grandes.
À l'instar des exemples précédents, nous devons écrire l'application et la classe du module de base. Nous implémenterons l'application dans une classe Shell. La fonction main appellera seulement la fonction membre de l'objet Shell.
#include
<iostream>
#include
<string>
#include
"shell.hpp"
#include
"shell_application.hpp"
int
main(int
argc, char
**
argv)
{
if
(argc !=
2
)
{
std::
cerr <<
"Utilisation: "
<<
argv[0
] <<
" prompt
\n
"
;
return
1
;
}
Shell shell(argv[1
]);
return
shell.loop();
}
#ifndef SHELL_HPP
#define SHELL_HPP
#include
<string>
#include
<vector>
#include
<map>
class
ShellApplication;
class
Shell
{
typedef
const
char
*
(*
GetNamePtr)();
typedef
ShellApplication*
(*
LoadPtr)();
private
:
const
std::
string prompt_;
const
std::
string load_name_;
std::
map<
std::
string, LoadPtr>
available_apps_;
std::
vector<
void
*>
apps_handlers_;
public
:
Shell(std::
string =
std::
string("shell> "
), std::
string =
std::
string(":load"
));
~
Shell();
bool
loadApplication(std::
string);
int
runCommand(std::
string);
int
loop();
private
:
std::
vector<
std::
string>
parseCommand_(std::
string);
}
;
#endif
#include
"shell.hpp"
#include
<dlfcn.h>
#include
<iostream>
#include
<sstream>
#include
<utility>
#include
"shell_application.hpp"
Shell::
Shell(std::
string prompt, std::
string load_name) :
prompt_(prompt),
load_name_(load_name)
{
available_apps_.insert(std::
make_pair(load_name_, nullptr
));
}
Shell::
~
Shell()
{
for
(auto
handler : apps_handlers_)
{
dlclose(handler);
}
apps_handlers_.clear();
}
bool
Shell::
loadApplication(std::
string path)
{
void
*
handler =
dlopen(path.data(), RTLD_LAZY);
if
(handler ==
0
)
{
std::
cerr <<
"erreur bibliothèque dynamique: "
<<
dlerror();
return
false
;
}
apps_handlers_.push_back(handler);
GetNamePtr getName =
reinterpret_cast
<
GetNamePtr>
(dlsym(handler, "getName"
));
LoadPtr load =
reinterpret_cast
<
LoadPtr>
(dlsym(handler, "load"
));
if
(!
(getName &&
load))
{
std::
cerr <<
"erreur bibliothèque dynamique: "
<<
dlerror();
return
false
;
}
auto
insertion =
available_apps_.insert(std::
make_pair(getName(), load));
if
(insertion.second)
{
std::
clog <<
getName() <<
" application chargée!
\n
"
;
}
return
insertion.second;
}
int
Shell::
runCommand(std::
string cmd)
{
std::
vector<
std::
string>
cmd_parts =
parseCommand_(cmd);
if
(cmd_parts.size() ==
0
)
{
std::
cerr <<
"commande invalide: "
<<
cmd <<
"
\n
"
;
return
-
1
;
}
std::
string&
app_name =
cmd_parts[0
];
if
(app_name ==
load_name_)
{
std::
cout <<
"Chargement des applications...
\n
"
;
int
result =
0
;
for
(auto
it =
cmd_parts.begin() +
1
; it !=
cmd_parts.end(); ++
it)
{
std::
cout <<
" + "
<<
*
it <<
" "
;
if
(loadApplication(std::
string("./"
) +
*
it +
std::
string("_application.so"
)))
{
std::
cout <<
"[ OK ]
\n
"
;
}
else
{
std::
cout <<
"
\n
"
;
result =
1
;
}
}
return
result;
}
auto
app_load =
available_apps_.find(app_name);
if
(app_load ==
available_apps_.end())
{
std::
cerr <<
"Application introuvable: `"
<<
app_name <<
"`
\n
"
;
return
-
1
;
}
ShellApplication*
app =
reinterpret_cast
<
ShellApplication*>
(app_load->
second());
int
result =
app->
runApplication(cmd_parts);
delete
app;
return
result;
}
int
Shell::
loop()
{
std::
string cmd;
std::
cout <<
prompt_;
while
(std::
getline(std::
cin, cmd))
{
int
status =
runCommand(cmd);
std::
cout <<
"Sortie du code: "
<<
status <<
"
\n
"
<<
prompt_;
}
return
0
;
}
std::
vector<
std::
string>
Shell::
parseCommand_(std::
string cmd)
{
std::
vector<
std::
string>
tokens;
std::
stringstream token;
const
char
delimiter =
' '
, quote =
'
\"
'
, escape =
'
\\
'
;
bool
escapeNext =
false
;
bool
quoteMode =
false
;
for
(std::string::
iterator it =
cmd.begin(); it !=
cmd.end(); ++
it)
{
char
c =
*
it;
if
(c ==
escape &&
escapeNext ==
false
)
{
escapeNext =
true
;
}
else
if
(c ==
quote &&
escapeNext ==
false
)
{
quoteMode =
!
quoteMode;
}
else
if
(c ==
delimiter &&
escapeNext ==
false
&&
quoteMode ==
false
)
{
if
(token.str().empty() ==
false
)
{
tokens.push_back(token.str());
token.str(std::
string());
}
}
else
{
token <<
c;
escapeNext =
false
;
}
}
if
(token.str().empty() ==
false
)
{
tokens.push_back(token.str());
}
return
tokens;
}
II-B-1. Explication du code▲
La classe Shell possède deux membres std::string. Le premier est prompt_ - il contient le message de prompt -, le deuxième est load_name_ qui spécifie le nom de la fonction load qui est responsable du chargement des applications. Ils sont définis dans le constructeur de Shell. Nous avons également deux conteneurs : un map pour les noms des applications et les pointeurs vers les fonctions de chargement et un vector pour toutes les ressources (pointeurs void retournés par la fonction dlopen). En marge du constructeur et destructeur de Shell, on trouve trois fonctions membres publiques : loadApplication, runCommand, loop. La première charge les bibliothèques partagées à partir de leur chemin, runCommand exécute la commande entrée par l'utilisateur, et loop comporte une boucle while qui demande à l'utilisateur d'écrire une commande puis l'exécute. Shell comporte également une fonction membre privée appelée parseCommand_. Elle contient un algorithme très simple qui sépare la commande en jetons (tokens). J'écrirai un article séparé sur ce sujet pour les débutants.
Shell suppose que nous chargeons les applications à partir des fichiers situés dans le répertoire de travail courant. La commande load myapp recherchera un fichier nommé : myapp_application.so dans le répertoire courant.
La fonction loadApplication suppose que chaque module partagé (application de Shell) possède deux fonctions. La première d'entre elles est getName et retourne un const char* contenant le nom de l'application. La seconde fonction initialise un objet de classe qui hérite de ShellApplication (la classe de base pour toutes les applications) et retourne un pointeur vers celui-ci.
La fonction runCommand utilise parseCommand_ pour découper la commande, appelle la fonction load pour cette application et lance la fonction runApplication de l'objet application. Je ne vais pas éclaircir la totalité du code. Si vous ne le comprenez pas, vous pouvez trouver l'explication de chaque instruction de dlfcn.h dans l'introduction.
II-B-2. Les applications de l'interpréteur▲
Une application est représentée par une classe héritée de la classe de base ShellApplication. Pour rendre cet exemple plus clair, ShellApplication ne possède qu'un nom clair et une fonction virtuelle pure runApplication. Nous déclarerons également les fonctions load et getName dans le fichier d'en-tête de ShellApplication. Le compilateur ne criera pas si vous ne les définissez (implémentez) pas dans votre application, car les applications sont des bibliothèques partagées, mais des déclarations rappelleront au programmeur de les écrire. L'absence des fonctions load et getName dans une application en cours de chargement provoquera l'arrêt de Shell.
#ifndef SHELL_APPLICATION_HPP
#define SHELL_APPLICATION_HPP
#include
<string>
#include
<vector>
class
ShellApplication
{
private
:
std::
string name_;
public
:
ShellApplication(std::
string name) :
name_(name)
{}
virtual
~
ShellApplication() {}
std::
string getName()
{
return
name_;
}
virtual
int
runApplication(std::
vector<
std::
string>&
) =
0
;
}
;
extern
"C"
const
char
*
getName();
extern
"C"
ShellApplication*
load();
#endif
Nous sommes, maintenant, prêts à créer nos applications !
II-B-3. Applications Echo et Concat▲
Pour montrer comment fonctionne notre Shell, nous devons avoir au moins une application. J'ai écrit deux exemples similaires : echo et concat. Le premier écrit tous les arguments séparés par un espace à l'écran. concat assemble tous les arguments et les affiche.
#include
<iostream>
#include
"shell_application.hpp"
class
EchoApplication :
public
ShellApplication
{
public
:
EchoApplication() :
ShellApplication("Echo"
)
{}
int
runApplication(std::
vector<
std::
string>&
argv)
{
for
(auto
it =
argv.begin() +
1
; it !=
argv.end(); ++
it)
{
std::
cout <<
*
it <<
" "
;
}
std::
cout <<
"
\n
"
;
return
0
;
}
}
;
extern
"C"
const
char
*
getName()
{
return
"echo"
;
}
extern
"C"
ShellApplication*
load()
{
return
new
EchoApplication();
}
#include
<iostream>
#include
"shell_application.hpp"
class
ConcatApplication :
public
ShellApplication
{
public
:
ConcatApplication() :
ShellApplication("Concatenate strings"
)
{}
int
runApplication(std::
vector<
std::
string>&
argv)
{
for
(auto
it =
argv.begin() +
1
; it !=
argv.end(); ++
it)
{
std::
cout <<
*
it;
}
std::
cout <<
"
\n
"
;
return
0
;
}
}
;
extern
"C"
const
char
*
getName()
{
return
"concat"
;
}
extern
"C"
ShellApplication*
load()
{
return
new
ConcatApplication();
}
II-B-4. Compilation▲
Pour compiler l'application entière (Shell) et les applications Shell, utilisez ce Makefile :
CXX=
g++
CXXFLAGS=
-std=
c++11
-g -DDEBUG -Wall
APPSFLAGS=
-fPIC -shared
OBJS=
shell_main.o shell
.o
APPS=
concat_application.so echo_application.so
LIBS=
-ldl
TARGET=
shell
all:
$(OBJS)
$(APPS)
$(CXX)
$(CXXFLAGS)
$(OBJS)
$(LIBS)
-o $(TARGET)
$(APPS)
:
%so:
%cpp
$(CXX)
$(CXXFLAGS)
$(APPSFLAGS)
$
< -o $
@
$(OBJS)
:
%.o:
%.cpp
$(CXX)
$(CXXFLAGS)
-c $
< -o $
@
cleanup:
rm -f $(OBJS)
$(TARGET)
$(APPS)
Pour ajouter de nouvelles applications au fichier make, ajoutez leur nom à la 5e ligne. Ensuite, vous pourrez charger les applications existantes en utilisant : load command. Pour le tester, écrivez ces lignes dans votre fenêtre de commandes :
make && ./shell « prompt> »
prompt> :load echo
prompt> :load concat
prompt> echo text one two three
prompt> concat text ont two three « four five »
Vous trouverez tous les fichiers de l'exemple Shell sur mon Gist.
II-C. Exercices▲
Écrivez quelques applications pour Shell. Elles peuvent être de simples opérations mathématiques ou des fonctions de création de fichiers.
- Ajoutez une commande exit à Shell. Vous devez le faire de la même manière que celle utilisée avec : load command.
- Essayez d'ajouter le support des touches de navigation (flèches). Les flèches Haut et Bas permettent de naviguer dans l'historique des commandes. Les flèches Gauche et Droite permettent de naviguer dans la commande écrite.
- Astuce : vous aurez besoin de la bibliothèque termios.h.
II-D. Remerciements▲
Cet article est une traduction autorisée de l'article paru sur le site de Kacper Kolodziej.
Merci aussi à Claude Leloup pour sa relecture orthographique.