Mécanisme de plugin sous GNU/Linux en C++ et CMake

Introduction

Un système de plugin permet, à une application, de pouvoir charger de façon dynamique des ”bouts de code”.

En fonction de l’implémentation du mécanisme de plugins, ces bouts de code peuvent être compilés (bibliothèque dynamique), pré-compilés (un objet java) ou même être lisibles directement par l’utilisateur, dans ce cas on parle de script (GIMP utilise, par exemple, le python pour certains de ces plugins).

Quelque soit la forme d’un plugin, sa fonction est toujours la même, apporter de nouvelles fonctionnalités à un programme.

Ce mécanisme permet donc de simplement rajouter des fonctionnalités à une application, en évitant, par exemple, de recompiler celle ci à chaque modification d’un petit bout de code.

Tout cela est bien beau mais ne semble pas répondre à des contraintes très strictes. Il peut arriver parfois que ce genre de mécanisme s’avère indispensable au fonctionnement d’une application.

Imaginons qu’une entreprise A développe une application pour une société B. A fournira l’application à B sous la forme d’un code machine dit binaire. Mais A ne fournira pas, sauf accord contraire, le code source à B. Dans ce cas un mécanisme de plugins est la seule solution pour permettre à B de faire évoluer les fonctionnalités de l’application (encore faut il que A implémente ce mécanisme, ce qui n’est pas dans son intérêt, qui fera donc en général partie du cahier des charges. Bien entendu, cela suppose que B ait songé à l’avenir de son application).

Implémentation

Dans le cas de Linux et d’une application écrite en C++, un plugin est une bibliothèque (dynamique ou statique) qui sera soit chargée à la demande (dans le cas des librairies dynamiques), soit inclue dans le code binaire de l’application (cas des bibliothèques statiques, mais dans ce cas là ce n’est plus un plugin mais une partie du code source de l’application).

Pour pouvoir charger une bibliothèque dynamique, il faut utiliser l’interface de programmation pour le chargeur de bibliothèques dynamiques (dlfcn.h) sous Linux.

Cette interface fournit 5 fonctions : dladdr, dlclose, dlerror, dlopen, dlsym, dlvsym. Le bout de code suivant remplit ce rôle (exemple tiré de man dlopen) :

Charger la bibliothèque mathématique et afficher le cosinus de 2,0 : (la gestion des erreurs a été retirée pour simplifier la compréhension)

#include <stdio.h>
#include <dlfcn.h>

int main(int argc, char **argv) { void *handle; double (*cosine)(double); char *error;

handle = dlopen (“libm.so”, RTLD_LAZY); //On charge la bibliotheque dynamique

*(void **) (&cosine) = dlsym(handle, “cos”); //Appel d’une fonction de la bibliothèque

printf (“%f\n”, (*cosine)(2.0)); dlclose(handle); return 0; }

Supposons que le programme s’appelle foo.c, on doit le compiler ainsi :gcc -rdynamic -o foo foo.c -ldl

Une bibliothèque (disons malib.c) sera compilée ainsi gcc -shared -nostartfiles -o malib malib.c

Cet exemple nous permet de voir comment l’on charge une bibliothèque dynamique. L’application utilisera exactement cette méthode pour charger les plugins.

Pour pouvoir utiliser des plugins, notre application va donc devoir charger des bibliothèques dynamiques. Pour simplifier, on regroupera tous les plugins dans un même répertoire.

Notre application possédera un objet plugin_manager dont le rôle sera de charger toutes les bibliothèques dynamiques se trouvant dans le répertoire plugins au démarrage. Cet objet construira une liste des plugins disponibles pour simplifier leur manipulation.

Il reste un problème un peu délicat à résoudre. Étant donné que l’on développe en C++, le plugin ne sera pas qu’une simple bibliothèque de fonctions, que l’on pourrait appeler avec dlsym, mais plutôt la définition d’un objet et de ses méthodes. Le problème est donc de trouver un moyen de créer une instance de ces objets lors du chargement des plugins.

Pour cela on utilise le fait que, lors du chargement d’une bibliothèque dynamique par dlopen, les variables globales se voient initialisées automatiquement.

Aussi chaque plugin, déclarera une variable globale, qui sera instancié au chargement :

#include <iostream>

#include <plugin_factory_initializer.h> #include “plugin1.h”

namespace Plugin { class Plugin1Factory : public PluginFactory {

};

PluginFactoryInitializer plugin1FactoryInitializer; } // namespace Plugin

Cette variable repose sur un template, PluginFactoryInitializer, pour enregistrer chaque plugin :

#ifndef _PLUGIN_FACTORY_INITIALIZERH
#define _PLUGIN_FACTORY_INITIALIZERH

//STL #include <iostream> #include <string>

#include “plugin_factory_manager.h”

namespace Plugin { template <typename T> class PluginFactoryInitializer { public: PluginFactoryInitializer() { T* factory = new T(); PluginFactoryManager::instance()->factories[factory->getName()] = factory; std::cout << “Plugins: “ << factory->getName() << “ registered\n”; } }; } //namespace Plugin #endif //_PLUGIN_FACTORY_INITIALIZERH

L’utilisation d’un std::map<std::string, iplugin*> permet d’avoir une table de hashage indexée sur le nom du plugin possédant un pointeur sur l’objet initialisé lors du chargement des plugins. Par la suite, en supposant que la table de hashage se nomme m_plugins, on accédera à l’objet d’écrit dans le plugin1 par : m_plugins[“plugin1”].

Compilation modulaire

Un autre problème est un peu délicat à résoudre: le fait de permettre de compiler l’application et ses plugins :

- En un gros binaire monolytique.
- En un binaire et ses plugins (plusieurs bibliothèques dynamiques).
- Un mix entre les deux (On inclut dans le binaire de l’application tel ou tel plugin, et les autres en bibliothèques dynamiques).

Sans modification du code source des plugins bien entendu.

Cette fonctionnalité, prend tout son sens lorsqu’elle est envisagée dans un cadre commercial.

En fait on va obtenir cet effet d’une façon détournée. Quand on voudra compiler un plugin dynamiquement on fera comme avant. C’est uniquement dans le cas d’un lien statique, avec le plugin, que l’on va redéfinir le comportement de l’outil de compilation.

Supposons que notre application possède deux plugins : plugin1 et plugin2.

On commence par définir un fichier qui contiendra les informations de compilation des plugins :

# 0 = Compilation dynamique du plugin

1 = Compilation statique du plugin

SET(PLUGIN1 0) #On definie (au sens cmake) une variable PLUGIN1 valant 0. SET(PLUGIN2 0)

Dans cette configuration, l’application n’intégrera aucun plugin, et la compilation produira 2 plugins sous forme de bibliothèque dynamique.

On enregistre ce fichier dans plugins.cmake que l’on place à côté du CMakeLists.txt principal.

Maintenant il faut modifier le CMakeLists.txt principal pour qu’il intègre ce fichier, on lui ajoute donc :

INCLUDE(plugins.cmake)

Grâce à cette directive, cmake a maintenant conscience de ce que l’on veut faire avec chaque plugin. Il reste à lui expliquer comment le faire :

##############################################################
#Gestion de la compilation dynamique ou statique des plugins
##############################################################
IF(${PLUGIN1} EQUAL 1)
AUX_SOURCE_DIRECTORY(plugins/plugin1 plugin1_src)
SET(projet_src ${projet_src} ${plugin1_src})
ENDIF(${GREEFON1} EQUAL 1)

IF(${PLUGIN2} EQUAL 1) AUX_SOURCE_DIRECTORY(plugins/plugin2 plugin2_src) SET(projet_src ${project_src} ${plugin2_src}) ENDIF(${PLUGIN2} EQUAL 1) #############################################################

Explications : \

Si un plugin doit être compilé en statique, sa variable associée dans le fichier plugins.cmake vaudra 1. Dans ce cas, on entre dans le IF. A ce moment là, la directive AUX_SOURCE_DIRECTORY ajoute la liste des fichiers sources du plugin plugin_src (défini dans le CMakeLists.txt du plugin) comme source additionnelle de l’application. Le SET qui suit permet de dire à cmake que les fichiers du plugins font partie du code source du binaire principal. On a ainsi compilé le plugin en statique avec le binaire de notre application.

Mais puisque l’on a rien modifié d’autre on risque d’avoir aussi une bibliothèque dynamique qui va être générée. Il reste à corriger cela.

En fait, nous allons simplement utiliser l’ancien comportement quand la variable vaut 1, sinon on ne fera rien (puisque le problème est résolu dans le CMakeLists.txt principal).

Voyons comment se présente le CMakeLists.txt de plugin1 :

set(plugin1_src plugin1.cpp)

IF(${GREEFON1} EQUAL 1) MESSAGE(STATUS “plugin1 : STATIC”) ELSE(${GREEFON1} EQUAL 1) ADD_LIBRARY(plugin1 SHARED ${plugin1_src}) TARGET_LINK_LIBRARIES(plugin1plugin manager directory nl) MESSAGE(STATUS “plugin1 : DYNAMIC”) ENDIF(${GREEFON1} EQUAL 1)

L’application peut maintenant compiler dans n’importe quel mode à condition de bien renseigner le fichier plugins.cmake.

Références

C++ dlopen mini HOWTO
Dynamically Loaded (DL) Libraries

Historique

Version 1.00, Septembre 2007 : Création du document
Version 1.01, Aout 2011 : s/plugin/plugin/
Version 1.10, Septembre 2011 : Mise en ligne du document en PDF
Version 1.20, Février 2013 : Mise en ligne du document en HTML