1
\$\begingroup\$

Code is meant to be used in Windows .NET (tested with dotnet5.0) to dynamically load C library and return delegate of given function

Code lower is result of my attempt to rewrite the [DllImport] annotations based header file, that was provided by 3rd party along with the DLL library itself, when multiple instances of given DLL must be used in parallel, which is impossible with DllImport

Given the nature of the library, the solution was required to work only on Windows, but could be extended to support multiple platforms (loading platform-specific library, eg. .dylib / .so / .dll)

FunctionLoader.cs

using System;
using System.Collections.Concurrent;
using System.IO;
using System.Runtime.InteropServices;

    /// <summary>
    /// Helper function to dynamically load DLL contained functions on Windows only
    /// </summary>
    class FunctionLoader
    {
        [DllImport("Kernel32.dll", CharSet = CharSet.Ansi)]
        private static extern IntPtr LoadLibrary(string path);

        [DllImport("Kernel32.dll", CharSet = CharSet.Ansi)]
        private static extern IntPtr GetProcAddress(IntPtr hModule, string procName);

        /// <summary>
        /// Map String (library name) to IntPtr (reference from LoadLibrary)
        /// </summary>
        private static ConcurrentDictionary<String, IntPtr> LoadedLibraries { get; } = new ConcurrentDictionary<string, IntPtr>();

        /// <summary>
        /// Load function (by name) from DLL (by name) and return its delegate
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="dllPath"></param>
        /// <param name="functionName"></param>
        /// <returns></returns>
        public static T LoadFunction<T>(string dllPath, string functionName)
        {
            // normalize
            if (!Path.IsPathFullyQualified(dllPath))
            {
                dllPath = Path.GetFullPath(dllPath);
            }
            // Get preloaded or load the library on-demand
            IntPtr hModule = LoadedLibraries.GetOrAdd(
                dllPath,
                valueFactory: (string dllPath) => {
                    IntPtr loaded = LoadLibrary(dllPath);
                    if (loaded == IntPtr.Zero) {
                        throw new DllNotFoundException(String.Format("Library not found in path {0}", dllPath));
                    }
                    return loaded;
                }
            );
            // Load function
            var functionAddress = GetProcAddress(hModule, functionName);
            if (functionAddress == IntPtr.Zero)
            {
                throw new EntryPointNotFoundException(String.Format("Function {0} not found in {1}", functionName, dllPath));
            }
            // Return delegate, casting is hack-ish, but simplifies usage
            return (T)(object)(Marshal.GetDelegateForFunctionPointer(functionAddress, typeof(T)));
        }
    }

Expected usage then follows this pattern

Library.cs

public class Library {

  // This is the only file that is needed in base distribution of the application, all other copies/instances will be created on-demand by Library constructor
  public string DefaultLibrarySource { get; } = "DLL\\1\\ExternalLibrary.dll";
  public string LibrarySourceTemplate { get; } = "DLL\\{0}\\ExternalLibrary.dll";

  public delegate Int32 T_externalFunction(Int32 numArgument, out Int32 outNumArgument);
  public T_externalFunction externalFunction;

  public Library(int InstanceNo)
  {
    // reference correct DLL file per instance ID
    var LibraryName = String.Format(LibrarySourceTemplate, InstanceNo);

    // ensure the dir with library exists 
    var dirName = Path.GetDirectoryName(LibraryName);
    if (!Directory.Exists(dirName))
    {
        Directory.CreateDirectory(dirName);
    }

    // ensure the library itself is in place
    if (!File.Exists(LibraryName))
    {
        Console.WriteLine(String.Format("Copy {0} to {1}", DefaultLibrarySource, LibraryName));
        File.Copy(DefaultLibrarySource, LibraryName);
    }

    // load each individual function that needs to be available later
    externalFunction = FunctionLoader.LoadFunction<T_externalFunction>(LibraryName, "externalFunction");
  }
}

Program.cs

public class Program {

  static void Main(string[] args)
  {
    // get instance number 2 of DLL
    var lib = new Library(2);
    // call DLL function obtain both return and out variables
    var result = lib.externalFunction(12345, out Int32 outVar);
    // ...
  }

}

What I hope was achieved correctly:

  1. Avoid loading the DLL (LoadLibrary) or the function (GetProcAddress) multiple times, if caller requests the same DLL name or function name
  2. Relative simplicity of use

What bothers me currently with this code is

  1. To define the function interface (delegate) and accessible function the type (T_externalFunction) is currently defined separately from the function accessor (externalFunction), where with DllImport this could be done with single declaration
  2. Each function must be loaded manually in Library object constructor (or on-access), which would be way better with some kind of Attribute and processor/hook, that would automatically do the binding
\$\endgroup\$
7
  • \$\begingroup\$ Avoid loading the DLL or the function multiple times, if caller requests the same DLL name or function name then why don't you use ConcurrentDictionary to avoid loading the same thing multiple times from different threads? \$\endgroup\$ Jul 28 '21 at 7:18
  • \$\begingroup\$ @PeterCsala thank you, that's good point, should I do some kind of mutex/lock synchronization in LoadFunction or will the ConcurrentDictionary itself suffice? \$\endgroup\$ Jul 28 '21 at 10:03
  • 1
    \$\begingroup\$ Please read the paragraph after the code sample \$\endgroup\$ Jul 28 '21 at 12:08
  • 1
    \$\begingroup\$ @PeterCsala thank you, i've updated the code in question with ConcurrentDictionary usage \$\endgroup\$ Jul 28 '21 at 12:33
  • 2
    \$\begingroup\$ @MarekSebera It’s implied in the documentation: “The system maintains a per-process reference count on all loaded modules. Calling LoadLibrary increments the reference count.”; and, “If the specified module is a DLL that is not already loaded for the calling process, the system calls the DLL's DllMain function”. \$\endgroup\$ Jul 28 '21 at 14:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.