Bibliothèques et fonctions
Introduction
Les bibliothèques sont un objet particulier des systèmes d’exploitation qui permet de factoriser le code. Cela signifie que certaines fonctionnalités peuvent ainsi être d’une part partagées par les programmes, et d’autre part, que les programmes peuvent profiter des mêmes fonctionnalités sans pour autant les implémenter. Les bibliothèques peuvent généralement être présentes sous deux formes sur les systèmes modernes : statique ou dynamique. Dans le cas des bibliothèques dynamiques, le système charge la bibliothèque à la demande du premier programme qui en a besoin, et tout autre programme peut ensuite profiter immédiatement des mêmes fonctionnalités. Dans le cas des bibliothèques statiques, les exécutables contiennent une forme binaire de celle-ci avec le programme, ce qui est généralement utilisé lorsque la bibliothèque n’est pas installée au préalable et que vous ne comptez pas le faire.
Les bibliothèques permettent donc de ne pas réinventer la roue, et souvent, d’utiliser du code plus optimisé que vous auriez pu ne l’écrire vous-même (par manque de connaissance, d’expérience ou tout simplement de temps). De plus, une bibliothèque est souvent utilisée par plusieurs programmes...
Créer et utiliser une bibliothèque avec les outils GNU
Problème
Vos fonctions sont utilisées dans plusieurs de vos programmes et vous voulez factoriser votre code en le plaçant dans une bibliothèque que vos programmes utiliseront.
Solution
Compilez vos fichiers source avec l’option -fPIC du compilateur gcc. Donnez à l’éditeur de liens les options suivantes : -shared -Wl,-soname,nom.so.majeur
Discussion
Supposons que nous voulions créer une bibliothèque libtest.so.1.2 à partir des fichiers source1.c et source2.c.
Ces fichiers source sont semblables à des fichiers source de programmes normaux. L’absence de fonction main() propre aux programmes constitue la seule exception notable.
Nous compilons d’abord les fichiers objets avec :
gcc -Wall -c -fPIC source1.c -o source1.o
gcc -Wall -c -fPIC source2.c -o source2.o
À l’édition des liens, vous exécutez :
gcc -shared -Wl,-soname,libtest.so.1 source1.o source2.o
-o libtest.so.1.2
Attention ! Remarquez l’absence d’espace après les virgules pour la syntaxe de l’option -Wl.
Si la création d’une bibliothèque partagée peut sembler simple, il faut néanmoins avoir en tête certains points. Commençons par aborder le principe de nommage. En effet, le nom d’une bibliothèque n’est pas simplement...
Charger une bibliothèque de manière dynamique
Problème
Vous souhaitez charger une bibliothèque de manière dynamique, c’est-à-dire seulement au moment choisi et non pas automatiquement au lancement du programme.
Solution
Utilisez dlopen().
Discussion
La fonction dlopen() s’utilise comme d’autres fonctions habituelles d’ouverture de fichiers. Son premier argument est en effet le nom du fichier contenant la bibliothèque dynamique à ouvrir et le deuxième argument comprend les modes et options d’ouverture. Le choix est assez restreint pour le mode. La seule différence entre RTLD_LAZY et RTLD_NOW réside dans l’indication que les symboles contenus dans la bibliothèque sont résolus. Ils le sont au besoin pour le premier, ce qui est préférable pour des raisons de performances, ou dès l’ouverture de la bibliothèque pour le second, ce qui permet de découvrir tous les symboles indéfinis dès l’ouverture. Deux options supplémentaires sont disponibles. RTLD_GLOBAL permet à d’autres objets d’utiliser les symboles de la bibliothèque pour résoudre des références indéfinies dans ces objets, alors que RTLD_LOCAL l’en empêche.
Lors de l’ouverture d’une bibliothèque partagée, le système recherche une fonction...
Lancer une fonction d’une bibliothèque dynamique
Problème
Vous souhaitez, après avoir chargé une bibliothèque avec dlopen(), que votre programme exécute une fonction de celle-ci.
Solution
Utilisez dlsym() pour obtenir une référence sur la fonction et l’attribuer à un pointeur. Puis exécutez-la en utilisant ce pointeur sur fonction.
Discussion
Supposons qu’une bibliothèque, chargée avec dlopen(), contienne la fonction module_test(), voici le code pour exécuter cette fonction :
/* Prototype de la fonction module_test() */
int module_test (int arg1, char *arg2);
/* Chargement de la bibliothèque libtest.so
* et exécution de la fonction.
*/
void *dh;
int (*m_test) (int arg1, char *arg2);
int resultat;
if (NULL == (dh = dlopen ("libtest.so", RTLD_LAZY)))
{
fprintf (stderr, "Impossible d'ouvrir libtest.so\n");
exit (EXIT_FAILURE);
}
if (NULL == (m_test = dlsym (dh, "module_test")))
{
fprintf (stderr, "Impossible d'obtenir module_test\n");
exit (EXIT_FAILURE);
}
resultat = m_test (1, "un");
printf ("Résultat = %d\n", resultat);
Prototypes
#include <dlfcn.h>
void *dlsym (void *restrict handle, const char *restrict...
Écrire un greffon
Problème
Vous souhaitez profiter de dlopen() pour écrire un greffon.
Solution
Écrivez l’API que chaque greffon devra respecter. Créez le chargeur de greffons (appel à dlopen(), puis exécution d’une fonction d’initialisation du greffon). Créez son déchargeur si nécessaire (exécution d’une fonction de destruction du greffon, suivie d’un appel à dlclose()). Enfin terminez par l’écriture du greffon lui-même.
Discussion
L’utilisation de dlopen() et dlsym() ouvre la voie vers la programmation de greffons (plug-in) car il est très facile de charger un greffon (une bibliothèque dynamique) avec dlopen() puis d’exécuter le code contenu dans ce greffon en lançant ses fonctions, dont vous obtenez les références avec dlsym(). Il est aussi très facile de programmer des greffons qui arrêteront le programme de manière inattendue si nous ne prenons pas au préalable quelques précautions.
Lors de la réalisation d’un système de greffons, il faut d’abord écrire en une API l’interface à laquelle le greffon doit se plier. Toutes les fonctions que le programme est susceptible d’appeler et que le greffon doit implémenter doivent être référencées. Dans ce référencement se trouve non seulement le nom des fonctions, mais aussi leur prototype, qui doit être immuable à moins d’implémenter un système de gestion de versions des greffons.
Lorsque cette API est réalisée, nous réalisons la partie la plus délicate : le chargeur de greffons. Celui-ci effectue un appel à dlopen() pour charger le greffon, suivi d’un appel à dlsym() pour obtenir le nom d’une fonction générique à appeler pour initialiser le greffon. Deux implémentations sont ensuite possibles pour le chargeur et le programme.
La première implémentation consiste à stocker l’identifiant de bibliothèque retourné par dlopen(), et à s’en servir via dlsym() à chaque fois que vous souhaitez appeler une fonction d’un greffon. Cette méthode, qui fonctionne très bien, présente néanmoins...
Lancer une fonction du programme depuis la bibliothèque dynamique
Problème
Vous souhaitez proposer la possibilité à la bibliothèque dynamique d’exécuter une fonction de votre programme, telle qu’une fonction de notification d’erreur.
Solution
Disposez d’un pointeur sur la fonction du programme à exécuter dans la bibliothèque dynamique. Après son chargement avec dlopen(), exécutez une fonction de la bibliothèque dynamique dont l’argument est un pointeur sur la fonction du programme à exécuter. Cette fonction de la bibliothèque dynamique stocke cet argument dans le pointeur réservé à cet effet.
Discussion
Supposons que la fonction du programme à exécuter depuis la bibliothèque dynamique s’appelle prog_log(), de prototype void prog_log(const char*). Voici le code à placer dans la bibliothèque dynamique :
void (*prog_log) (const char *);
void
set_prog_log (void (*f) (const char *))
{
prog_log = f;
}
Après le chargement de la bibliothèque dynamique avec dlopen(), cette fonction pourra être exécutée dans le programme :
void *handle;
void (*set_prog_log) (const char *);
if (NULL == (handle = dlopen ("plugin.so", RTLD_LAZY)))
{
fprintf (stderr, "Problème...
Créer une bibliothèque dynamique liée statiquement aux bibliothèques dont elle dépend
Problème
Vous générez une bibliothèque dynamique, liée à d’autres bibliothèques, mais vous souhaitez lier statiquement ces autres bibliothèques à la vôtre.
Solution
Indiquez la forme statique des bibliothèques à lier à votre bibliothèque dynamique.
Discussion
Lorsque vous déployez un programme ou une bibliothèque dynamique, vous pouvez avoir besoin de lier statiquement les bibliothèques dont la vôtre dépend. Cela offre l’avantage de ne pas dépendre de ces bibliothèques dans l’environnement où votre bibliothèque va être utilisée. Pour cela, il suffit de disposer des bibliothèques statiques dont dépend la vôtre et de les indiquer ainsi à l’éditeur de liens :
$ gcc -shared -Wl,-soname,libtest.so.1 source1.o source2.o
/usr/lib/libglib-2.0.a -o libtest.so.1.0.0
Cet exemple montre comment générer une bibliothèque dynamique liée statiquement à la bibliothèque glib.
Cette manière de faire présente néanmoins un danger lors d’une séance de débogage. En effet, une modification effectuée dans une bibliothèque dépendante n’est...