C++ PlugIn Architecture in Ubuntu 18.04

Posted in software by Christopher R. Wirz on Mon Oct 15 2018



An . so file is a compiled library file. It stands for "Shared Object" and is analogous to a Windows DLL. Archive libraries (.a) are statically linked (compile-time) and use the -c option in gcc. A change to a .a library means you need to compile and build your code again. The advantage of .so (shared object) over .a library is that they are linked during the runtime. If there's any change in .so file, you don't need to recompile your main program.

This makes for an ideal way to build a PlugIn architecture. Using a PlugIn architecture, several libraries may fill a certain role in the application (such as filtering an image), but each do it in a different way. The application does not need these capabilities to run, but they extend the capabilities of the application.

Note: Using a PlugIn approach, you don't need to make sure that your main program is linked to the new .so file with ln command.

For this example we begin with the common PlugIn API.


// PlugInApi.h
#ifndef PlugIn_API_H_Include
#define PlugIn_API_H_Include

#include <stddef.h> // includes NULL, wchar_t, and size_t

#ifdef WIN32 // declared when compiling with windows (if not, declare it)
    #ifdef API_EXPORT // must be declared in Preprocessor Definitions
        #define API __declspec(dllexport)
    #else
        #define API __declspec(dllimport)
    #endif
#else
    #define API
#endif

#ifdef __cplusplus
extern "C" {
#endif

	/**
	 * Gets the name of the PlugIn
	 * @return a character array
	 */
    API const char* get_PlugInName();

	/**
	 * Initializes an object
	 * @return a pointer to the object
	 */
    API void* Object_ctor();

	/**
	 * Disposes of the object
	 * @param  a pointer to the object
	 * @return void
	 */
    API void Object_Dispose(void* ptr);

#ifdef __cplusplus
}
#endif
#endif


Given this API, we can start to define PlugIn functionality.


// SamplePlugIn.cpp
#include  "PlugInApi.h"

const char* get_PlugInName() {
    return "Sample1";
}

class MyObject {
	public:
		int Size = 0;
		bool UsingGPU = false;
};

void* Object_ctor() {
    return new MyObject();
}

void Object_Dispose(void* ptr) {
    if (ptr == NULL) { return; }
    delete (MyObject*)ptr;
    ptr = NULL;
}

And another PlugIn is defined...


// SamplePlugIn2.cpp
#include  "PlugInApi.h"
#include <string>

const char* get_PlugInName() {
    return "Sample2";
}

struct MyObject {
    int Size = 0;
};

void* Object_ctor() {
    return malloc(sizeof(MyObject));
}

void Object_Dispose(void* ptr) {
    if (ptr == NULL) { return; }
    free((MyObject*)ptr);
    ptr = NULL;
}

Now we can define a main method to show how the PlugIns are loaded at runtime.


// main.cpp
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <dirent.h>

std::string extension(char* f)
{
	std::string file_name(f);
	int position = file_name.find_last_of(".");
	return file_name.substr(position+1);
}

int main(int argc, char** argv)
{
	printf("\nStarting demo...\n\n\n");
	void* handle;
	const char* (*get_PlugInName)();
	char* error;

	DIR *dir;
	struct dirent *ent;

	if ((dir = opendir ("./")) != NULL) {
		while ((ent = readdir (dir)) != NULL) {
			// Check the extension
			auto ext = extension(ent->d_name);
			if (ext.compare("so") != 0){
				continue;
			}

			// Make the file name
			std::string fname(ent->d_name);
			fname = "./" + fname;

			// Open the file
			handle = dlopen(fname.c_str(), RTLD_LAZY);
			if (!handle || ((error = dlerror()) != NULL)) {
				fprintf(stderr, "%s\n", error);
				continue;
			}

			dlerror();    /* Clear any existing error */

			*(void**)(&get_PlugInName) = dlsym(handle, "get_PlugInName");

			if ((error = dlerror()) != NULL) {
				// fprintf(stderr, "%s\n", error);
				continue;
			}

			printf("PlugIn Name:      %s\n", (*get_PlugInName)());
			dlclose(handle);
		}
		closedir (dir);
	}

	printf("\n\nPress any key to exit...\n\n");
	getchar();
	exit(EXIT_SUCCESS);
}

Now it's time to compile (note the -shared for the *.so files).

g++ -shared PlugInApi.h SamplePlugIn.cpp -o SamplePlugIn.so
g++ -shared PlugInApi.h SamplePlugIn2.cpp -o SamplePlugIn2.so
g++ -rdynamic -lstdc++ main.cpp -ldl -o main

And run...

./main

So let's see the results:

PlugIn Name:      Sample1
PlugIn Name:      Sample2

Both PlugIns have been detected and successfully called.