How to dynamically load native DLLs from C#

By , last updated August 16, 2019

Loading native DLLs from a managed context (C#, VB.net) is an evil necessity. Native and managed must sometimes communicate, and sometimes the best way is to call native methods directly from a managed context.

The reasons can be many, and sometimes it’s just about pure performance and legacy code written in C and/or C++.

One of the major headaches is architecture. Managed code can be built with “Any CPU”. This means one set of binaries will run as 32-bit on 32-bit platforms, and as 64-bit on 64-bit platforms. Native code (and DLLs) are either 32-bit or 64-bit. It’s not possible to mix architectures.

Common pitfalls

Native code is either 32-bit or 64-bit. With managed binaries being Any CPU, it’s not always straight forward.

It’s a complex topic. There are many pitfalls with P/Invoke. With 32-bit and 64-bit there is an extra layer of complexity. One way is to force the managed binaries to run as 32-bit when shipping with 32-bit native libraries, and force run with 64-bit when shipping with 64-bit native libraries. But then you have two different installers and systems to maintain.

Yes, that’s not too easy and it’s easy to make mistakes. Here is a method on how to ship managed binaries with Any CPU, with both 32-bit and 64-bit native architectures.

Assumptions

Here we assume the native DLLs have identical names, but they will be placed in different directories based on the architecture. 32-bit DLLs goes into the “Win32”-folder, and 64-bit DLLs go into the “x64”-folder.

Unfortunately, relying on native DLLs to do a job comes with a cost. Maybe it’s more beneficial to send off a computationally expensive job to a native component, but the cost is more complex code to be able to send it. Therefore, each library requires it’s own wrapper, where each method needs an equivalent in C#.

The C++ DLL

A Win32 DLL can be very complicated to get right, but for the sake of this post, I’m creating a very simple DLL with 1 exported function, GetVersion.

The signature is uint32_t GetVersion(char * buffer, uint32_t length). I’m not going into implementation details, but it returns a version string based on the architecture it was built with. The version is either of:

  • Lib1 – 32-bit
  • Lib1 – 64-bit

This way we’ll know for sure what architecture the native DLL is using.

With Dependency Walker it is possible to verify that the DLL actually exports this method.

dependency-walker

The Interop Interface

Managed code doesn’t have a managed way of dynamically loading DLLs. The closest way is to hardcode the DLL name and what methods it should import or use. This doesn’t work very well with mixing architectures. However, there is a small backdoor into dynamically loading DLLs, and that is using the native kernel methods LoadLibrary, GetProcAddress and FreeLibrary. GetProcAddress may be familiar with OpenGL developers.

The plan is to use those methods to load the GetVersion function into C#.

To streamline the process, we add those methods into a C# class called NativeLibrary. It will load and unload native DLLs.

NativeLibrary

The implementation is short and sweet. For added convenience, there is a method for getting the current DLL path name based on the environment of the running process.

public static class NativeLibrary
{
    [DllImport("kernel32.dll")]
    public static extern IntPtr LoadLibrary(string dllToLoad);

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

    [DllImport("kernel32.dll")]
    public static extern bool FreeLibrary(IntPtr hModule);

    public static string GetLibraryPathname(string filename)
    {
        // If 64-bit process, load 64-bit DLL
        bool is64bit = System.Environment.Is64BitProcess;

        string prefix = "Win32";

        if (is64bit)
        {
            prefix = "x64";
        }

        var lib1 = prefix + @"\" + filename;

        return lib1;
    }
}

Using NativeLibrary to dynamically load DLLs

To access our GetVersion method, we need to add a wrapper around this library. With a wrapper, it’s easier to extend the wrapper to import more methods from the DLL.

The name of our C++ DLL is called Lib1, hence the name Lib1Wrapper is fitting.

To be able to call GetVersion, we need to define the function signature in C# exactly as it is defined in C++. For this we can use delegates.

// Delegate with function signature for the GetVersion function
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.U4)]
delegate UInt32 GetVersionDelegate(
    [OutAttribute][InAttribute] StringBuilder versionString,
    [OutAttribute] UInt32 length);

Remember the function signature in C++? It is uint32_t GetVersion(char * buffer, uint32_t length). Notice how we use only POD (plain old data). Those PODs are really the least common denominator in interop between languages. And it has to be identical in both languages.

Using this delegate, we can create a GetVersion delegate, which will hold the function pointer for GetVersion in the native library.

// Handles and delegates
IntPtr _dllhandle = IntPtr.Zero;
GetVersionDelegate _getversion = null;

For good measure, default initialize it to null.

Loading the library

With this information, we can begin to load the DLL. It’s a two step process. First we load the DLL, then the methods within the dynamic library. And finally when we’re done with the dynamic library, we have to free the native handles.

With the NativeLibrary class above, we can try to load a library using only two lines of code.

// Get 32-bit/64-bit library directory
var lib1 = NativeLibrary.GetLibraryPathname("Lib1.dll");
_dllhandle = NativeLibrary.LoadLibrary(lib1);

And then we have to check for errors.

// Handle error loading
if (_dllhandle == IntPtr.Zero)
{
    return;
}

This is a very simple example wrapper. Implement exceptions or other error reporting mechanisms to suit your needs.

Loading the methods

If the loading is successful, try to get a handle to the exported method in the dynamic library.

// Get handle to GetVersion method in Lib1.dll
var get_version_handle = NativeLibrary.GetProcAddress(_dllhandle, "GetVersion");

If we get a handle, we can get the function pointer and cast it to the delegate.

// If successful, load function pointer
if (get_version_handle != IntPtr.Zero)
{
    _getversion = (GetVersionDelegate)Marshal.GetDelegateForFunctionPointer(
        get_version_handle, 
        typeof(GetVersionDelegate));
}

If everything is successful, _getversion is a delegate to the GetVersion method in Lib1.dll.

Using the method

Using the method is as simple as calling the delegate, if the delegate is not null. To avoid writing if-else around the delegate, we use our wrapper to to check the state of the delegate. If the delegate is not null, it’s probably valid and may safely be called. If not, return a default value or raise you custom exception if it’s necessary.

public string GetVersion()
{
    if (_getversion != null)
    {
        // Allocate buffer
        var size = 100;
        StringBuilder builder = new StringBuilder(size);

        // Get version string
        _getversion(builder, (uint)size);

        // Return string
        return builder.ToString();
    }

    return "";
}

Cleanup

When we’re done with the dynamic library, we have to clean up the native handles. As a library and a wrapper, we don’t know how long the application will live, so it’s deemed hygienic to clean up.

~Lib1Wrapper()
{
    // Free resources. 
    // Probably should use SafeHandle or some similar class, 
    // but this will do for now.
    NativeLibrary.FreeLibrary(_dllhandle);
}

Complete code samples are available on GitHub.