Over the last few weeks I’ve been working on getting Grantlee finally into a releasable state for a 0.1 release.
Part of that has been to change the Granltee::Engine to not be a singleton anymore. Engine was a singleton because it managed plugin libraries which may be accessed my multiple Template objects at the same time. When the Engine was deleted it in turn deleted all plugins it had loaded. That worked fine when Engine was a singleton, but after this change I was getting multiple deletions when using multiple Engines.
It turns out that QPluginLoader maintains its own cache of plugins and only loads them once. Great! The only problem is deleting the plugin. My first thought was to put the plugin in a QSharedPointer. As that is reference counted, it would delete the plugin when nothing refers to it anymore.
QPluginLoader loader("/path/to/plugin"); QSharedPointer myPlugin = QSharedPointer( loader.instance() );
The problem is that two Engine objects create two shared pointers. The QPluginLoader caching meant that I was creating two shared pointers for one raw pointer. That leads to a problem similar to this:
int* num_p; QSharedPointer p1(nump); QSharedPointer p2(nump); // ref counts of p1 and p2 are both 1. p1.clear(); // ref count drops to 0. p1 deletes num_p p2.clear(); // ref count drops to 0. p2 tries to delete num_p but it's a dangling pointer.
Hmm, how to resolve that so that I delete the plugins, and I get reference counting so that they are not deleted multiple times?
QPluginLoader has its own ref counting and will delete the plugin when unload() has been called for it for each QPluginLoader instance that has a handle on it. unload() is not called automatically in the QPluginLoader destructor so a plugin is never automatically deleted.
So the trick is to treat the QPluginLoader as a smart pointer. Each time you want a plugin, create a new QPluginLoader for it, store that, and when you no longer need the plugin, call unload() on each QPluginLoader. When the reference count drops to 0, the QPluginLoader will delete the plugin.
MyClass::~MyClass() { foreach(QPluginLoader *loader, m_loaders) { loader->unload(); } qDeleteAll(m_loaders); } /** The caller does not have to delete the returned plugin. */ MyPluginInterface* MyClass::loadPlugin( const QString &name ) { QPluginLoader *loader = new QPluginLoader( name, this ); MyPluginInterface *plugin = qobject_cast( loader->instance() ); m_loaders.append(loader); return plugin; } void someMethod() { MyClass object1; object1.loadPlugin("plugin1"); // plugin1 ref count == 1 object1.loadPlugin("plugin2"); // plugin2 ref count == 1 if (true) { MyClass object2; object2.loadPlugin("plugin1"); // plugin1 ref count == 2 object2.loadPlugin("plugin3"); // plugin1 ref count == 1 } // object2 destructor calls unload() for each loader. // plugin2 ref count drops to 1. plugin3 ref count drops to 0. plugin3 deleted. } // object1 destructor calls unload() for each loader. plugin1 and // plugin2 ref counts drop to 0. Both are deleted.
This means that there should be no attempt to use the plugins returned by loadPlugin after the MyClass has been deleted:
MyPluginInterface *plugin; if (true) { MyClass object1; plugin = object1.loadPlugin("plugin1"); } // object1 destructor deletes plugin. plugin->someMethod(); // It's already deleted. Crash.
In the case of Grantlee, the Engine manages the plugins and deletes them in its destructor. Applications never handle plugins directly, but the Engine is responsible for all of them.
As a final and reusable convenience, I wrote a Grantlee::PluginPointer class to load and manage plugins. It wraps a QSharedPointer and has a custom deleter which calls unload() when needed. It can be used like a normal plugin, but it will always be valid and will be deleted when the application exits at the latest. That means I don’t have to worry about the plugin being unloaded and deleted before I want to use it.
Reading all that back I can’t help thinking of the house that Jack built.
// This is the plugin that you want. TagLibraryInterface *myPlugin; // This is the QPluginLoader to load the plugin that you want. QPluginLoader; // This is the shared pointer to manage the QPluginLoader to // load the plugin that you want. QSharedPointer; // This is the convenience pointer wrapping the shared pointer to manage the QPluginLoader to // load the plugin that you want Grantlee::PluginPointer; // This is the interface to provide the convenience pointer // wrapping the shared pointer to manage the QPluginLoader to // load the plugin that you want. Grantlee::Engine;
So it’s still a bit convoluted. I don’t see reports from anyone else to hit this problem, and the
The QPluginLoader docs are not very clear on where the responsibility is to delete a loaded plugin, but running this test in valgrind is enough for me. I may have missed something though?
At any rate, I call this the Mother Goose Plugin Loader Pattern.
Leave a Reply