C++ plug-ins in Linux with dlopen()/dlsym()
One possible way to implement plug-ins in the Linux environment is to define an "interface" which in this case is an abstract base class with only pure virtual methods (see references below for more information). Then have an implementation of this interface inside a dynamic shared object file.But dlsym() is actually only able to load C functions and is not able to load C++ classes directly. So we create a "factory"-function that is responsible for creating one plugin-object and return it. This function is called PluginFactory()and return a pointer to the newly created object. You can naturally call this function anything you like, as long as you use it consistently when creating plug-ins and using plug-ins from somewhere else.
But since the C++ compiler is performing name mangling (see references below for more information) in an unstandardized way, we need to mark PluginFactory() with extern "C" to tell the C++ compiler we want to export its name the way C compiler does it. This so we know what the name of the function will be when using dlsym() to reach the function in the dynamic shared object file, also containing the plug-in implementation.
Interface definition
In this example we are only making two methods. You are naturally free to make as many methods are you like. The important thing is that this file is identical when creating plug.ins and creating the program that is using the plug-ins. If this file is altered, all plug-ins need to be updated and re-compiled in order to conform to this "defined standard".Below is the definition of the plug-in interface given:
#ifndef IPLUGIN_HPP_
#define IPLUGIN_HPP_
#include <string>
class IPlugin {
public:
virtual std::string GetName()=0;
virtual void DoSomething()=0;
virtual ~IPlugin()
{
}
};
#endif /* IPLUGIN_HPP_ */
Save this as IPlugin.hpp
This file is necessary both when creating and using plug-ins. class IPlugin is not going to have any implementation file. However, all plug-ins will be implementations that inherit from IPlugin
The first plugin
All of the class methods are implemented inside the class definition since it is only used inside this compilation unit. Then we only need one file for the plug-in.#include <iostream>
#include "IPlugin.hpp"
class FirstPlugin: public IPlugin {
public:
virtual std::string GetName()
{
return "First plug-in";
}
;
virtual void DoSomething()
{
std::cout << "Hello World from first plug-in." << std::endl;
}
;
};
extern "C" IPlugin* PluginFactory()
{
return new FirstPlugin();
}
Save this as firstplugin.cpp
The second plugin
Is quite similar to the first plugin. In a more real world setting, they could be completely different, only having the same methods.#include <iostream>
#include "IPlugin.hpp"
class SecondPlugin: public IPlugin {
public:
virtual std::string GetName()
{
return "Second plug-in";
}
virtual void DoSomething()
{
std::cout << "Hello World from second plug-in." << std::endl;
}
};
extern "C" IPlugin* PluginFactory()
{
return new SecondPlugin();
}
Save this as secondplugin.cpp
The plugin user
Here we are loading the PluginFactory() function from both plug-ins. In a more realistic example, this program would look for plug-ins in a specified directory or used a file to find plug-ins and stored all found plug-ins in a list or another convenient data structure to easily access them.Here, however for simplicity it is just hard-coded to load the two plug-ins we've made with one function pointer each to create new objects.
#include <iostream>
#include <dlfcn.h>
#include "IPlugin.hpp"
typedef IPlugin* (*PluginFactory_t)(); // typedef to function pointer to the factory function which is included in all plugin implementations
// This function is returning a function pointer to a plug-in factory.
PluginFactory_t LoadPluginFactory(const std::string& fileName)
{
// open a shared libray
void* library = dlopen(fileName.c_str(), RTLD_NOW|RTLD_NODELETE); // RTLD_NODELETE is to avoid deleting this plugin when closing library.
if (!library) {
std::cerr << "Error loading " << fileName << ": " << dlerror() << std::endl;
return nullptr;
}
// load the factory function from the library
PluginFactory_t CreatePlugin = (PluginFactory_t) dlsym(library, "PluginFactory");
if (CreatePlugin == nullptr) {
std::cerr << "Error loading " << fileName << ": Cannot find the PluginFactory() function inside file: " << dlerror() << std::endl;
dlclose(library);
return nullptr;
}
// close the library
dlclose(library);
return CreatePlugin;
}
int main()
{
PluginFactory_t firstPluginFactory = LoadPluginFactory("./libfirstplugin.so"); // get a function pointer to the factory of the first plugin
PluginFactory_t secondPluginFactory = LoadPluginFactory("./libsecondplugin.so"); // get a function pointer to the factory of the second plugin
// We should ideally check that firstPluginFactory and secondPluginFactory isn't null as a part of error handling before continuing
// Create one object for each plugin
IPlugin * firstPlugin = firstPluginFactory();
IPlugin * secondPlugin = secondPluginFactory();
// Print the names of them to the user
std::cout << "Found these two plug-ins: '" << firstPlugin->GetName() << "' and '" << secondPlugin->GetName() << "'" << std::endl;
// Let the plugin do whatever they're programmed to do
firstPlugin->DoSomething();
secondPlugin->DoSomething();
// Delete them from memory
delete (firstPlugin);
delete (secondPlugin);
return 0;
}
Save this as pluginuser.cpp
Compilation and running
You should now have four files IPlugin.hpp, firstplugin.cpp, secondplugin.cpp and pluginuser.cpp in the same folder (if not, put them in same folder). Then type the following commands to compile all of them:g++ -std=c++11 -shared -fPIC firstplugin.cpp -o libfirstplugin.so
g++ -std=c++11 -shared -fPIC secondplugin.cpp -o libsecondplugin.so
g++ -std=c++11 pluginuser.cpp -o pluginuser -ldl
This should generate the following files: libfirstplugin.so, libsecondplugin.so and pluginuser, respectively the two plug-ins and one executable. -shared means that it should create shared libraries and -fPIC to create position independent code. For more information about these options see references below.
Then execute following command to load the plug-ins and execute them:
./pluginuser
Which should give the following output:
Found these two plug-ins: 'First plug-in' and 'Second plug-in'
Hello World from first plug-in.
Hello World from second plug-in.
Meaning everything is working as intended. If something is wrong, the program will probably crash with a "segmentation fault". If that happens, check the code, add more error handling and check if the options provided to the compiler is sufficient.
References
https://en.wikipedia.org/wiki/Name_manglinghttps://en.wikibooks.org/wiki/C%2B%2B_Programming/Classes/Abstract_Classes
https://linux.die.net/man/3/dlopen
https://linux.die.net/man/3/dlsym
https://en.wikipedia.org/wiki/Library_(computing)#Shared_libraries
https://en.wikipedia.org/wiki/Position-independent_code
https://en.wikipedia.org/wiki/Segmentation_fault