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▲
Au nom de toute l'équipe Qt, j'aimerais adresser le plus grand remerciement à Nokia pour nous avoir autorisé la traduction de cet article !