C++ PlugIn Architecture in Windows 10

Posted in software by Christopher R. Wirz on Wed Oct 10 2018



PlugIn architectures are somewhat easy using a pure .Net framework, but if you want to take advantage of the speed of C/C++, things get a little more... dynamic. Finding and loading assemblies is fortunately built into the Windows API, and this example will explore the simplicity and features with respect to

Note: This approach only works in Windows, you may want to use #ifdef macros for a common codebase.

For this example we begin with the common PlugIn API.


// PlugInApi.h
#ifndef PlugIn_API_H_Include
#define PlugIn_API_H_Include

#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

    struct MyObject {
        unsigned int Length;
        const char* Id;
        void* Data;
    };

    /**
     * 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 MyObject* MyObject_ctor();

    /**
     * Copies the object (very shallow)
     * @param ptr a pointer to the object
     * @param other a pointer another object
     * @return a MyObject
     */
    API void MyObject_CopyTo(MyObject* ptr, MyObject* other);

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

    /**
     * Gets the object's ID
     * @param ptr a pointer to the object
     * @return a character array
     */
    API const char* MyObject_get_Id(MyObject* ptr);

    /**
     * Gets a pointer to the object data
     * @param ptr a pointer to the object
     * @return a byte array
     */
    API void* MyObject_get_Data(MyObject* ptr);

    /**
     * Gets the last error/debug message on the object
     * @param ptr a pointer to the object
     * @return a character array
     */
    API const char* MyObject_get_Message(MyObject* ptr);

#ifdef __cplusplus
}
#endif
#endif

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


// SamplePlugIn.cpp
#include  "PlugInApi.h"
#include <stddef.h> /* provides NULL */
#include <string> /* provides malloc and free */

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

MyObject* MyObject_ctor() {
    auto ptr = new MyObject();
    ptr->Id = "Id_01";
    ptr->Length = 15;
    ptr->Data = malloc(ptr->Length * sizeof(unsigned char));
    unsigned char* data = (unsigned char*)ptr->Data;
    for (unsigned int i = 0; i < ptr->Length; i++) {
        data[i] = 65 + i;
    }
    return ptr;
}

void MyObject_CopyTo(MyObject* ptr, MyObject* other) {
    other->Length = ptr->Length;
    other->Id = ptr->Id;
    other->Data = ptr->Data;
}

void MyObject_Dispose(MyObject* ptr) {
    if (ptr == NULL) { return; }
    if (ptr->Data != NULL) {
        free(ptr->Data);
        ptr->Data = NULL;
        ptr->Length = 0;
        ptr->Id = NULL;
    }
    delete ptr;
    ptr = NULL;
}

const char* MyObject_get_Id(MyObject* ptr) {
    if (ptr == NULL) { return NULL; }
    return ptr->Id;
}

void* MyObject_get_Data(MyObject* ptr) {
    if (ptr == NULL) { return NULL; }
    return ptr->Data;
}

const char* MyObject_get_Message(MyObject* ptr) {
    if (ptr == NULL || ptr->Data == NULL || ptr->Length < 1 || ptr->Id == NULL)
    { return "Object has been disposed"; }
    return "Valid";
}

SamplePlugIn.cpp will be compiled into its own SamplePlugIn.dll.

Another PlugIn (SamplePlugIn2.cpp) is similarly defined, but not shown for clarity. Assume it is similarly defined but with 10 elements in the data array.

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


// main.cpp
#include <iostream>
#include <windows.h>

// The header common to all PlugIns
#include "PlugInApi.h"

// Define the functions a PlugIn should have
typedef const char* (WINAPIV* get_PlugInName_func)();
typedef MyObject* (WINAPIV* MyObject_ctor_func)();
typedef void (WINAPIV* MyObject_Dispose_func)(MyObject*);
typedef void (WINAPIV* MyObject_CopyTo_func)(MyObject*, MyObject*);
typedef const char* (WINAPIV* MyObject_get_Id_func)(MyObject*);
typedef void* (WINAPIV* MyObject_get_Data_func)(MyObject*);

/// <summary>
///     Gets a human-readable copy of the data
/// </summary>
/// <param name="obj">A pointer a MyObject instance</param>
/// <returns>A character array (a string)</returns>
char* GetDataString(MyObject* obj) {
    char* str = (char*)malloc(sizeof(char) * (3 * obj->Length+1));
    char* ret = str;
    char* data = (char*)obj->Data;
    *(str++) = '[';
    for (int i = 0; i < obj->Length - 1; i++) {
        *(str++) = data[i];
        *(str++) = ',';
        *(str++) = ' ';
    }
    *(str++) = data[obj->Length-1];
    *(str++) = ']';
    *(str++) = '\0';
    return ret;
}

/// <summary>
///     Gets the current execution directory
/// </summary>
/// <returns>A std::string giving the path (no slash at the end)</returns>
std::string ExePath() {
    wchar_t wcbuffer[MAX_PATH];
    GetModuleFileName(NULL, wcbuffer, MAX_PATH);
    std::wstring wbuffer(wcbuffer);
    std::string buffer(wbuffer.begin(), wbuffer.end());
    std::string::size_type pos = std::string(buffer).find_last_of("\\/");
    return std::string(buffer).substr(0, pos);
}

/// <summary>
///     The main function is called at program startup after
///     initialization of the non-local objects
/// </summary>
/// <param name="argc">
///     Non-negative value representing the number of arguments
///     passed to the program.
/// </param>
/// <param name="argv">
///     Pointer to the first element of an array of argc + 1 pointers,
///     of which the last one is null and the previous ones, if any,
///     point to null-terminated ('\0') multibyte strings
/// </param>
/// <returns>0 for success</returns>
int main(int argc, char** argv) {

    // The data for each file we find.
    WIN32_FIND_DATA fileData;

    // The number of PlugIns loaded
    int loaded = 0;

    std::string path = ExePath() + "\\*.dll";
    std::wstring stemp = std::wstring(path.begin(), path.end());
    LPCWSTR sw = stemp.c_str();
    HANDLE fileHandle = FindFirstFile(sw, &fileData);

    if (fileHandle == (HANDLE)ERROR_INVALID_HANDLE ||
        fileHandle == (HANDLE)ERROR_FILE_NOT_FOUND) {
        printf("No PlugIns found\n"); \
        printf("\n\nPress any key to exit...\n"); \
        getchar();
        return EXIT_FAILURE;
    }
    path = ExePath() + "\\";

    do {
        try {
            std::wstring wst(fileData.cFileName);
            std::string st(wst.begin(), wst.end());
            std::string fullPath = path + st;
            stemp = std::wstring(fullPath.begin(), fullPath.end());
            LPCWSTR swt = stemp.c_str();
            HINSTANCE temp = LoadLibrary(swt);

            if (!temp || temp == NULL) {
                continue; // could not load
            }

			auto get_PlugInName_process = GetProcAddress(temp, "get_PlugInName");
			auto MyObject_ctor_process = GetProcAddress(temp, "MyObject_ctor");
			auto MyObject_Dispose_process = GetProcAddress(temp, "MyObject_Dispose");
			auto MyObject_CopyTo_process = GetProcAddress(temp, "MyObject_CopyTo");
			auto MyObject_get_Id_process = GetProcAddress(temp, "MyObject_get_Id");
			auto MyObject_get_Data_process = GetProcAddress(temp, "MyObject_get_Data");

			if (get_PlugInName_process == NULL ||
				MyObject_ctor_process == NULL ||
				MyObject_Dispose_process == NULL ||
				MyObject_CopyTo_process == NULL ||
				MyObject_get_Id_process == NULL ||
				MyObject_get_Data_process == NULL) {

				// could not get process
				FreeLibrary(temp);
				continue;
			}
			printf("Loaded %s\n", st.c_str());
			printf("PlugIn Name:      %s\n", ((get_PlugInName_func)get_PlugInName_process)());

			auto ptr = ((MyObject_ctor_func)MyObject_ctor_process)();
			printf("Object Id:        %s\n", ((MyObject_get_Id_func)MyObject_get_Id_process)(ptr));
			printf("   Length:        %d\n", ptr->Length);
			auto data = GetDataString(ptr);
			printf("     Data:        %s\n", data);
			free(data);
			((MyObject_Dispose_func)MyObject_Dispose_process)(ptr);

			FreeLibrary(temp);

			loaded++;
        }
        catch (...) {
            continue;
        }
    } while (FindNextFile(fileHandle, &fileData));


    printf("\n\nPress any key to exit...\n");
    getchar();
    return loaded > 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}

Now it's time to compile (main.exe, SamplePlugIn.dll, SamplePlugIn2.dll) and run. Let's see the results of running main.exe:

Loaded SamplePlugIn.dll
PlugIn Name:      SamplePlugIn_01
Object Id:        Id_01
   Length:        15
     Data:        [A, B, C, D, E, F, G, H, I, J, K, L, M, N, O]

Loaded SamplePlugIn2.dll
PlugIn Name:      SamplePlugIn_02
Object Id:        Id_21
   Length:        10
     Data:        [A, B, C, D, E, F, G, H, I, J]

Press any key to exit...

Both PlugIns have been detected and successfully called.