Création de plugin avec Qt
Par Nicolas le dimanche, février 18 2007, 23:11 - Tutoriels - Lien permanent
- Comment créer des plugins avec Qt ? et de manière multi-plateforme ?
- Comment permettre à mes utilisateurs d'étendre eux-mêmes mon application ?
Ce tutoriel va nous détailler la création d'un système de plugins avec la bibliothèque Qt, et le chargement de ces plugins depuis notre application.
Version : Qt4 (supérieur ou égal à Qt 4.1)
Auteur : Nicolas Arnaud-Cormos (nikikko)
Test : Linux (Qt 4.1, 4.2)
Plugin, vous avez dit plugin ?
Qu'est-ce que c'est ?
Je n'ai pas réussit à faire mieux que Wikipedia pour la définition :
En informatique, un plugin ou plug-in, de l'anglais to plug in (brancher), parfois traduit en module externe, module enfichable, module d'extension, greffon ou plugiciel, est un logiciel tiers venant se greffer à un logiciel principal afin de lui apporter de nouvelles fonctionnalités. Le logiciel principal fixe un standard d'échange d'informations auquel ses modules se conforment. Le module n'est généralement pas conçu pour fonctionner seul mais avec un autre programme.
Comment ça marche ?
C'est relativement simple (en théorie ;)) :
- Le logiciel principal défini une API standard : un ensemble de classes et de fonctions pouvant être utilisées/héritées par les plugins. C'est le lien, le langage commun entre le logiciel principal et les plugins.
- Les plugins se basent sur cette API, et créent ainsi de nouvelles fonctionnalités pour le logiciel principal. Ils se présentent généralement sous forme de bibliothèques dynamiques (*.dll sous Windows ou *.so sous Linux). Pour pouvoir être chargés par le programme principal, ils doivent être copiés dans un répertoire spécifique.
- Le logiciel principal, au démarrage, charge les plugins qu'il trouve, ajoutant ainsi de nouvelles fonctionnalités.
Avantages ?
Les avantages d'un système de plugins sont nombreux. En voici quelques uns :
- l'utilisateur ou un intervenant externe peut étendre les fonctionnalités d'une application sans avoir son code source,
- le logiciel est beaucoup plus modulaire : possibilité de faire des versions réduite, version complète...
- la mise en place d'un système de plugin force à réfléchir un peu à l'architecture du logiciel, ce qui n'est pas un mal ;)
Les plugins avec Qt ? (pourquoi c'est pratique)
La mise en place d'un système de plugin est parfois assez lourde et nécessite par exemple de vérifier dans le programme principal si un plugin contient telle ou telle fonction. Je ne parle même pas d'un système multi-plateforme... heureusement, Qt est là.
Grâce à Qt, la réalisation d'un système de plugins multi-plateforme est relativement simple : la documentation Qt sur le système de plugins.
CalcOp
Pour illustrer la création de plugin, nous allons utiliser un exemple assez simple : un petit projet que j'ai nommé calcop, et qui permet à partir de 1, 2 ou 3 variables, d'effectuer une opération au choix de l'utilisateur (par exemple des additions, multiplications, remplacement de caractères dans une chaîne...).
Ça ne sert à rien, mais le principal c'est que l'exemple est très simple, il permet donc de se concentrer sur le code lié à la gestion de plugin. Le code source est livré avec deux plugins :
- un plugin contenant des opérations mathématiques (addition, multiplication, division, soustraction, factoriel)
- un plugin contenant des opérations sur les chaînes (inversion de sens et remplacement dans une chaîne)
Les sources du logiciel sont téléchargeables ici. Je vous conseille de les télécharger avant de vous lancer dans la lecture du tutoriel.
Le programme principal
Le programme principal est relativement simple. Je ne vais pas détailler tout le code, mais je vais me focaliser sur les parties nécessaires à la mise en place de plugins.
La première chose à faire, c'est de définir un ensemble d'interfaces à nos plugins : ce sont des classes avec seulement des fonctions virtuelles pures utilisées pour dialoguer entre l'application et les plugins. Il est possible bien entendu de définir plusieurs interfaces, pour différents types de plugin, mais dans notre cas nous n'en définissons qu'une seule, OperationInterface :
class OperationInterface { public: virtual ~OperationInterface() {} virtual QStringList operationList() = 0; virtualbool canCalculate( QString opName ) = 0; virtualint numVariable( QString opName ) = 0; virtual QString calculate( QString opName, QStringList variableList ) = 0; }; Q_DECLARE_INTERFACE(OperationInterface, "org.nikikko.CalcOp.OperationInterface/1.0")
La macro Q_DECLARE_INTERFACE permet d'indiquer lors de la compilation (à l'outil moc, le précompilateur Qt) que cette classe est une interface.
Attention : le deuxième élément de la macro
"org.nikikko.CalcOp.OperationInterface"doit obligatoirement se finir par le nom de la classe (je me suis fait avoir au début).
Le reste du code, notamment la classe OperationManager, sera détaillé par la suite.
Création d'un plugin
La création du plugin est relativement simple. Il suffit de créer une classe qui hérite de notre interface OperationInterface. Voici par exemple le code pour le plugin mathématique :
#include <QObject> #include <operationinterface.h> class MathPlugin : public QObject, public OperationInterface { Q_OBJECT Q_INTERFACES(OperationInterface) public: QStringList operationList(); bool canCalculate( QString opName ); int numVariable( QString opName ); QString calculate( QString opName, QStringList variableList ); };
Deux remarques importantes :
- la classe de base d'un plugin (qui hérite d'une classe d'interface) doit toujours hériter de
QObject, - toutes les fonctions de l'interface doivent être définies.
Note : il est possible dans un plugin d'avoir une classe qui hérite de plusieurs interfaces, pour cela voir l'exemple plugandpaint de Qt.
Et dans le fichier source :
Q_EXPORT_PLUGIN2(calcop_math, MathPlugin)
La macro Q_EXPORT_PLUGIN2 indique au compilateur qu'il faut exporter cette classe, et que cette classe est le point d'entrée du plugin. Pour faire simple, une classe exportée est une classe qui est visible à l'extérieur de la bibliothèque (donc par le programme principal).
Enfin, il faut indiquer dans le fichier .pro que l'on souhaite créer un plugin. Voici le fichier .pro correspondant au plugin calcop_math :
TEMPLATE = lib CONFIG += release plugin INCLUDEPATH += ../../src HEADERS = mathplugin.h SOURCES = mathplugin.cpp TARGET = calcop_math DESTDIR = ../../bin/plugins
Voilà quelques explications :
TEMPLATE = lib: création d'une bibliothèqueCONFIG += plugin: cette bibliothèque est un pluginTARGET = calcop_math: ce plugin s'appelle calcop_math (même nom que dans la macroQ_EXPORT_PLUGIN2)DESTDIR = ../../bin/plugins: mettre ce plugin dans le bon répertoire (répertoire des plugins de l'application)
Voilà, c'est relativement simple à faire, et en plus c'est multi-plateforme.
Chargement des plugins dans l'application
Dernière étape, c'est le chargement des plugins dans l'application. Tout ce qui concerne la gestion des plugins se fait à travers la classe OperationManager dans le programme principal. Cette classe charge les plugins, et s'occupe d'effectuer les appels nécessaires aux fonctions des plugins lors du calcul.
Pour me faciliter la vie, j'ai décidé d'en faire un singleton : c'est pour ça que le constructeur est privé, et que la classe comporte une fonction membre statique instance. Mais le code le plus intéressant se trouve dans le constructeur de cette classe :
QDir pluginsDir = QDir(qApp->applicationDirPath()); pluginsDir.cd("plugins"); foreach (QString fileName, pluginsDir.entryList(QDir::Files)) { QPluginLoader loader(pluginsDir.absoluteFilePath(fileName)); QObject *plugin = loader.instance(); if (plugin) { OperationInterface * op = qobject_cast<OperationInterface *>(plugin); if (op) { m_operationList << op; } } }
Nous avons trois actions importantes dans ce code :
- déplacement dans le répertoire de plugin : les deux premières lignes vont nous permettre d'aller dans le répertoire des plugins,
- chargement du plugin avec la classe
QPluginLoader: on vérifie que le plugin est bien compatible avec la version du logiciel et de Qt, et crée une instance de l'objet racine (qui est une classe héritée de l'interface, dans notre cas ce sera une instance deMathPlugin). - vérification de la compatibilité du plugin : de quelle interface hérite-t-il (ici
OperationInterface)
Si tout se passe bien, on enregistre le plugin dans la liste des opérations. Et voilà, c'est simple, clair et propre...
La classe QPluginLoader est au coeur de ce code : c'est elle qui va se charger de créer une instance du plugin. Au passage, l'instance créé, que l'on récupère à l'aide de la fonction instance(), est statique : si l'on utilise le même code dans une autre fonction, l'instance renvoyé sera toujours la même (ie le même pointeur).



Commentaires
Bjr,
Je me mets à QT avec Dev-Cpp pr la compilation (bon choix?).
Il me manque un peu de code pour tout comprendre et je n'archive pas à extraire l'archive:peux tu la mettre en zip ou rar à la place de tar?
Il y a t il d'autres tutoriaux?
Merci.
Alexandre
Dev-Cpp n'est pas forcément le bon choix, vu qu'il n'est plus maintenu... je te conseille plutôt des IDE spécialisés Qt4 comme : MonkeyStudio, QDevelop, Edyuk.
Pour l'archive, je ne peux que te conseiller d'utiliser 7-zip : http://www.7-zip.org/fr/
Enfin, pour les autres tutoriaux, il y en a qui sont en cours d'écriture, d'autres qui n'attendent qu'à être écrits, mais on manque de temps (et parfois de motivation). Si tu as un problème particulier, va sur notre forum.
J'ai beau compiler les sources, les plugins ne se charge pas, pourtant ils existent bien...
Aucune erreure à la compilation.
Je suis sur Leopard.
Je lance:
qmake -spec macx-g++ calcop.pro
puis
make
Je me demande ou est le blem. Merci pour ce superbe tutoriel.
Peux-tu vérifier que cette ligne est bien exécutée?
OperationInterface * op = qobject_cast<OperationInterface *>(plugin);
Si elle ne l'est pas, le programme ne cherche pas au bon endroit (je songe plus à ça qu'au problème de cast --ce sont les 2 seules pistes que je vois pour l'instant)
J’ai changé quelques lignes et tout marche à merveille.
Encore Merci pour ta réponse rapide, et pour ce tutoriel génial.
Au plaisir de te lire.
OperationManager::OperationManager()
{
QDir pluginsDir = QDir("/Developpement/calcop/bin/plugins").dirName(); //Mon chemin
foreach (QString fileName, pluginsDir.entryList(QDir::Files)) {
QPluginLoader loader(pluginsDir.absoluteFilePath(fileName));
QObject *plugin = loader.instance();
if (plugin) { OperationInterface * op = qobject_cast<OperationInterface *>(plugin) ;
if (op) { m_operationList << op; }
}
}
}
en Qt4.4, est-il possible d'utiliser les mécanismes de Signal/Slot au sein de l'interface des plugins ? Dans l'objectif d'émettre un signal depuis le plug vers l'appli, et de connecter un sig de l'appli au plug ?
Quelle méthode ? Exemple :
class Interface_Plug_Sig_Slot : virtual public QObject {
Q_OBJECT
public :
virtual ~Interface_Plug_Sig_Slot();
virtual void fonction() = 0;
public slots:
virtual void on_Appli_Sends_Signal_To_This_Slot() = 0;
signals:
virtual void plug_Emit_A_Signal_To_Appli() = 0;
};
Et dans le plug :
void fonction()
{ emit plug_Emit_A_Signal_To_Appli(); }
Dans la classe QObject de l'appli :
connect( this, SIGNAL( nia_nia() ), plugin, SLOT( nia_nia_toi_meme() ) );
Un truc de ce style fonctionnerait-il ?
A+
Et merci pour toutes vos infos !
Eric (medintux.org)
J'ai trouvé ce thread mais hard to understand...
http://lists.trolltech.com/qt-inter...
Il semble qu'il faille réaliser un kit de classes abstraites (dérivant QObject) que le plugins envoie et connecter ces classes.
Une partie du thread ici :
class AbstractPluginFactory
{
public:
virutal AbstractClass* createMyClass()=0;
};
Q_DECLARE_INTERFACE(AbstractPluginFactory)
class AbstractClass : public QObject
{
Q_OBJECT
public:
//Everything is legal here and inherited correctly
};
class MyClass : public AbstractClass
{
Q_OBJECT
public:
//Do whatever you usually do for QObjects
};
class MyPluginFactory : public QObject, public AbstractPluginFactory
{
Q_OBJECT
Q_INTERFACES(AbstractPluginFactory)
public:
virtual AbstractClass* createMyClass() { return new MyClass };
};
MedinTux - Eric>
Désolé de la réponse tardive, je pense que nous sommes assez peu à surveiller les commentaires ;)
Ce que tu as décrit dans ton premier post fonctionne, à une exception: le signal. Un signal ne peut pas être abstrait car ce n'est pas une méthode virtuelle (ou plus exactement, c'est inutile de la rendre virtual).
Le seul bémol est la nécessité du fichier résultant du moc. En général, je contourne le problème en faisant une lib statique par domaine où les interfaces et son moc sont compilés. Il ne reste plus qu'à lier contre cette lib dans le plugin pour que le code généré par le moc soit présent.
L'approche de la factory est plus puissante bien sûr; quant à savoir quelle approche adopter, ça dépend entièrement du problème à résoudre ;)