Interoperability with C#

The term interoperability comes from intercommunication and means communication between codes written in different languages. In this article, I am going to explain how to use Interoperability with C#.

What is Marshaling?

The whole process of communication between the managed and the unmanaged code in .NET is called marshaling. It processes messages between the two environments and is one of the major services provided by the CLR.

Most types in unmanaged environments donโ€™t have their representations in the managed one, so the marshaling process provides a way to convert a data type from one environment to the appropriate data type in another. For instance, the System.UInt32 in .NET representation in the unmanaged memory is DWORD. You can marshal DWORD in .NET as a 32-bit unassigned integer and vice versa. .NET provides a lot of functionality for working with unmanaged data inside the System.Runtime.InteropServices namespace.

Simple Interoperability with C# using DllImport

Letโ€™s see the following example โ€“ we have a simple C++ library, that exports only one method for calculating the sum of two integers. I am not going into detail on how to create the library, but how to use it in a managed environment. The exported method is called CalculateSum:

C++
#pragma once

#ifdef ExternalCPlusPlusLibrary_EXPORTS
#define ExternalCPlusPlusLibrary_API __declspec(dllexport)
#else
#define ExternalCPlusPlusLibrary_API __declspec(dllimport)
#endif

extern "C" ExternalCPlusPlusLibrary_API long CalculateSum(
    const unsigned long long a, const unsigned long long b);

And here it is its definition, where we just return the sum of two integers:

C++
#include "pch.h"
#include "ExternalCPlusPlusLibrary.h"
#include <utility>
#include <limits.h>

long CalculateSum(
    const unsigned long long a,
    const unsigned long long b)
{
    return a + b;
}

This library is compiled to a DLL, but how to use it in a C# program? This can be achieved with the DLLImport attribute:

C#
internal class Program
{
    static void Main(string[] args)
    {
        var sum = CalculateSum(1, 2);
        Console.WriteLine(sum);
        Console.ReadLine();
    }

    [DllImport("C:\\PathToYourCPlusPlusDll\\ExternalCPlusPlusLibrary.dll")]
    private static extern IntPtr CalculateSum(int a, int b);
}

As you can see the DllImport accepts as a parameter the path to the unmanaged library, we want to import. If we run this code, the result will be:

Interoperability with C# - simple example
Interoperability with C# – simple example

The extern method definition

The name of the method in the example above CalculateSum must match the defined function method, which is exported in the DLL. Otherwise, you will get an exception that this method is not found as an exported member in the provided DLL:

Interoperability with C# - wrong import path
Interoperability with C# – wrong import path

The extern keyword in the method definition is telling the compiler that this is a code, implemented externally, mostly not in C#. The external method implementation is outside of the C# program, so the method doesnโ€™t have a body. The DllImport is working with a combination of the extern and the static keywords. The static also makes sense since this external method is something common, and not depends on any object instances.

DllImport – what to watch out for?

One important thing to notice when working with DllImport is that your external library remains locked by the program process and you are not able to delete it:

Interoperability with C# - locked DLL
Interoperability with C# – locked DLL

Once the program ends the OS will release the process, and it won’t be locked anymore. But, take this note:

Not releasing the process in large applications can lead to some strange and hard-to-track issues. So, the developer needs to take care of the external library release after it does its job.

Here is an example:

Let’s assume you are building a large enterprise web application with a well-established release cycle. In this application, you have a lot of interrogability operations. When you release a new version, and your customers get this version and try to upgrade their product, they will probably have to change the DLLs, but if these DLLs are accidentally locked by any process the update will fail. This is just one example of why you must always release your external DLLs, to protect your application from such unplanned behaviors.

Interoperability with C# – the right way. Step-by-step guide.

Our example until now is very simple, and we canโ€™t easily release the loaded library at runtime. So, let’s rewrite the code.

Step 1: Introduce disposable class for working with unmanaged code

Since we want to release our DLL, after it has done its job, we can create a disposable class. It will act as a connector between the managed and the unmanaged environment. Letโ€™s name it InteroperabilityConnector.cs, for instance:

C#
internal class InteroperabilityConnector : IDisposable
{
    private IntPtr libHandle;

    private const string LibPath = "C:\\PathToYourCPlusPlusDll\\ExternalCPlusPlusLibrary.dll";

    public InteroperabilityConnector()
    {
        this.libHandle = NativeMethods.LoadLibrary(LibPath);
    }

    public void Dispose()
    {
    }

    [DllImport("kernel32.dll")]
    internal extern static IntPtr LoadLibrary(string libraryName);
}

As you can see here we are loading the whole library, not just specific method into the memory. The kernel32 LoadLibrary method will load our C++ library into the address space of our application process. It returns a handle, which can be used to work with that library.

Step 2: Define the dispose logic

With the handle already available, we can easily add the dispose logic:

C#
internal class InteroperabilityConnector : IDisposable
{
    private IntPtr libHandle;

    private const string LibPath = "C:\\PathToYourCPlusPlusDll\\ExternalCPlusPlusLibrary.dll";

    public InteroperabilityConnector()
    {
        this.libHandle = NativeMethods.LoadLibrary(LibPath);
    }

    public void Dispose()
    {
        if (this.libHandle != IntPtr.Zero)
        {
            NativeMethods.FreeLibrary(this.libHandle);
            this.libHandle = IntPtr.Zero;
        }
    }

    [DllImport("kernel32.dll")]
    internal extern static IntPtr LoadLibrary(string libraryName);

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

For the disposal, we are using another kernel32 function โ€“ FreeLibrary. It unloads the library from the address space of the process, the handle is no longer valid, and the DLL is not locked. We are also setting the handle to nowhere, since it is no longer valid, and it will not be reset automatically.

This is also a good example of marshaling the result from the FreeLibrary method to a known C# variable in the managed memory. It is done with the [return: MarshalAs(UnmanagedType.Bool)].

Step 3 – Import the external function

Next step is to call our method CalculateSum. To do that we will use a 3rd kernel32 function called GetProcAddress. It will retrieve the address of our exported CalculateSum function from the C++ library:

C#
internal class InteroperabilityConnector : IDisposable
{
    private IntPtr libHandle;

    private SumDelegate? sumDelegate;

    private delegate long SumDelegate(long n1, long n2);

    private const string LibPath = "C:\\PathToYourCPlusPlusDll\\ExternalCPlusPlusLibrary.dll";

    public InteroperabilityConnector()
    {
        this.libHandle = NativeMethods.LoadLibrary(LibPath);
        this.sumDelegate = (SumDelegate?)GetExternalFunctionDelegate("CalculateSum", typeof(SumDelegate));
    }

    public void Dispose()
    {
        if (this.libHandle != IntPtr.Zero)
        {
            NativeMethods.FreeLibrary(this.libHandle);
            this.libHandle = IntPtr.Zero;
        }
    }

    private Delegate GetExternalFunctionDelegate(string functionName, Type type)
    {
        var pointer = NativeMethods.GetProcAddress(libHandle, functionName);

        if (pointer == IntPtr.Zero)
            throw new ArgumentNullException($"Function pointer for: {functionName} not found!");

        return Marshal.GetDelegateForFunctionPointer(pointer, type);
    }

    [DllImport("kernel32.dll")]
    internal extern static IntPtr GetProcAddress(IntPtr hModule, string procName);

    [DllImport("kernel32.dll")]
    internal extern static IntPtr LoadLibrary(string libraryName);

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

The method GetExternalFunctionDelegate will get the C++ exported function address by calling the kernel32 native method, and then will transform it to a delegate with the static Marshal method GetDelegateForFunctionPointer. We also assign this delegate in the constructor.

Having this delegate, we can easily define a public method in our connecter, to call it:

C#
public long CalculateSum(long n1, long n2)
{
    if (sumDelegate != null)
        return sumDelegate(n1, n2);

    throw new ArgumentNullException($"External method: {MethodBase.GetCurrentMethod().Name} not found!");
}

Step 4 – Define the extern methods in a separate class

The static external methods can be defined in a separate static class like this:

C#
using System.Runtime.InteropServices;

namespace Interopability
{
    internal static class NativeMethods
    {
        [DllImport("kernel32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        internal static extern bool FreeLibrary(IntPtr hModule);

        [DllImport("kernel32.dll")]
        internal extern static IntPtr GetProcAddress(IntPtr hModule, string procName);

        [DllImport("kernel32.dll")]
        internal extern static IntPtr LoadLibrary(string libraryName);
    }
}

Andi finally, our InteropabilityConnector is fully shaped:

C#
internal class InteroperabilityConnector : IDisposable
{
    private IntPtr libHandle;

    private SumDelegate? sumDelegate;

    private delegate long SumDelegate(long n1, long n2);

    private const string LibPath = "C:\\PathToYourCPlusPlusDll\\ExternalCPlusPlusLibrary.dll";

    public InteroperabilityConnector()
    {
        this.libHandle = NativeMethods.LoadLibrary(LibPath);
        this.sumDelegate = (SumDelegate?)GetExternalFunctionDelegate("CalculateSum", typeof(SumDelegate));
    }

    public long CalculateSum(long n1, long n2)
    {
        if (sumDelegate != null)
            return sumDelegate(n1, n2);

        throw new ArgumentNullException($"External method: {MethodBase.GetCurrentMethod().Name} not found!");
    }

    public void Dispose()
    {
        if (this.libHandle != IntPtr.Zero)
        {
            NativeMethods.FreeLibrary(this.libHandle);
            this.libHandle = IntPtr.Zero;
        }
    }

    private Delegate GetExternalFunctionDelegate(string functionName, Type type)
    {
        var pointer = NativeMethods.GetProcAddress(libHandle, functionName);

        if (pointer == IntPtr.Zero)
            throw new ArgumentNullException($"Function pointer for: {functionName} not found!");

        return Marshal.GetDelegateForFunctionPointer(pointer, type);
    }
}

Step 5 – Use the connector

At last but not least, use the connector:

C#
internal class Program
{
    static void Main(string[] args)
    {
        using (var connector = new InteroperabilityConnector())
        {
            var sum = connector.CalculateSum(1, 2);
            Console.WriteLine(sum);
        }

        Console.ReadKey();
    }
}

With this example we are sure, the library will be released. Moreover, it can be easily extended by adding new delegates in the connector, pointing to methods in the external library.

Resources