Design and Implementation
Though the class_loader package is quite simple in its implementation, the code may be a bit tricky to understand internally. Hence some notes are given here about the design so as to give guidance to future maintainers.
ROS has implemented the concept of plugins from early in its inception via the pluginlib package. Though pluginlib is adequate for loading plugins, it was decided to refactor the code to address some shortcomings as well as make it more maintainable. Here were some of the shortcomings of pluginlib:
Dependency on the ROS build system meaning that applications that want to utilize pluginlib but not use the ROS package management system could not do so.
- There is no true introspection of exported plugins from an arbitrary runtime library. Introspection is provided through the ROS build system and the contents of XML files.
- It was not thread-safe.
- It was using a hacked and undocumented version of the third-party Poco library and included as part of the baseline code.
The resulting solution was breaking pluginlib into two packages: a class loader to implement the loading/unloading of classes (class_loader) and the existing pluginlib which sits on top of it. pluginlib itself does not change API-wise so as not to break packages depending on it.
Dependency on Poco
class_loader depends on the open source Poco library as pluginlib for providing the underlying ability to load and unload classes from runtime libraries. Unlike the legacy pluginlib which utilized a modded version of Poco, class_loader uses the stock version (libpoco-dev). Also, the class_loader does not use Poco's ClassLoader class, but rather just the lower-level Poco::SharedLibrary class so as to provide a more convenient class registration technique than the one provided by Poco::ClassLoader.
Below is a class diagram for class_loader. Note that not all classes are shown, only public interfaces are exposed, and some of the method parameters that are really passed by const & appear to be passed by value for sake of compactness.
Understanding Class Registration
The macro CLASS_LOADER_REGISTER_CLASS is used to register classes to be available to a ClassLoader when a library is loaded into memory. This mechanism is how the ClassLoader is able to perform introspection of the library. The underlying library that is being used to open libraries, Poco, has it's own class loader called Poco::ClassLoader which performs almost the same functionality as class_loader::ClassLoader. The difference is the way registration works.
What Makes a Plugin System Hard to Design?
It is helpful to understand why implementing a plugin system is hard. Here are some reasons:
C++ being native, compiled (not have an dynamic eval() function like Lisp or Python), and statically-typed language (need to know type information of plugin interface at compile time) without any sort of introspectable runtime (e.g. Java JVM, Python VM)...it's a difficult problem.
When you load a library (e.g. via dlopen() on Linux), you cannot inspect the library for symbols it has! You could do an nm at the command-line, parse it, demangle symbols, display the resulting function names and signatures to the user There's no native way to do it on Linux from what we could find. You could hack nm.c, make it into a function...or invoke nm from the command line. Also you can only call a function in C++ code with a compile-time function signature, so calling an arbitrary function without invoking the C++ compiler sounds tough :/
- Even if you know the symbols ahead of time, the symbols are mangled because of C++ and not standard across platforms and compilers.
Automated Registration Through C++ Static Variable Instantiation
In Poco::ClassLoader, the user is forced to register all of their classes within a single source file in one place. This can be a bit inconvenient, so class_loader improves upon this. Instead the user can register with each class and then mix and match classes across various libraries with no worry. The way CLASS_LOADER_REGISTER_MACRO works is that it expands out to a new struct type and a corresponding static global variable of the same type. The constructor of this class invokes:
which creates a new factory of type class_loader::class_loader_private::MetaObject<Base,Derived>. These factories are stored by the plugin system and used to create plugins via their create() method.
What this trick does is that when the library is loaded, the C++ standard dictates that global variables are instantiated first. This invokes the constructor of the static variable and in turn the registration function above. That means each registered class is automatically added to a globally available list.
What's Up With the Global Functions?
The core of class_loader is implemented within a set of global functions in the namespace plugins::plugins_private. Originally the idea was to make things in a class, but because of implementation issues, reverted having to make all the data structures globally available and having a class did not make sense. The cleaner result was just a set of global functions and writing the exposed interface in the class_loader::ClassLoader and class_loader::MultiLibraryClassLoader classes.