Libraries and Plugins

Материал из Wiki.crossplatform.ru

Перейти к: навигация, поиск
Image:qt-logo_new.png Image:qq-title-article.png
Qt Quarterly | Выпуск 17 | Документация


by Mark Summerfield
There are three approaches tomaking use of non-Qt external libraries: directly linking to them,dynamically loading them when required, and using application plugins.In this article we'll look at all three approaches and comment ontheir pros and cons.

Содержание

Suppose we have sales data that we wish to issue to a global sales team.The data is saved in a dbm file (a file of key – value pairs)which the team members pick up by email and search using the "Lookup"application. We don't want to use a SQL database, not even SQLite,because we only need simple fast key – value lookup.The application searches for the city the user has typedin, and populates the form if a match was found. Rather than describethe user interface, we'll focus on the underlying functionality.

We want to open a dbm file and hold some kind of reference tothis file while the Lookup application is running (to make lookupsas fast as possible), and we want a function with a signature like

QStringList lookup(const QString &city)

to read the dbm file. The dbmfile structure we're using has a key which is the canonicalised name ofa city (all lower-case, no spaces), and a value which is a stringconsisting of colon ':' separated fields. Initially we'll use thepopular Berkeley DB library.

[Download source code]

Direct Inclusion

We will create a class, Lookup, that will provide a thin wrapperaround the Berkeley DB functionality we need:

class Lookup
{
public:
    Lookup(const QString &filename);
    ~Lookup();
 
    QStringList lookup(const QString &key) const;
 
private:
    Db *db;
};

The filename is the name of the dbm file. The Db type representsa Berkeley DB dbm file. We open the file in the constructor:

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;
    }
}

Note that we've used Berkeley DB's C++ interface, so we must include theheader db_cxx.h in our .cpp file and, to our .profile, we must extend the libraries used with LIBS += -ldb_cxx -ldb.We've used exception handlers to catch failure of the open() call as recommended by the Berkeley DB documentation. In the destructor we simplyclose the dbm file:

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

In the lookup() function, we create two Dbt objects:k contains the canonical form of the key the user typed in, andv contains the value. Berkeley DB will manage the memory neededfor these.

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(":");
}

If the lookup succeeds, we take the data string corresponding to the key, andsplit it into a string list which we then return.

We can use the Lookup object by constructing a new instance with thepath to the dbm file, and call a member function to look up values forany given key:

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

Using direct inclusion is simple to implement, but it does tightlycouple our application to a particular dbm library. This makes itinconvenient to switch to another dbm implementation since we would haveto make many changes to the source code then recompile and redistributethe entire application.

Dynamic Library Loading

One way to decouple our application from the underlying dbmimplementation is to use wrapper libraries. This means that ourapplication will simply communicate with our wrapper library, and wecan change the dbm implementation we use at will.

One way of achieving this is to use QLibrary.We'll start by looking at the wrapper's implementation, and then see how itis used in the Lookup application. The .pro file for the wrapperuses a different template (the default is app, "application") and,since it uses no GUI functionality, it does not link to QtGui.

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

Here are the header file's declarations:

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

We must use extern "C" since QLibrary can only resolvesymbols to functions that use the C linking conventions. In the .cppfile we keep a reference to the dbm file using a static variable,static Db *db = 0;, and wrap all the functions in extern"C". We reuse the code from Lookup::Lookup(),Lookup::~Lookup(), and Lookup::lookup() to create thedbopen(), dbclose(), and dblookup() functions for ournew function-based API.

Although the implementation is basically the same as before, the BerkeleyDB libraries are now linked to the wrapper, and we no longer need to use aLIBS line in our .pro file, but we do need to change ourimplementation to reflect the new interface.Instead of holding a pointer to a dbm implementation, we hold a pointer toa wrapper library: QLibrary *dblib.To look up values for a given key, we first need to open the dynamic library(e.g., mydblib):

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");

The actual query is performed by obtaining the dblookup functionfrom a valid dblib library object, before calling it with the searchkey as in the previous case:

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

When we have finished with the library, we have to close it:

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

This approach requires more code and more care than direct inclusion,but it does decouple the application from the underlying dbm library. Wecould easily replace Berkeley DB with, say GDBM, and the applicationitself would not require a single change. But what if we want toswitch to a different dbm format but still allow the salespeople to beable to access their old files?

Application Plugins

Ideally, we would like the Lookup application to load the dbm file inany format, using any suitable dbm library, while keeping theapplication decoupled from the dbm implementation.To achieve this, we use a Qt 4 application plugin, which we definewith the interfaces.h file:

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")

We must include this header in any application that wants to make use ofour DbmInterface library. We'll call our library dbmpluginand build it in a subdirectory of the same name in our application'sdirectory. Here is its .pro file:

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

The CONFIG line ensures that the library is built for use as aQt plugin, we extend INCLUDEPATH with the location ofinterfaces.h, and we include both the Berkeley and the GDBMlibraries because we are creating a combined plugin.

We want to be able to open as many dbm files as we like, with each onepotentially a different kind. To support this we will use an integer IDto reference each open file and its dbm type, so we need a tiny helperclass:

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

We store the dbm file handle as a void pointer because each dbm has itsown handle type. We must provide default values because we will storeInfo objects using QMap, which needs tobe able to construct objects without 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;
};

Our plugin class inherits both QObject and DbmInterface. The QMap maps unique integers to dbm file handles and type names. Tosupport this, in the .cpp file we have the following line:

static int nextId = 0; // zero represents an invalid ID

The destructor ensures that all the dbm files are properly closed:

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

The open(), close(), and lookup() functions areall similar to the ones we originally implemented, so here we'll focus on thedifferences for Berkeley DB, and show the GDBM code in full.

int DbmInterfacePlugin::open(const QString &amp;filename)
{
    QByteArray fname = filename.toLatin1();
 
    Db *bdb = new Db(NULL, 0);
    // Same as Lookup::Lookup() shown earlier
    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;
}

We attempt to open the dbm file using the specified dbm library.If successful, we keep the name of the library type and a pointer to itsopen file in the dbptrs map.

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);
}

In close(), we determine which type of dbm file is in use,and call the appropriate close function on its file handle.The lookup() function requires an id for the dbptrsmaps as well as a key.

QStringList DbmInterfacePlugin::lookup(int id,
                                       const QString &amp;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;
        // same as Lookup::lookup() shown earlier
    } 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();
}

To create a plugin we must export its interface. This is done by addingthe following line in the plugin's .cpp file:

Q_EXPORT_PLUGIN(DbmPlugin, DbmInterfacePlugin)

A slightly different approach is needed when loading our application plugins.We only want to use the dbm library that works with the dbm file we'regiven, so we need to iterate over the plugins in the pluginsdirectory until we find one that works:

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;
    }
}

If we manage to open a plugin with a suitable interface (a non-zeroID is returned), we store the ID in dbmId and stop searching.We use dbmId to look up values with the interface'slookup() function:

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

We should also close the interface when our application exits:

dbm->close(dbmId);

We can extend the supported dbm file formats, either by extending ourplugin, or by adding another plugin with more interfaces. Either way,the application is decoupled from the dbm implementations and can readany of the supported formats.

Conclusion

It is straightforward to include non-Qt libraries with Qt 3 and Qt 4applications, either directly or by using QLibrary. Qt 4 builds on Qt 3'splugin technology, allowing developers to extend applications with customplugins.

In the case of our plugin-enabled Lookup application, wecould easily add support for other dbm libraries, such as SDBM or NDBM,without changing the core application's source code.


Copyright © 2006 Trolltech Trademarks