lørdag 20. januar 2018

C++ plug-ins in Linux with dlopen()/dlsym()

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_mangling
https://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