A C# P/Invoke Alternative that works with a PlugIn Architecture

Posted in software by Christopher R. Wirz on Thu Nov 15 2018



C-style PlugIns offer some great advantages, but aren't really common in applications with managed languages like C#. A DLL is a dynamic link library in Windows that is used for holding procedures that can be shared with other programs - it is crucial to a PlugIn architecture whether is uses managed (C#) or unmanged/native (C/C++) code.

Note: This method actually does still use P/Invoke for the kernel32 API.

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";
}

We compile this C/C++ code into SamplePlugIn.dll (no runtime libraries used). This is the PlugIn DLL.


using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Security;

internal static class UnsafeNativeMethods
{
	[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
	internal static extern IntPtr LoadLibrary(string lpFileName);

	[DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
	internal static extern IntPtr GetProcAddress(IntPtr hModule, string procName);

	[SuppressUnmanagedCodeSecurity]
	[DllImport("kernel32.dll")]
	[return: MarshalAs(UnmanagedType.Bool)]
	internal static extern bool FreeLibrary(IntPtr hModule);

	/// <summary>
	///     Converts a pointer into a byte array
	/// </summary>
	/// <param name="ptr">A pointer to the data</param>
	/// <param name="length">The length of the byte array</param>
	/// <remarks>
	///     ptr is the address of the first element of a byte array
	/// </remarks>
	/// <returns>A byte array</returns>
	public static byte[] PtrToByteArray(IntPtr ptr, int length)
	{
		byte[] array = new byte[length];
		if (length > 0) { System.Runtime.InteropServices.Marshal.Copy(ptr, array, 0, length); }
		return array;
	}

	/// <summary>
	///     Converts a pointer into a byte array
	/// </summary>
	/// <param name="ptr">A pointer to the data</param>
	/// <remarks>
	///     Without specifying the length, this method will attempt to read bytes
	/// </remarks>
	/// <returns>A byte array</returns>
	public static byte[] PtrToByteArray(IntPtr ptr)
	{
		int length = 0;
		if (ptr == IntPtr.Zero) { return new byte[length]; } // NULL
		while (System.Runtime.InteropServices.Marshal.ReadByte(ptr, length) != 0)
		{ length++; }
		return PtrToByteArray(ptr, length);
	}

	/// <summary>
	///     Converts a pointer into a string
	/// </summary>
	/// <param name="ptr">A pointer to the data</param>
	/// <remarks>
	///     ptr is null ('\0') terminated character array
	/// </remarks>
	/// <returns>A string</returns>
	public static string PtrToStringUtf8(IntPtr ptr)
	{
		return System.Text.Encoding.UTF8.GetString(
			PtrToByteArray(ptr)
		);
	}
}

Now for the C# code to call the PlugIns at runtime. The first thing we need is an object that will hold the same data as the API object... Without an object to do the wrapping, we can use the CopyTo method fo the API to easily read the data.


[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct MyObject
{
	public uint Length;

	public IntPtr Id;

	public IntPtr Data;

	public string GetId() => UnsafeNativeMethods.PtrToStringUtf8(Id);
	public byte[] GetBytes() => UnsafeNativeMethods.PtrToByteArray(Data, (int)Length);
}

With that taken care of, we now define the method delegates and the Main method of the Program.



class Program
{
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate IntPtr get_PluginName();
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate IntPtr MyObject_ctor();
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void MyObject_CopyTo(IntPtr ptr, ref MyObject other);
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void MyObject_Dispose(IntPtr ptr);
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate IntPtr MyObject_get_Message(IntPtr ptr);

    static void Main(string[] args)
    {
        var di = new DirectoryInfo(Environment.CurrentDirectory);
        foreach (var f in di.GetFiles("*.dll"))
        {
            var dllHandle = UnsafeNativeMethods.LoadLibrary(f.FullName);
            try
            {
                var plugInPointer = UnsafeNativeMethods.GetProcAddress(dllHandle, "get_PlugInName");
                var ctorPointer = UnsafeNativeMethods.GetProcAddress(dllHandle, "MyObject_ctor");
                var disposePoitner = UnsafeNativeMethods.GetProcAddress(dllHandle, "MyObject_Dispose");
                var copyPointer = UnsafeNativeMethods.GetProcAddress(dllHandle, "MyObject_CopyTo");
                var getMessagePointer = UnsafeNativeMethods.GetProcAddress(dllHandle, "MyObject_get_Message");

                if (plugInPointer != IntPtr.Zero &&
                    ctorPointer != IntPtr.Zero &&
                    disposePoitner != IntPtr.Zero &&
                    copyPointer != IntPtr.Zero &&
                    getMessagePointer != IntPtr.Zero)
                {
                    var pluginFunc = (get_PluginName)(object)Marshal.GetDelegateForFunctionPointer(plugInPointer, typeof(get_PluginName));
                    var ctorFunc = (MyObject_ctor)(object)Marshal.GetDelegateForFunctionPointer(ctorPointer, typeof(MyObject_ctor));
                    var disposeFunc = (MyObject_Dispose)(object)Marshal.GetDelegateForFunctionPointer(disposePoitner, typeof(MyObject_Dispose));
                    var copyFunc = (MyObject_CopyTo)(object)Marshal.GetDelegateForFunctionPointer(copyPointer, typeof(MyObject_CopyTo));
                    var getMessageFunc = (MyObject_get_Message)(object)Marshal.GetDelegateForFunctionPointer(getMessagePointer, typeof(MyObject_get_Message));

                    Console.WriteLine("Loaded " + f.Name);
                    Console.WriteLine("PlugIn Name:      " + UnsafeNativeMethods.PtrToStringUtf8(pluginFunc()));

                    var obj = ctorFunc();
                    var msg = getMessageFunc(obj);

                    Console.WriteLine("       Message:   " + UnsafeNativeMethods.PtrToStringUtf8(msg));

                    var other = new MyObject();
                    copyFunc(obj, ref other);
                    Console.WriteLine("Object Message:   " + other.GetId());

                    Console.Write("         Bytes:   ");
                    var bytes = other.GetBytes();
                    Console.WriteLine(String.Join(", ", bytes));

                    disposeFunc(obj);
                    msg = getMessageFunc(obj);
                    Console.WriteLine("       Message:   " + UnsafeNativeMethods.PtrToStringUtf8(msg));
                }
                Console.WriteLine();
            }
            finally
            {
                UnsafeNativeMethods.FreeLibrary(dllHandle);
            }
        }

        Console.WriteLine("Press any key to exit...");
        Console.ReadKey();
    }
}

And now we run the application and see the results:

Loaded SamplePlugIn.dll
PlugIn Name:      SamplePlugIn_01
       Message:   Valid
Object Message:   Id_01
         Bytes:   65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79

Press any key to exit...

The C-Style PlugIn has successfully been called from C#.