Wiki

Libraries and Plugins

by Mark Summerfield
There are three approaches to making 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 on their 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 typed in, and populates the form if a match was found. Rather than describe the user interface, we'll focus on the underlying functionality.

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

QStringList lookup(const QString &city)

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

[Download source code]

Direct Inclusion

We will create a class, Lookup, that will provide a thin wrapper around 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 represents a 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 the header db_cxx.h in our .cpp file and, to our .pro file, 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 simply close 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, and v contains the value. Berkeley DB will manage the memory needed for 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, and split it into a string list which we then return.

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

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

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

Dynamic Library Loading

One way to decouple our application from the underlying dbm implementation is to use wrapper libraries. This means that our application will simply communicate with our wrapper library, and we can 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 it is used in the Lookup application. The .pro file for the wrapper uses 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 resolve symbols to functions that use the C linking conventions. In the .cpp file 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 the dbopen(), dbclose(), and dblookup() functions for our new function-based API.

Although the implementation is basically the same as before, the Berkeley DB libraries are now linked to the wrapper, and we no longer need to use a LIBS line in our .pro file, but we do need to change our implementation to reflect the new interface. Instead of holding a pointer to a dbm implementation, we hold a pointer to a 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 function from a valid dblib library object, before calling it with the search key 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. We could easily replace Berkeley DB with, say GDBM, and the application itself would not require a single change. But what if we want to switch to a different dbm format but still allow the salespeople to be able to access their old files?

Application Plugins

Ideally, we would like the Lookup application to load the dbm file in any format, using any suitable dbm library, while keeping the application decoupled from the dbm implementation. To achieve this, we use a Qt 4 application plugin, which we define with 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 of our DbmInterface library. We'll call our library dbmplugin and build it in a subdirectory of the same name in our application's directory. 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 a Qt plugin, we extend INCLUDEPATH with the location of interfaces.h, and we include both the Berkeley and the GDBM libraries because we are creating a combined plugin.

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

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 its own handle type. We must provide default values because we will store Info objects using QMap, which needs to be 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. To support 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 are all similar to the ones we originally implemented, so here we'll focus on the differences for Berkeley DB, and show the GDBM code in full.

int DbmInterfacePlugin::open(const QString &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 its open 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 dbptrs maps as well as a key.

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;
        // 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 adding the 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're given, so we need to iterate over the plugins in the plugins directory 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-zero ID is returned), we store the ID in dbmId and stop searching. We use dbmId to look up values with the interface's lookup() 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 our plugin, or by adding another plugin with more interfaces. Either way, the application is decoupled from the dbm implementations and can read any of the supported formats.

Conclusion

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

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


Copyright © 2006 Trolltech Trademarks