Bibliothèques et plug-ins

| Mark Summerfield | traducteur : Thibaut Cuvelier | Qt Quarterly |

Il y a trois approches pour utiliser des bibliothèques externes à Qt : les lier directement, les charger dynamiquement quand nécessaire et utiliser les plug-ins pour applications. Dans cet article, nous jetterons un oeil à ces trois approches et discuterons de leurs avantages et inconvénients.

Cet article est une traduction autorisée de Libraries and Plugins, par Mark Summerfield.

Commentez

- Titre : Bibliothèques et plug-ins

- Auteurs : Mark Summerfield, traducteur : Thibaut Cuvelier, Qt Quarterly

- Parution : 14 mai 2009

- Mise à jour : 1 août 2011

- Licence : Copyright ® 2009 Mark Summerfield. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. L'article original

- II. Introduction

- III. Inclusion directe

- IV. Chargement à l'exécution

- V. Plugins pour applications

- VI. Conclusion

- VII. Divers

I - L'article original

Qt Quarterly est une revue trimestrielle électronique proposée par Nokia à destination des développeurs et utilisateurs de Qt. Vous pouvez trouver les versions originales.

Nokia, Qt, Qt Quarterly et leurs logos sont des marques déposées de Nokia Corporation en Finlande et/ou dans les autres pays. Les autres marques déposées sont détenues par leurs propriétaires respectifs.

Cet article est la traduction de l'article Libraries and Plugins de Mark Summerfield paru dans la Qt Quarterly Issue 17.

Cet article est une traduction d'un des tutoriels écrits par Nokia Corporation and/or its subsidiary(-ies) inclus dans la documentation de Qt, en anglais. Les éventuels problèmes résultant d'une mauvaise traduction ne sont pas imputables à Nokia.

II - Introduction

Supposons que nous avons les données des ventes que nous aimerions distribuer à l'équipe globale des ventes. Le fichier est stocké dans un fichier dbm (un fichier de paires clé-valeur), que les membres de l'équipe récupéreront par mail, dans lequel ils chercheront avec une application. Nous ne voulons pas utiliser de base de données, ni même SQLite, parce que nous avons uniquement besoin d'une recherche rapide dans des paires clé-valeur. L'application recherche la ville demandée par l'utilisateur, et remplit le formulaire si une correspondance est trouvée. Plutôt que décrire l'interface utilisateur, nous allons nous concentrer sur les fonctionnalités sous-jacentes.

Nous voulons ouvrir un fichier dbm et garder une référence à ce fichier pendant que l'application de recherche est lancée (pour que les recherches soient aussi rapides que possible), et nous voulons une fonction pour lire ce fichier, avec un prototype tel que :

QStringList lookup(const QString &city)

La structure du fichier que nous utiliserons possède une clé avec le nom d'une ville, sous une forme canonique (sans espace, en minuscules), et une valeur, une chaîne, dont les champs sont séparés par ":". À l'origine, nous allons utiliser la populaire bibliothèque Berkeley DB (ou BDB).

III - Inclusion directe

Nous allons créer une classe, Lookup, qui fournira une couche d'abstraction pour les fonctionnalités de BDB dont nous avons besoin.

class Lookup
{
public:
Lookup(const QString &filename);
~Lookup();

QStringList lookup(const QString &key) const;

private:
Db *db;
};

filename est le nom du fichier dbm, fichier représenté par le type Db. Nous ouvrons le fichier dans le constructeur.

Lookup::Lookup(const QString &filename)
{
db = new Db(NULL, 0);
try
{
db->open(NULL, filename.toLocal8Bit().constData(),
NULL, DB_BTREE, 0, 0);
}
catch(DbException &e)
{
if (db)
db->close(0);
db = 0;
}
catch(std::exception &e)
{
if (db)
db->close(0);
db = 0;
}
}

Notez que nous avons utilisé l'interface C++, nous devons donc inclure db_cxx.h dans notre cpp et, dans notre pro, nous devons ajouter la ligne LIBS += -ldb_cxx -ldb.
Nous avons utilisé les exceptions pour récupérer les échecs des appels à open(), comme recommandé par la documentation.

Dans le destructeur, nous fermons juste le fichier.

Lookup::~Lookup()
{
if (db)
db->close(0);
}

Dans la fonction lookup(), nous créons deux objets Dbt. k contient la forme canonique de la clé entrée par l'utilisateur, et v, la valeur. BDB s'occupe de gérer la mémoire pour nous.

QStringList Lookup::lookup(const QString &key) const
{
if (!db)
return QStringList();
QByteArray normkey = key.toLower().replace(" ", "")
.toLatin1();
Dbt k(normkey.data(), normkey.size() + 1);
Dbt v;
if (db->get(NULL, &k, &v, 0) != 0)
return QStringList();

QString value((char *)v.get_data());
return value.split(":");
}

Si la recherche aboutit, nous prenons les données de la chaîne correspondant à la clé, et la séparons en une liste de chaînes, que nous retournons.

Nous pouvons utiliser ces objets en créant une nouvelle instance avec le chemin vers le fichier dbm, et appeler une fonction membre pour rechercher dans les valeurs pour toute clé donnée.

db = new Lookup(qApp->applicationDirPath() + "/sales.db");
QStringList values = db->lookup(keyText);

Utiliser l'inclusion directe est simple à implémenter, mais cela nécessite un couplage fort de notre application à une bibliothèque particulière. Ceci ne facilite pas le changement pour une autre bibliothèque ou implémentation, vu que nous devons changer énormément notre code, puis le recompiler, puis redistribuer l'application en entier.

IV - Chargement à l'exécution

Une manière de dissocier notre application de l'implémentation est d'utiliser des bibliothèques d'emballage. Ceci signifie que notre application communiquera avec l'emballage, nous pouvons alors changer l'implémentation comme nous le souhaitons.

Une des meilleures manières de comprendre ceci est la mise en pratique. Nous allons commencer par regarder l'implémentation de l'emballage, et ensuite voir comment il est utilisé dans l'application. Le fichier de projet pour l'emballage utilise un autre template, celui par défaut étant app. Vu qu'il n'a pas besoin des fonctionnalités de GUI, il ne lie pas avec QtGui.

TEMPLATE = lib
LIBS    += -ldb_cxx -ldb
QT      -= gui
HEADERS += lookup.h
SOURCES += lookup.cpp

Voici les déclarations dans le fichier d'en-tête.

extern "C"
{
void dbopen(const QString &filename);
void dbclose();
QStringList dblookup(const QString &key);
}

Nous devons utiliser extern "C", vu que QLibrary ne peut résoudre que des symboles de fonctions qui utilisent les conventions C. Dans le fichier de sources, nous gardons une référence au fichier de données, en utilisant une variable statique, que nous incluerons dans l'emballage C. Nous réutilisons le code de Lookup::Lookup(), Lookup::~Lookup() et Lookup::lookup() pour écrire les fonctions dbopen(), dbclose() et dblookup() de notre nouvelle API.

Bien que l'implémentation soit similaire à la précédente, BDB est désormais lié à l'emballage. Nous n'avons plus besoin d'utiliser une ligne LIBS dans notre fichier de projet, mais il est nécessaire de changer l'implémentation afin de satisfaire la nouvelle interface. Au lieu de garder un pointeur vers l'implémentation de dbm, nous allons utiliser un pointeur vers la bibliothèque emballée : QLibrary *dblib.

Pour rechercher des valeurs correspondant à une clé donnée, nous devons d'abord ouvrir la bibliothèque dynamique.

QString path = qApp->applicationDirPath();
dblib = new QLibrary(path + "/mydblib/mydblib", this);
typedef void (*Dbopen)(const QString &);
Dbopen dbopen = (Dbopen)dblib->resolve("dbopen");
if (dbopen)
dbopen(path + "/sales.db");

La requête actuelle est effectuée en obtenant la fonction dblookup() d'un objet valide dblib, avant de l'appeler avec la clé de recherche, comme dans le cas précédent.

typedef QStringList (*Dblookup)(const QString &);
Dblookup dblookup = (Dblookup)dblib->resolve("dblookup");
if (dblookup)
QStringList values = dblookup(keyText);

Quand nous avons fini avec la bibliothèque, nous devons la fermer.

if (dblib)
{
typedef void (*Dbclose)();
Dbclose dbclose = (Dbclose)dblib->resolve("dbclose");
if (dbclose)
dbclose();
}

Cette approche requiert plus de code, et plus d'attention que pour une inclusion directe, mais elle a l'avantage de séparer l'application de l'implémentation de la bibliothèque dbm. Nous pouvons facilement remplacer BDB par, par exemple, GDBM, et l'application elle-même ne nécessitera aucun changement. Mais que faire si nous voulons changer de format tout en autorisant les vendeurs à accéder à leurs vieux fichiers ?

V - Plugins pour applications

Idéalement, nous aimerions que l'application charge le fichier dbm dans n'importe quel format, en utilisant n'importe quelle bibliothèque dbm nécessaire, tout en conservant le découplage entre l'application et l'implémentation dbm. Pour accomplir cet objectif, nous utilisons un plug-in pour applications de Qt 4, que nous définissons dans le fichier interfaces.h.

class DbmInterface
{
public:
virtual ~DbmInterface() {}
virtual int open(const QString &filename) = 0;
virtual void close(int id) = 0;
virtual QStringList lookup(int id, const QString &key) = 0;
};

Q_DECLARE_INTERFACE(DbmInterface, "com.trolltech.dbm.DbmInterface/1.0")

Nous devons inclure ce fichier dans toute application qui nécessite l'utilisation de notre bibliothèque DbmInterface. Nous appellerons notre bibliothèque dbmplugin, et la construirons dans un sous-répertoire du même nom dans le répertoire de notre application. Voici le fichier de projet.

TEMPLATE     = lib
CONFIG      += plugin
LIBS        +=  -ldb_cxx -ldb -lgdbm
INCLUDEPATH += ..
DESTDIR      = ../plugins
HEADERS      = dbmplugin.h
SOURCES      = dbmplugin.cpp

La directive CONFIG s'assure que la bibliothèque est compilée pour être un plug-in, nous étendons INCLUDEPATH avec l'emplacement de interfaces.h, et nous incluons les bibliothèques BDB et GDBM, car nous créons un plug-in combiné.

Nous voulons pouvoir utiliser autant de fichiers dbm que nous voulons, chacun d'un type potentiellement différent. Pour supporter ceci, nous utiliserons un ID entier comme référence à chaque fichier ouvert, avec son type, nous aurons donc besoin d'une petite classe pour nous aider.

class Info
{
public:
Info(void *_db=0, const QString &_dbmName=QString())
: db(_db), dbmName(_dbmName) {}

void *db;
QString dbmName;
};

Nous stockons le lien vers le fichier en tant que pointeur void, car chaque bibliothèque possède son propre type. Nous devons fournir des valeurs par défaut parce que nous stockerons les objets Info dans une map QMap, qui doit pouvoir construire des objets sans arguments.

class DbmInterfacePlugin : public QObject,
public DbmInterface
{
Q_OBJECT
Q_INTERFACES(DbmInterface)

public:
~DbmInterfacePlugin();

int open(const QString &filename);
void close(int id);
QStringList lookup(int id, const QString &key);

private:
QMap<int, Info> dbptrs;
};

Notre classe plug-in hérite, et de QMap, et de DbmInterface. QMap ne fait correspondre qu'un entier, unique, à un fichier dbm. Ceci est effectué par cette ligne.

static int nextId = 0; // 0 représente un indentifiant invalide

Le destructeur s'assure que tous les fichiers sont correctement fermés.

DbmInterfacePlugin::~DbmInterfacePlugin()
{
QMapIterator<int, Info> i(dbptrs);
while (i.hasNext())
{
i.next();
close(i.key());
}
}

Les fonctions open(), close(), et lookup() sont similaires à celles déjà implémentées, nous allons juste nous concentrer sur les différences, pour BDB, et montrer le code GDBM en entier.

int DbmInterfacePlugin::open(const QString &filename)
{
QByteArray fname = filename.toLatin1();

Db *bdb = new Db(NULL, 0);
// Comme Lookup::Lookup(), montré précédemment
if (bdb)
{
int id = ++nextId;
dbptrs.insert(id, Info((void*)bdb, "bdb"));
return id;
}
GDBM_FILE gdb = gdbm_open(fname.data(), 0, GDBM_READER, O_RDONLY, 0);
if (gdb)
{
int id = ++nextId;
dbptrs.insert(id, Info((void*)gdb, "gdb"));
return id;
}
return 0;
}

Nous essayons d'ouvrir le fichier en utilisant la bibliothèque demandée. Si l'opération se déroule sans problème, nous gardons le nom du type de la bibliothèque et un pointeur vers son fichier ouvert dans la map dbptrs.

void DbmInterfacePlugin::close(int id)
{
if (!dbptrs.contains(id))
return;

Info info = dbptrs.value(id);
if (info.dbmName == "bdb")
{
Db *db = (Db*)info.db;
if (db)
db->close(0);
}
else if (info.dbmName == "gdb")
{
GDBM_FILE db = (GDBM_FILE)info.db;
if (db)
gdbm_close(db);
}
dbptrs.remove(id);
}

Dans close(), nous déterminons quel type de fichier est utilisé, et appelons la fonction de fermeture appropriée sur le fichier. La fonction lookup() requiert un id pour la map dbptrs, ainsi qu'une clé.

QStringList DbmInterfacePlugin::lookup(int id,
const QString &key)
{
if (!dbptrs.contains(id)) return QStringList();
Info info = dbptrs.value(id);
QByteArray normkey = key.toLower().replace(" ", "")
.toLatin1();
if (info.dbmName == "bdb")
{
Db *db = (Db*)info.db;
// Comme Lookup::lookup(), montré plus haut
}
else if (info.dbmName == "gdb")
{
GDBM_FILE db = (GDBM_FILE)info.db;
if (!db)
return QStringList();
datum k;
k.dptr = normkey.data();
k.dsize = normkey.size() + 1;
datum v = gdbm_fetch(db, k);
if (!v.dptr)
return QStringList();
QString value(v.dptr);
free(v.dptr);
return value.split(":");
}
return QStringList();
}

Pour créer un plug-in, nous devons exporter son interface. Ceci est effectué en ajoutant cette ligne dans le fichier .cpp du plug-in.

Q_EXPORT_PLUGIN(DbmPlugin, DbmInterfacePlugin)

Une approche légèrement différente est nécessaire pour charger nos plug-ins. Nous voulons n'utiliser que la bibliothèque qui fonctionne avec le fichier qui nous est fourni, nous devons donc itérer sur tout les plug-ins du dossier plugins jusqu'à en trouver un fonctionnel.

QString filename = qApp->applicationDirPath()+"/sales.db";
int dbmId = 0;
QDir path(qApp->applicationDirPath()+"/plugins");

foreach (QString pname, path.entryList(QDir::Files))
{
QPluginLoader loader(path.absoluteFilePath(pname));
QObject *plugin = loader.instance();
if (plugin)
{
dbm = qobject_cast<DbmInterface*>(plugin);
if (dbm)
dbmId = dbm->open(filename);
if (dbmId)
break;
}
}

Si nous arrivons à ouvrir un plug-in avec une interface convenable (un ID non nul est retourné), nous stockons l'ID dans dmbId, et arrêtons la recherche. Nous utilisons dbmId pour rechercher des valeurs avec la fonction lookup() de recherche de l'interface.

QStringList values = dbm->lookup(dbmId, keyText);

Nous devrions aussi fermer l'interface quand notre application se ferme.

dbm->close(dbmId);

Nous pouvons étendre les formats supportés, soit en étendant notre plug-in, soit en ajoutant un autre plug-in, avec d'autres interfaces. De chaque manière, l'application est détachée de l'implémentation et peut lire tous les formats supportés.

VI - Conclusion

Il est rapide et facile d'inclure des bibliothèques non Qt dans des applications Qt 3 ou 4, soit directement, soit en utilisant QLibrary. Qt 4 est construit sur la technologie de plug-ins de Qt 3, ce qui permet aux développeurs d'étendre leurs applications avec des plug-ins personnalisés.

Dans le cas de notre application, nous pouvons sans problème ajouter un support pour d'autres bibliothèques, comme SDBM ou NDBM, sans changer le code source de notre application.

VII - Divers

Les fichiers source

Au nom de toute l'équipe Qt, j'aimerais adresser le plus grand remerciement à Nokia pour nous avoir autorisé la traduction de cet article !