Plugins: Dynamically Loadable Classes
Both the philosophy of OOP and the virtual function mechanism of C++ make dynamic loadable classes or plugins easy. In fact, plugins are almost the ultimate proof of the power of the object oriented view of software.
The essence of polymorphism is swappable classes. One class can be used in place of another. This lets you change the details of a program, without changing the grand structure. This makes it easy to separate the problem domain, from the problem of the moment.
The problem domain changes very little. The problem of the moment can change quite often. Customer wants a new feature. Boss wants something done. If you have a box of tools that model the domain you are dealing with, then making these changes is like building something from Lego(tm) blocks. You just plug stuff together. If you don't have classes and functions appropriate to the domain you are dealing with, you are programming the same way cars were made before Henry Ford, before the assembly line, each car tediously built from scratch by master craftsmen.
For example, paying bills (the problem domain). Since Babylonia, the basic action of giving money to someone else hasn't changed in essence, even though the objects involved have changed. From barter, to gold nuggets, to horses, to daughters, to coins, to bank drafts, to credit cards to Paypal(tm). Money has represented the same thing, even though its form has changed. Your Pay() function only needs the general concept of money to function. It doesn't need to know or care about the particulars.
Plugins are classes that can be constructed at run-time instead of compiled into your program. Swappable classes can therefore be changed, without having to recompile your program. This is useful for reasons I will outline later.
Plugins are normally a real pain because each function you want to use must be "located" by symbol name before it is used. The old way to do it uses the C style functions GetProcAddress() for windows and dlsym() for Linux. You have to locate the function, get back a void* pointer and cast this pointer to the function involved, something like this:
typedef void (*TypeOfTheFunction)(int, const char*, long); // First you have to create a pointer
TheFunction* function = (TypeOfTheFunction) GetProcAddress("TheFunctionName", TheSharedLibrary);
if (function == NULL)
error;
Execute the function: TheFunction(1, “test value”, 2000);
This is tedious, error prone and inflexible. You have to write a locator function for each loaded function. You have to save that pointer for use later. All the code that calls that function must be aware it is a pointer and not a function. It's error prone because if you change a function in the library, but don't change the cast, your program will blow up. It's inflexible because you have to keep the library and your code constantly synchronized. The casts are hard coded and have to be recompiled if you change the library. Since C++ functions are garbled (decorated) you can’t even know the real function name anyway without a lot of paperwork.
However, with C++ you can do a neat little trick.
1) Define a base class, ideally, an abstract base class but not necessarily, in your program. All the functions you intend to be dynamic must be virtual functions.
2) Derive your plugin class from this base class.
3) Compile your plugin class into its own shared library (.dll for window, .so for linux) . See the example make file below on how to do this for Linux.
4) Export one function from your shared library that does a C++ new of your class object. That might be called "Create()" or something like that. This must be declared as extern “C”. The reasons are technical but the simple explanation is that the function name, unless declared extern “C” will be altered during compilation and you won’t know what the name is to look up when you use GetProcAddress() or dlsym().
5) Your class instance is created and returned to you but in the form of a base class pointer. Because the compiler knows about the base class functions, and your class has virtual versions of these, the program can use your class without having to have linked at compile time with your class. Even though the program thinks it is talking to the base class, the magic of virtual functions causes the functions in your plugin to be called. This means you don't have to manually locate all the function names and signatures in your loadable library and makes plugins via loadable libraries very easy to create.
Why would you want to do this?
1) You can change the classes your program uses by changing only a configuration file or command line option. No recompilation necessary and still type safe.
2) Users of your program can create their own plugins. Just provide them a skeleton of the abstract base class being used in your program and the header file of that abstract base class. When users create and contribute plugins it expands the glory of your original program for free.
3) You can load only those classes actually being used instead of an entire pantheon of code that only might be used.
4) This enables you to rapidly change your program's configuration so you can do comparative testing rapidly to see which plugins are better than others without recompiling. You can change major classes in your program without having to recompile.
5) Your program no longer needs massive functions full of "if" statements for each class of thing you are dealing with. For example, if you are converting text from a source format, to a destination format, the old style way of doing it would be to have a huge case statement where some output format is checked for and the proper output producer is called. This is inflexible. To add another format you have to change the “if” statements. Using plugins instead the user can specify an output plugin. Your program becomes infinitely configurable (without recompiling) in respect to output format.
6) Since your classes are not compiled in, there is no need to have a separate header and implementation file. The entire class declaration goes into the implementation file.
7) Once the plugin is created, any number of member functions is available without having to be explicitly looked up.
8) Plugin architecture prevents programmers from corrupting the core of your application. It forces them to do whatever needs to be done within the plugin, without altering the application and risking side effects caused by changing core features of your program. It enforces thinking in terms of encapsulation and polymorphism. That mental jump has not been made by too many programmers that I have met.
The reason this works without having to look up each function separately is the fact that the virtual function tables for the base and derived classes are in the same order. Virtual function number 1 is the same named function for both classes.
Here is the template class does most of the heavy lifting. The sample code works both on Linux and Windows.
---------- cut here -------------
#ifndef PLUG_IN
#define PLUG_IN
#include "string"
#ifdef _WIN32
#include "windows.h"
#else
#include "dlfcn.h"
#endif
// Anonymous namespace is here to keep linker from making this function (ErrorString) visible
// to the rest of your program. This is to prevent collision with some other library using the
// name ErrorString()
namespace
{
std::string ErrorString()
{
#ifdef _WIN32
return ::strerror(::GetLastError());
#else
return ::dlerror();
#endif
}
}
//ABSTRACT_BASE is the class that all the plugin we be being.
template class Plugin
{
std::string shared_library_filename_;
#ifdef _WIN32
HMODULE shared_library_handle_;
#else
void* shared_library_handle_;
#endif
typedef ABSTRACT_BASE* (*CreateFunction)(void);
CreateFunction create_function_pointer_;
void load()
{
#ifdef _WIN32
shared_library_handle_ = :: LoadLibrary(std::string(shared_library_filename_ + ".dll").c_str());
#else
shared_library_handle_ = ::dlopen(std::string(shared_library_filename_ + ".so").c_str(), RTLD_LAZY|RTLD_GLOBAL);
#endif
if (shared_library_handle_ == NULL)
throw std::string(std::string("Failed to load dynamic library ") + shared_library_filename_.c_str() +
"\nReason: " + ErrorString());
}
CreateFunction getSymbolAddress(const std::string &symbol)
{
if (shared_library_handle_ == NULL)
load();
#ifdef _WIN32
void* rval = ::GetProcAddress(shared_library_handle_, symbol.c_str());
#else
void* rval = ::dlsym(shared_library_handle_, symbol.c_str());
#endif
if (!rval)
throw std::string(std::string("Failed to find symbol ") + symbol + " in dynamic library "
+ shared_library_filename_ + "\nReason: " + ErrorString());
return (CreateFunction) rval;
}
public:
Plugin(const std::string &filename)
: shared_library_filename_(filename), shared_library_handle_(NULL), create_function_pointer_(NULL)
{
}
ABSTRACT_BASE* Create()
{
if (!create_function_pointer_)
create_function_pointer_ = getSymbolAddress("Create");
return create_function_pointer_();
}
~Plugin()
{
if (shared_library_handle_){
#ifdef _WIN32
::FreeLibrary(shared_library_handle_);
#else
::dlclose(shared_library_handle_);
#endif
}
create_function_pointer_ = NULL;
shared_library_handle_ = NULL;
}
};
#endif // endif PLUG_IN
---------- cut here -------------
#Linux Makefile
PLUGIN = Plugin1.so Plugin2.so
PROGS = PluginTest
INCLUDE = -I.
CPPFLAGS = -g $(INCLUDE)
LINKFLAGS = -L/lib -L/usr/lib -Wl,-rpath,.
CXX = g++
LIBS = -lreadline -lpcre
VERSION = 1
all: $(PROGS) $(PLUGIN)
$(PROGS): % : %.o
$(CXX) $(LINKFLAGS) $^ -o $@ $(LIBS)
%.o: %.cc Plugin.h
$(CXX) $(CPPFLAGS) -fPIC -o $@ -c $<
%.so: %.o
$(CXX) -shared -Wl,-soname,$@.$(VERSION) $< -o $@.$(VERSION).0 $(LIBS)
ln -f -s $@.$(VERSION).0 $@
clean:
-rm *.so
-rm *.o
-rm $(PROGS)
-rm $(PLUGIN)
---------- cut here -------------
// Abstract base class. For simplicity's sake, only has one function.
#ifndef PLUGIN_ABSTRACT
#define PLUGIN_ABSTRACT
class PluginAbstract {
public:
virtual const char *Value() const = 0;
};
#endif // endif PLUGIN_ABSTRACT
---------- cut here -------------
#include "pluginabstract.h"
class Plugin1: public PluginAbstract {
public:
virtual const char *Value() const {
return "Plugin1";
}
};
namespace {
extern "C"
{
#ifdef _WIN32
__declspec(dllexport)
#endif
Plugin1 *Create() { return new Plugin1; }
}
}
---------- cut here -------------
#include "pluginabstract.h"
class Plugin2: public PluginAbstract {
public:2
virtual const char *Value() const {
return "Plugin2";
}
};
namespace {
extern "C"
{
#ifdef _WIN32
__declspec(dllexport)
#endif
Plugin2 *Create() { return new Plugin2; }
}
}
---------- cut here -------------
#include "plugin.h"
#include "pluginabstract.h"
#include "iostream"
int main(int argc, char *argv[])
{
try {
Plugin plugin_factory(argv[1]);
PluginAbstract *plugin = plugin_factory.Create();
std::cout << Value()
}
catch(const std::string &e) {
std::cerr << e << std::endl;
return -1;
}
return 0;
}