I have developed a core framework with a friend of mine some time ago which provides a plugin system where every plugin provides a set of extensions and interfaces for available extension points. The whole system follows the
Eclipse Plugin System which provides a lot of flexibility for plugin writer.
Because plugins may depend on other plugins to work correctly the resulting plugin structure can become a complex tree (actually a
DAG) at runtime. Because its possible to register and unregister plugins at runtime a lot of effort went into managing the availability of a certain plugin depending on its dependencies. I decided to use shared pointers to manage the lifetime of all kind of resources including the plugin and extension instances themself.
This decision leads to two major design issues I like to share with you together with my solutions:
1. Keeping shared pointer to direct runtime dependencies
If object
A depends (directly) on object
B and
C it seems to be straight forward that
A holds shared pointers of object
B and
C during its lifetime to ensure that these dependencies exist. It doesn´t matter if this pointers are used by
A to access its dependencies or not. In the latter case for example it is even possible to store all dependend shared pointer into an array of their base class type. If they don´t share a common base class wrapping them into
any objects is a safe alternative. Since they are never accessed its only important that they are stored somehow. Now, as long as
A exists, all dependencies exist as well because there is at least one handle left pointing on them. When
A is destroyed all dependency shared pointer will be released which might invoke the actual destruction of the dependency if there is no further handle left.
A nice and clean general solution, he? No, not if one of the dependencies is an instance managing a
dynamic linked library which actually implements
A.
In my case particularly
A is a plugin instance which holds a dependency instance which manages the load/unload status of the plugin DLL. Ofcourse its important that during the lifetime of
A the DLL is loaded since the whole implementation of
A is located their. The problem with this design occurs when it comes to the destruction of
A and iff
A holds the last pointer to its DLL. During the destruction of
A the last pointer of the DLL will be destroyed and this will corrupt the callstack even if this instruction is the (implicitly) last one in the destruction process of
A. As soon as the next call is executed a strange error will occure e.g. telling you that some virtual functions could not be called (on Windows).
What a pity! It could have been so elegant...
Wait, there is a solution:
The trick is to put the destruction of all dependencies after the destruction of
A. Ok thats obvious, but how can we apply that without introducing another manager tracking the destruction of
A and its dependencies from outside? Answer: Using a custom deleter.
I use the Boost::SharedPtr implementation which offers an interface to define custom deleter which then takes care of deleting the dependencies
after the destruction of
A.
The following snippet shows how such a class looks like:
// Type definition for a list of boost::any instances.
typedef std::vector DependencyList;
// Stores all dependencies
template struct DependencyDeleter
{
// Constructor is initialized with the list of dependencies
DependencyDeleter(DependencyList dependencies) : mDependencies(dependencies)
{}
// Is called on last SharedPtr destruction
void operator()(T* ptr)
{
// delete our pointer first
delete ptr;
// delete the dependencies now
while(mDependencies.size())
mDependencies.pop_back();
}
// holds all dependencies
DependencyList mDependencies;
};
The usage on creation of the object looks like this:
// create the dependency list
DependencyList dependencyList;
// (...gather all required dependencies and add them to the list...)
// create the plugin instance
Plugin* plugin = createPlugin();
// create a shared pointer managing the lifetime of the plugin and its dependencies
PluginPtr pluginPtr(plugin, DependencyDeleter(dependencyList));
2. Distributing a shared pointer as "self" pointer
It happens that a shared object is a factory (or manager) which is able to create instances. A problem with the shared pointer concept arises in this case if these instances should also know its creator via a shared pointer e.g. provided on construction as a parameter. Why? Because that implies that the factory knows its own shared pointer to provide it as parameter on construction. Obviously its not clever that the factory creates a new shared pointer of itself (this would mean having several shared pointer which want to manage the same objects lifetime independently, very bad!). The solution is to pass on construction of the shared pointer (see snippet above) the shared pointer as parameter to the object like this:
plugin->_setSelfHandle(pluginPtr);
It would also be possible that the constructor receives an empty SharedPtr reference which is initialised with "self" in the constructor and stored within the object. But that is a matter of taste...
This concept only makes sense if the shared pointer implementation supports a
weak pointer. Its mandatory that the shared pointer stored in the object itself is weak pointer which does not count as handle and therefore does not prevent destruction as soon as the last external pointer has been released. Otherwise the object won´t be destroyed and you have created a nice memory leak.
Eventually the whole concept helps a lot managing resources and its dependencies elegantly after solving the described issues...