En l'état, notre programme ne fait pas grand chose d'intéressant :
il invite à la saisie et affiche la pile des valeurs réelles qu'il a
reconnues (en ignorant les autres mots).
Il s'agit en fait d'une calculatrice en
notation
polonaise inversée ; seulement, elle ne sait faire aucune
opération...
Nous chercherons donc à lui fournir autant de nouvelles opérations que
nous le souhaitons, sans jamais la modifier, grâce à un mécanisme
d'extension par
plugins.
Pour simplifier notre réalisation, les opérations seront saisies en toutes
lettres, comme les mots
Plus et
Quit sur cet exemple :
$ ./prog_calc ↵
stack:
? 2 3
stack: 2 3
? Plus
stack: 5
? 6 7 Plus
stack: 5 13
? 2 Plus Plus
stack: 20
? Quit
$
L'idée est donc de charger une bibliothèque dynamique dont le nom est déduit
du nom d'opération saisi afin d'exécuter une fonction de cette bibliothèque
dont le rôle sera d'influer sur les données de l'application pour réaliser
l'opération attendue.
La différence essentielle entre l'usage que nous faisions
juste avant d'une bibliothèque dynamique et celui que nous
envisageons maintenant, tient dans l'aspect implicite ou explicite du
chargement de cette bibliothèque.
Dans la situation précédente, le chargement de la bibliothèque
calc est
implicite car le programme ne peut simplement pas fonctionner sans sa
présence ; d'ailleurs l'usage de cette bibliothèque est requis dans
le procédé de fabrication.
Au contraire, dans ce que nous nous apprêtons à réaliser, ce n'est que lors
de l'acquisition d'une information (la saisie au clavier dans cet exemple)
que le programme décide de charger explicitement une bibliothèque dont il
fera usage.
Pour réaliser ceci, nous devons intervenir sur le fichier
calc.cpp
qui constitue le code source de la bibliothèque
calc sur laquelle
repose notre programme.
La fonction
apply_operation() est appelée lorsqu'un mot qui ne peut
pas être interprété comme une valeur réelle a été saisi au clavier ;
ce mot est représenté par le paramètre
operation.
Lorsqu'elle sera complétée, cette fonction aura une influence sur le
tableau dynamique de réels
values et indiquera par un résultat
booléen si le programme doit continuer à fonctionner ou s'arrêter.
Une première étape consiste à déterminer le nom de la bibliothèque que nous
allons tenter de charger explicitement ; la fonction
compose_library_name() a été dédiée ici à ce propos.
Nous choisissons arbitrairement dans cet exemple que le nom de la
bibliothèque sera composé du préfixe
Calc suivi du nom d'opération
saisi ; par exemple
CalcPlus pour l'opération
Plus.
Toutefois, comme nous l'avons constaté dans la
partie
précédente, le nom exact de la bibliothèque est dépendant du système
d'exploitation ce qui nécessite l'ajout d'un préfixe et/ou d'un
suffixe ; par exemple
libCalcPlus.so,
libCalcPlus.dylib ou
CalcPlus.dll pour l'opération
Plus
En toute rigueur, certains systèmes acceptent que les bibliothèques
dynamiques chargées explicitement comme plugins ne respectent
pas les conventions de nommage habituelles.
Toutefois, ceci n'apporterait qu'un peu plus de confusion ici donc
nous nous imposons le respects de ces conventions.
.
Enfin, et à nouveau de manière arbitraire, nous décidons que, pour faciliter
l'organisation de nos fichiers, cette bibliothèque dynamique sera placée
dans un répertoire ayant le même nom que nous avons choisi
précédemment ; par exemple
CalcPlus/libCalcPlus.so,
CalcPlus/libCalcPlus.dylib ou
CalcPlus/CalcPlus.dll pour
l'opération
Plus.
Une autre étape consiste à déterminer le nom de la fonction que nous
chercherons à invoquer au sein de cette bibliothèque ; la fonction
compose_function_name() a été dédiée ici à ce propos.
Elle est triviale puisque nous choisissons arbitrairement d'ajouter
le suffixe
_operation au nom d'opération saisi ; par exemple
Plus_operation pour l'opération
Plus.
Les fonctionnalités du système d'exploitation qui permettront d'arriver
à nos fins sont déclarées dans le fichier d'en-tête standard
<dlfcn.h>.
Nous y trouvons principalement :
void *dlopen(const char *filename, int flags);
- Cette fonction tente de charger la bibliothèque désignée par
filename et renvoie un pointeur désignant cette ressource
en cas de succès ou bien un pointeur nul en cas d'échec.
- Le paramètre flags sert à contrôler les dépendances au sein de
cette bibliothèque ; nous nous contenterons de la constante
RTLD_LAZY qui convient à la très grande majorité des usages.
void *dlsym(void *handle, const char *symbol);
- Cette fonction tente de retrouver dans la bibliothèque désignée par
handle (obtenue par dlopen()) la fonction
En toute rigueur, il peut également s'agir de l'adresse d'une
variable globale au sein de cette bibliothèque, mais cet usage est
assez marginal.
ayant le nom précisé par symbol et renvoie un pointeur désignant
cette fonction en cas de succès ou bien un pointeur nul en cas d'échec.
int dlclose(void *handle);
- Cette fonction indique que nous ne souhaitons plus faire usage de la
bibliothèque désignée par handle (obtenue par dlopen()).
- Il n'est alors plus question de continuer à utiliser la moindre fonction
de cette bibliothèque particulière (obtenue par dlsym()).
const char *dlerror(void);
- Cette fonction sert à obtenir un message de diagnostic en cas d'échec
d'une des fonctions précédentes.
Sous Windows, le fichier d'en-tête
<dlfcn.h> n'existe pas mais des
fonctionnalités équivalents sont déclarées dans
<windows.h> ;
nous nous contentons ici d'une émulation triviale des fonctionnalités
précédentes par un jeu de
macros qui fera amplement l'affaire.
Complétez alors la fonction
apply_operation() en faisant usage des
fonctions précédentes.
Il faudra systématiquement contrôler le succès ou l'échec des appels
à
dlopen() et
dlsym() et produire un message d'erreur explicite
le cas échéant.
Pour rappel, les appels
data(lib_name) et
data(fnct_name) permettent
d'obtenir les
const char * constitutifs des
std::string en
question.
Lorsque
dlsym() fournira un pointeur non nul, ce dernier sera censé
désigner une fonction que nous souhaitons invoquer dans la bibliothèque
chargée.
Toutefois, le type
void * n'est pas suffisant pour réaliser une telle
invocation.
Notre fichier d'en-tête
calc.hpp désigne par le nom
OperationFunction un type de pointeur de fonction attendant une
référence sur un tableau dynamique de réels et renvoyant un booléen.
Les instructions suivantes :
auto fnct=OperationFunction(pointer_from_dlsym);
go_on=fnct(values);
convertissent le
void * fourni par
dlsym() en un pointeur de
fonction du type souhaité afin d'invoquer la fonction désignée en lui
transmettant le paramètre attendu et en obtenant son résultat.
Fabriquez à nouveau la bibliothèque
calc afin de prendre en compte
le code que vous venez de rédiger.
Sauf si vous êtes sous Windows, l'édition de liens doit vous indiquer
que les fonctionnalités de
<dlfcn.h> sont introuvables
Il se peut également que les fonctionnalités de mise au point
(le sanitizer) provoquent un lien implicite vers les
fonctionnalités dont nous avons besoin, auquel cas le problème
ne se posera pas.
Toutefois, si nous recompilons en mode optimisé, ces fonctionnalités
de mise au point ne seront pas utilisées et le problème se
posera.
.
Il faut alors intervenir dans le fichier
GNUmakefile servant à
fabriquer la bibliothèque
calc afin de compléter la première
occurrence de la variable
LDFLAGS que nous trouvons :
La bibliothèque standard
dl fournit en effet les fonctionnalités
en question.
Exécutez le programme
prog_calc en saisissant maintenant des noms
d'opérations que vous imaginez.
Bien entendu, puisque ces opérateurs n'existent pas encore, des messages
d'erreurs explicites doivent signaler l'impossibilité de charger les
bibliothèques correspondantes.
Placez vous dans le sous-répertoire
CalcQuit qui fournit ce qui nous
permettra de réaliser l'opération
Quit servant à demander à la
calculatrice de se terminer.
En l'état, le fichier de code source
CalcQuit.cpp contient déjà la
définition de la fonction
Quit_operation() qui est conforme à ce
que nous espérons invoquer depuis la calculatrice :
- son nom est celui de l'opération accompagné du suffixe _operation,
- elle reçoit en paramètre une référence sur un tableau dynamique de réels,
- elle renvoie un résultat booléen.
Son fonctionnement est trivial puisqu'elle se contente d'indiquer par son
résultat le fait que la calculatrice doit se terminer.
La fabrication de cette bibliothèque ne doit poser aucun soucis.
En revanche, lorsque vous revenez à l'exécution du programme
prog_calc
et que vous saisissez la commande
Quit vous devez constater un
échec d'une nature différente de celle des échec précédents.
Jusqu'alors le chargement de la bibliothèque échouait car elle était
simplement introuvable ; désormais, la bibliothèque
CalcQuit
est bien chargée mais c'est la fonction
Quit_operation() qui est
introuvable.
Cet échec est dû au fait que nous utilisons la langage
C++ pour réaliser
cette fonction :
- Puisque le langage C++ autorise la surcharge, il est tout à fait
légitime de fournir plusieurs fonctions qui ont exactement le même
nom mais qui attendent des paramètres différents.
- Dans ces conditions et de manière générale, le seul nom de la fonction
n'est pas suffisant pour retrouver une surcharge particulière parmi
toutes celles qui ont le même nom (même s'il n'y en a qu'une dans le
cas présent).
- En interne, dans le code exécutable et les bibliothèques, chaque fonction
est désignée précisément par un symbole qui reprend le nom de
la fonction mais qui le complète par des caractères supplémentaires
afin de différencier les jeux de paramètres des différentes
surcharges ; c'est un tel symbole que la fonction dlsym()
recherche.
- Malheureusement, la norme du langage C++ n'impose pas la façon de
déterminer précisément ces symboles et chaque version de compilateur
peut décider de procéder différemment ; il nous est donc très
difficile d'indiquer à dlsym() le symbole précis à rechercher
Nous pourrions le faire en étudiant les règles précises de nommage des
symboles par un compilateur particulier.
Cependant, en changeant d'environnement de développement il faudrait
probablement recourir à d'autres règles de nommage des symboles, ce
qui rendrait la portabilité du code très délicate.
.
- Heureusement, en plaçant la notation extern "C" juste avant le type de
retour de la fonction, nous demandons au compilateur d'utiliser la règle
de nommage des symboles du langage C : le nom du symbole n'est rien
d'autre que le nom de la fonction
Il en est ainsi en langage C car la surcharge n'est pas autorisée :
à un nom de fonction ne peut correspondre qu'une seule fonction, donc
ce même nom sert pour le symbole.
Bien entendu, en C++ nous ne pouvons utiliser extern "C"
que pour une seule surcharge d'un même nom de fonction, sinon
nous retomberions sur l’ambiguïté initiale.
.
Placez donc la notation
extern "C" juste avant le type de retour
bool de la fonction
Quit_operation().
Après une nouvelle fabrication de la bibliothèque
CalcQuit, vous
devriez constater que désormais, à la saisie de
Quit, la
fonction
Quit_operation() de cette bibliothèque est bien
invoquée, ce qui provoque la terminaison de la calculatrice.
Si vous avez bien compris le principe de ce que vous venez de réaliser,
il est désormais possible d'étendre à l'infini le jeux d'opérations de
votre calculatrice.
Par exemple, pour ajouter l'opération
Plus :
- créez un nouveau sous-répertoire CalcPlus à coté du sous-répertoire
CalcQuit,
- recopiez-y le fichier GNUmakefile de CalcQuit,
- modifiez ce nouveau fichier GNUmakefile afin de remplacer la
ligne LIB_TARGET=CalcQuit par LIB_TARGET=CalcPlus (le nom
de la nouvelle bibliothèque à fabriquer),
- recopiez le fichier CalcQuit/CalcQuit.cpp vers
CalcPlus/CalcPlus.cpp,
- renommez la fonction Quit_operation() en Plus_operation()
dans ce nouveau fichier,
- complétez son algorithme,
- si size(values) est inférieur à deux, un simple message d'erreur
explicite suffira,
- sinon
- placez dans une variable a le sommet de pile values.back(),
- retirez le sommet de pile avec values.pop_back(),
- placez dans une variable b le sommet de pile values.back(),
- retirez le sommet de pile avec values.pop_back(),
- empilez la somme de a et b avec values.emplace_back(a+b),
- dans tous les cas cette fonction renverra true car il ne faut
pas arrêter la calculatrice.
Sans arrêter le programme
prog_calc qui est en cours fonctionnement
dans autre terminal et qui échoue encore à utiliser l'opération
Plus, fabriquez la bibliothèque
CalcPlus.
Une nouvelle tentative d'invocation de l'opération
Plus doit alors
aboutir.
Cette nouvelle opération a été rendue accessible sans même devoir arrêter
le programme en cours de fonctionnement : voilà qui illustre encore
l'aspect dynamique du procédé.
Pour aller plus loin dans cette idée, toujours sans arrêter le programme
prog_calc qui est en cours fonctionnement, apportez une modification
triviale au code source de l'opération
Plus (l'affichage d'un simple
message par exemple), refabriquez cette bibliothèque et invoquez à
nouveau cette même opération.
Vous devez constater que le changement de comportement de l'opération est
perceptible alors que nous n'avons même pas arrêté le programme.
Ce dernier point n'est vrai que parce que nous prenons soin de libérer
chaque
plugin (avec
dlclose()) dès que nous avons fini de
nous en servir.
Si vous oubliez volontairement (à l'aide d'un commentaire) l'appel à
dlclose() dans
calc.cpp et que vous reprenez la même
expérimentation
Il faut bien entendu refabriquer la bibliothèque calc et
relancer prog_calc pour qu'il la prenne en compte.
vous constaterez qu'une fois chargé, un
plugin ne change
plus de comportement (les chargements suivants du
plugin de
même nom sont en quelque sorte ignorés
En fait un compteur de références sur cette ressource est simplement
incrémenté.
Les éventuels appels à dlclose() décrémentent ce compteur de
références et ce n'est que lorsqu'il retombe à zéro que le
plugin est effectivement déchargé.
).
Bien entendu, cet oubli est une très mauvaise pratique (semblable à
l'oubli d'une libération de mémoire ou d'une fermeture de fichier) ;
revenez alors à la situation initiale (sans l'oubli de
dlclose()).
Si vous souhaitez vous amuser, il ne reste plus qu'à inventer toute
une collection d'opérations reposant sur le même principe.
La seule limite est désormais votre imagination.