Ajoutez des fonctions plugin à votre code

Linux Gazette n°84 - Novembre 2002

Tom Bradley


Table des matières
1. Introduction
2. Comment travailler avec des plugins
3. Un programme de chargeur simple pour des plugins
4. Deux plugins simples
5. Utilisation du chargeur
6. Ajout de fonctions de comptabilité pour des plugins
7. Conclusion

1. Introduction

La durée de vie d'un programme en tant qu'entité unique est bien loin de disparaître. Les programmes actuels se doivent d'être plus polyvalents et plus extensibles. La manière la plus simple d'offrir souplesse et extensibilité à votre programme passe par l'utilisation de modules également connus sous le terme de «plugins». Les navigateurs web et les lecteurs musicaux sont deux bons exemples de programmes autorisant les plugins. Les navigateurs utilisent des plugins pour ajouter aux pages web des fonctionnalités telles que Java, Flash et QuickTime, de façon à ce que vous puissiez bénéficier d'une expérience de surf plus riche. Les lecteurs musicaux tels que XMMS utilisent des plugins pour gérer différents encodages et disposer en outre de plugins visuels permettant de suivre vos morceaux sur l'écran. Cet article décrit comment fournir la gestion des plugins à vos programmes. Remarque : pour les besoins de cet article, j'utiliserai indifféremment les termes «module» et «plugin» : ils sont identiques.


2. Comment travailler avec des plugins

Pour travailler avec des plugins, seules quatre fonctions sont nécessaires. Elles font partie de la bibliothèque dl (Dynamic Loader). Je n'en fournirai ici qu'une brève introduction. Vous pouvez consulter les pages d'info pour chacune d'elles afin d'obtenir une description plus exhaustive.

dlopen

Cette fonction permet de charger un module en mémoire.

dlclose

Cette fonction permet de décharger le module de la mémoire.

dlsym

Cette fonction permet de rechercher et de renvoyer l'adresse d'une fonction à l'intérieur d'un module.

dlerror

Cette fonction vous renvoie un message d'erreur.


3. Un programme de chargeur simple pour des plugins

Voici le code d'un programme de chargeur simple qui considère le nom du plugin comme un argument de ligne de commande.

Version texte de ce listing : main.c

#include (unistd.h)
#include (string.h)
#include (errno.h)
#include (dlfcn.h)

#define PATH_LENGTH 256

int main(int argc, char * argv[])
{
	char path[PATH_LENGTH], * msg = NULL;
	int (*my_entry)();
	void * module;

	/* contruisez le nom de chemin du module */
	getcwd(path, PATH_LENGTH);
	strcat(path, "/");
	strcat(path, argv[1]);

	/* chargez le module, et résolvez les symboles maintenant */
	module = dlopen(path, RTLD_NOW);
	if(!module) {
		msg = dlerror();
		if(msg != NULL) {
			dlclose(module);
			exit(1);
		}
	}

	/* recherchez l'adresse du point d'entrée */
	my_entry = dlsym(module, "entry");
	msg = dlerror();
	if(msg != NULL) {
		perror(msg);
		dlclose(module);
		exit(1);
	}

	/* appelez le point d'entrée du module */
	my_entry();

	/* fermez le module */
	if(dlclose(module)) {
		perror("error");
		exit(1);
	}

	return 0;
}

Le code est assez simple. Après avoir chargé le plugin, le chargeur examine la table des symboles des plugins à l'aide de la commande dlsym pour obtenir l'adresse de la fonction entry. Une fois l'adresse de cette fonction obtenue, je peux appeler la fonction, l'affecter au pointeur de fonction que j'ai créé. Le plugin est alors déchargé. La ligne du pointeur de fonction peut nécessiter quelque explication.

   int (*my_entry)()

sert de pointeur vers une fonction qui ne contient aucun argument et renvoie un entier. Je peux l'utiliser pour pointer sur la fonction «entrée» dans le plugin.

    int entry()

La commande suivante sert à compiler le programme du chargeur :

$ gcc -o loader main.c -ldl

4. Deux plugins simples

Maintenant que nous avons un chargeur, il nous faut quelques plugins à lui faire charger. Aucun prototype n'est défini pour un point d'entrée de modules ; vous pouvez utiliser celui de votre choix. Dans mes exemples, je fais en sorte que le point d'entrée renvoie un entier et ne contienne aucun argument. Vous pouvez configurer vos points d'entrée de façon à ce qu'ils contiennent tout argument dont ils ont besoin et renvoient celui que vous désirez. Il n'est pas non plus nécessaire de l'appeler «entrée». Je l'utilise simplement pour faciliter la compréhension de l'objectif de cette fonction. En outre, vous pouvez avoir plusieurs points d'entrée dans un plugin. Voici deux exemples de modules, dont chacun a le même point d'entrée :

Version texte de ce listing : module1.c

int entry()
{
    printf("I am module one!\n");
    return 0;
}

Version texte de ce listing : module2.c

int entry()
{
    printf("I am module two!\n");
    return 0;
}

Pour compiler les plugins :

$ gcc -fPIC -c module1.c
$ gcc -shared -o module1.so module1.o
$ gcc -fPIC -c module2.c
$ gcc -shared -o module2.so module2.o

Deux aspects sur la manière dont ils sont compilés méritent d'être notés. Tout d'abord, le drapeau -fPIC. PIC signifie «Position Independent Code» : ceci indique au compilateur que ce code devrait être configuré de façon à utiliser un espace d'adressage «relatif». Cela signifie que le code peut être placé n'importe où en mémoire et que le chargeur veille à redéfinir les adresses lors du chargement. Le drapeau -shared indique au compilateur que ce code devrait être compilé d'une façon qui lui permette d'être lié à un autre exécutable. En d'autres termes, le .so (shared object) agira de la même manière qu'une bibliothèque ; toutefois, votre .so n'est pas une bibliothèque et ne peut pas être lié à en utilisant le -l avec gcc.


5. Utilisation du chargeur

Voici les commandes permettant d'utiliser les deux plugins différents et leur sortie :

$ ./loader module1.so
Je suis le module 1 !

$ ./loader module2.so
Je suis le module 2 !

6. Ajout de fonctions de comptabilité pour des plugins

Cette section part du principe que vous utilisez le compilateur gcc du fait que les commandes utilisées sont spécifiques à gcc. D'autres compilateurs pouvant avoir des fonctionnalités similaires, vérifiez la compatibilité dans votre documentation. Gcc fournit un drapeau __attribute__ à utiliser avec les fonctions. Ce drapeau offre de nombreuses caractéristiques utiles aux fonctions ; toutefois, je n'en décrirai ici que deux. Consultez la page info sur gcc pour obtenir d'autres descriptions des autres attributs. Les deux que je souhaite évoquer sont constructor et destructor. L'exécutable ELF (Executable and Linkable Format fournit deux sections : .init et .fini pouvant contenir du code qui est exécuté avant et après le chargement d'un module (dans un programme ordinaire, ceux-ci seraient lancés avant et après l'exécution de main(). Placer du code dans ces sections peut vous permettre d'initialiser des variables ou d'effectuer d'autres fonctions de comptabilité que votre module peut exiger. Vous pourriez par exemple faire lire au module des variables à partir du programme principal dont il aura besoin pour démarrer ou lui faire définir des variables au sein du programme principal, telles que le type d'interface du plugin. Le type d'interface d'un plugin est l'ensemble des commandes que fournit le plugin en question. Dans mon exemple, il ne proposait qu'une fonction «entrée» le vôtre pourra en fournir d'autres. Voici un exemple d'utilisation de ces attributs :

__attribute__ ((constructor)) void init()
{
  /* le code ici est exécuté après que dlopen() a chargé le   module */
}


__attribute__ ((destructor)) void fini()
{
  /* le code ici est exécuté juste avant que dlclose() ne décharge le module */
}

Les noms init() et fini() ne sont pas nécessaires, je les utilise pour clarifier l'endroit où il faut placer ces fonctions pour faciliter la lecture. Plusieurs noms de fonctions sont à éviter car gcc les utilise. Citons parmi ceux-ci, _init,_fini, _start et _end. Pour afficher un listing complet des fonctions et des variables que gcc crée, exécutez nm sur le fichier binaire. Les attributs constructor et destructor indiquent au compilateur où placer le code dans le fichier binaire. Le simple fait de placer constructor indique au compilateur que la fonction correspondante se positionne dans la section .init et, de la même manière, l'attribut destructor indique au compilateur la place de la fonction correspondante dans la section .fini.


7. Conclusion

L'utilisation de la bibliothèque dl facilite la mise en place de la gestion des plugins dans votre programme. Elle offre une extensibilité et une flexibilité aisées. Bien que cet exemple ne montre que la saisie d'une fonction depuis un plugin, il est facile de s'approprier de multiples fonctions à partir d'un plugin et de les utiliser comme si elles faisaient partie du programme d'origine.

Copyright © 2002, Tom Bradley

Copying license http://www.linuxgazette.com/copying.html

Paru dans le n°84 de la Linux Gazette de novembre 2002.

Traduction française par Joëlle Cornavin .

Relecture de la traduction française par Guillaume Lelarge .