多次导入同一个DLL库,动态DllImport注解

Import same DLL library multiple times, dynamic DllImport annotation

关于之前涉及该主题的 QA

情况:

我正在寻找的是解决方案,它将允许我扩展经过验证的解决方案,从 2 个 DLL 实例到可能的 1000 个实例(如果硬件允许)。

我可以设法复制 DLL 文件并维护代码实例与文件实例映射,但是要准备 100 个 dll code-instances,我需要准备 100 个 .cs 副本 header 文件,这是不切实际和丑陋的。 header 文件有大约 30 个结构和 60 个接口方法。

header 文件的片段

// ExternalLibrary.cs
public class ExternalLibrary {
  // Changing this (eg. "ExternalLibrary2.dll") along with class name (ExternalLibrary2) and filename (ExternalLibrary2.cs) is enough, nothing else imho needs to be changed
  public const String ApiDll = "ExternalLibrary.dll";

  [DllImport(ApiDll, CallingConvention = CallingConvention.Cdecl)]
  public static extern Int32 ExternalRoutine(UInt32 Input, out UInt32 Output);
}
  1. 是否有可能使用不同的文件名动态创建 class (header) 实例,这会影响 [DllImport] 注释?
  2. 如果不是,可以使用什么可扩展的解决方案?

感谢 @David Heffernan 和其他一些人,我提出了解决方案,它也可能适用于不同构建的库,所以我分享它。

这在很大程度上依赖于三件重要的事情

  1. 每次都必须从 DLL 文件的单独副本加载库
  2. ExternalLibrary.dll 已实现,因此它可以存在于自己的目录中,不会污染项目分发文件的其他部分
  3. 这里介绍的FunctionLoader只是Windows-compatible(使用Kernel32.dll提供的例程)

解决方案是在 Microsoft.NET.Sdk.Web sdk 和 net5.0 目标之上测试的,项目是使用 Kestrel 而非 IIS 部署的,因此在测试时请注意这一点。

1。在分发中包含模板目录结构和原始库

具有这样的文件结构

/RootSolution.sln
/Project/
/Project/Project.csproj
/Project/ExternalLibrary/1/ExternalLibrary.dll

允许像这样创建 per-instance(GUID 标识)目录

/Project/ExternalLibrary/1/ExternalLibrary.dll
/Project/ExternalLibrary/516bbd6d-a5ec-42a5-93e0-d1949ca60767/ExternalLibrary.dll
/Project/ExternalLibrary/6bafaf3c-bc2b-4a1f-ae5c-696c37851b22/ExternalLibrary.dll
/Project/ExternalLibrary/0d0589fc-fc37-434d-82af-02e17a26d927/ExternalLibrary.dll

2。改造原库头文件

从如下所示的原始库头文件开始:

// ExternalLibrary.cs
public class ExternalLibrary {
  public const String ApiDll = "ExternalLibrary.dll";

  [DllImport(ApiDll, CallingConvention = CallingConvention.Cdecl)]
  public static extern Int32 ExternalRoutine(UInt32 Input, out UInt32 Output);
}

将其转化为

// ExternalLibrary.cs
using System;
using System.IO;

public class ExternalLibrary {
  public string LibraryName { get; }
  public Guid InstanceNo { get; }
  public string DefaultLibrarySource { get; } = Path.Combine("ExternalLibrary", "1", "ExternalLibrary.dll");
  public string LibrarySourceTemplate { get; } = Path.Combine("ExternalLibrary", "{0}", "ExternalLibrary.dll");

  public ExternalLibrary(Guid InstanceNo)
  {
    // use constructor provided Guid to construct full path to copy of library and it's living directory
    LibraryName = String.Format(LibrarySourceTemplate, InstanceNo);
    LibraryName = Path.GetFullPath(LibraryName);

    InstanceNo = InstanceNo;

    // create guid-appropriate directory if it does not exist
    var dirName = Path.GetDirectoryName(LibraryName);
    if (!Directory.Exists(dirName))
    {
        Directory.CreateDirectory(dirName);
    }

    // copy over the source library if it's not yet present in guid-appropriate directory
    if (!File.Exists(LibraryName))
    {
        File.Copy(DefaultLibrarySource, LibraryName);
    }

    // load function from correct DLL file into exposed delegated routine
    ExternalRoutine = FunctionLoader.LoadFunction<_ExternalRoutine>(LibraryName, "ExternalRoutine");
  }

  [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
  public delegate Int32 _ExternalRoutine(UInt32 Input, out UInt32 Output);
  public _ExternalRoutine ExternalRoutine;
}

3。在您的项目中包含 FunctionLoader class

// 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>
internal 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($"Library not found in path {dllPath}");
                }
                return loaded;
            }
        );
        // Load function
        var functionAddress = GetProcAddress(hModule, functionName);
        if (functionAddress == IntPtr.Zero)
        {
            throw new EntryPointNotFoundException($"Function {functionName} not found in {dllPath}");
        }
        // Return delegate, casting is hack-ish, but simplifies usage
        return (T)(object)(Marshal.GetDelegateForFunctionPointer(functionAddress, typeof(T)));
    }
}


请告诉我,如果这个解决方案对您有用,或者您是否找到了更优雅的方法,谢谢