You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

动态加载含原生库依赖的程序集时,如何仅修改加载端解决托管包装库找不到原生库的问题?

动态加载含原生库依赖的程序集时,如何仅修改加载端解决托管包装库找不到原生库的问题?

这个坑我太熟悉了!动态加载程序集时遇到原生依赖找不到的问题,尤其是还不能碰被加载的库,确实让人头大。咱们先搞清楚问题根源,再一步步解决它。

问题根源

像LibGit2Sharp这种托管包装库,通常会把对应的原生DLL放在程序集目录下的runtimes/{平台}/native子文件夹里(比如Windows下的runtimes/win-x64/native)。正常启动的.NET程序,运行时会自动探测这些runtime子目录,但动态加载程序集时,默认的加载上下文不会自动处理这些路径——当包装库尝试调用原生方法时,系统找不到对应的DLL,自然就抛出DllNotFoundException了。

解决方案(仅修改DynamicLoadingApp)

下面给你两种可行的方案,都是只改加载端代码,完全不需要碰MyLib或者依赖的第三方库。

方案一:手动添加原生库搜索路径(简单直接)

思路是在加载MyLib之前,把对应平台的native目录添加到系统的DLL搜索路径里,让系统能找到原生库。

代码示例:

using System;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;

namespace DynamicLoadingApp
{
    class Program
    {
        // 导入Windows的SetDllDirectory函数,用于添加DLL搜索路径
        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        private static extern bool SetDllDirectory(string lpPathName);

        static void Main(string[] arguments)
        {
            string assemblyFilePath = Path.GetFullPath(arguments.Single());
            string assemblyDir = Path.GetDirectoryName(assemblyFilePath)!;

            // 根据当前运行平台,定位对应的native目录
            string runtimeNativeDir;
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                runtimeNativeDir = Path.Combine(assemblyDir, "runtimes", "win-x64", "native");
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                runtimeNativeDir = Path.Combine(assemblyDir, "runtimes", "linux-x64", "native");
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                runtimeNativeDir = Path.Combine(assemblyDir, "runtimes", "osx-x64", "native");
            }
            else
            {
                throw new PlatformNotSupportedException("当前操作系统暂不支持");
            }

            // 如果目录存在,就添加到系统DLL搜索路径
            if (Directory.Exists(runtimeNativeDir))
            {
                bool success = SetDllDirectory(runtimeNativeDir);
                if (!success)
                {
                    throw new System.ComponentModel.Win32Exception();
                }
                // 注意:如果需要添加多个路径,Windows 10+可以用AddDllDirectory,或者把路径用分号拼接后传入SetDllDirectory
            }

            // 现在再加载程序集并调用方法就没问题了
            Assembly assembly = Assembly.LoadFrom(assemblyFilePath);
            MethodInfo methodInfo = assembly.GetTypes().Single().GetMethod("DoYourThing") ?? throw new Exception("未找到DoYourThing方法");
            methodInfo.Invoke(null, null);
        }
    }
}

如果是跨平台场景,也可以用环境变量的方式(更通用):

// 替换上面的SetDllDirectory部分
if (Directory.Exists(runtimeNativeDir))
{
    string pathEnvVar = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "PATH" : 
                        RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "LD_LIBRARY_PATH" : "DYLD_LIBRARY_PATH";
    
    string currentPath = Environment.GetEnvironmentVariable(pathEnvVar) ?? string.Empty;
    string newPath = $"{runtimeNativeDir}{Path.PathSeparator}{currentPath}";
    Environment.SetEnvironmentVariable(pathEnvVar, newPath);
}

方案二:自定义AssemblyLoadContext(更优雅的现代方案)

这种方式是创建一个自定义的程序集加载上下文,接管程序集和原生库的加载逻辑,自动去runtime目录查找原生库,不需要修改系统路径或环境变量,更符合.NET的设计理念。

代码示例:

using System;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
using System.Runtime.InteropServices;

namespace DynamicLoadingApp
{
    // 自定义程序集加载上下文
    class CustomAssemblyLoadContext : AssemblyLoadContext
    {
        private readonly string _assemblyRootDir;

        public CustomAssemblyLoadContext(string assemblyRootDir) : base(isCollectible: false)
        {
            _assemblyRootDir = assemblyRootDir;
        }

        // 托管程序集的加载逻辑
        protected override Assembly? Load(AssemblyName assemblyName)
        {
            // 先尝试从默认上下文加载系统或全局程序集
            try
            {
                return Default.LoadFromAssemblyName(assemblyName);
            }
            catch
            {
                // 尝试从目标程序集目录加载依赖
                string assemblyPath = Path.Combine(_assemblyRootDir, $"{assemblyName.Name}.dll");
                if (File.Exists(assemblyPath))
                {
                    return LoadFromAssemblyPath(assemblyPath);
                }
                return null;
            }
        }

        // 原生库的加载逻辑
        protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
        {
            // 根据平台找到对应的native目录
            string platformSubDir = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "win-x64" :
                                   RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "linux-x64" : "osx-x64";
            string dllFullPath = Path.Combine(_assemblyRootDir, "runtimes", platformSubDir, "native", unmanagedDllName);
            
            // 如果找到原生库,就加载它
            if (File.Exists(dllFullPath))
            {
                return LoadUnmanagedDllFromPath(dllFullPath);
            }
            
            // 找不到的话,交给默认加载逻辑
            return base.LoadUnmanagedDll(unmanagedDllName);
        }
    }

    class Program
    {
        static void Main(string[] arguments)
        {
            string assemblyFilePath = Path.GetFullPath(arguments.Single());
            string assemblyDir = Path.GetDirectoryName(assemblyFilePath)!;

            // 使用自定义加载上下文加载程序集
            using var loadContext = new CustomAssemblyLoadContext(assemblyDir);
            Assembly assembly = loadContext.LoadFromAssemblyPath(assemblyFilePath);
            
            MethodInfo methodInfo = assembly.GetTypes().Single().GetMethod("DoYourThing") ?? throw new Exception("未找到DoYourThing方法");
            methodInfo.Invoke(null, null);
        }
    }
}

方案对比

  • 方案一简单直接,适合快速解决Windows单平台的问题;
  • 方案二更优雅,跨平台兼容性更好,也能避免修改系统路径带来的潜在问题,推荐用于需要长期维护的工具(比如你写的测试运行器)。

不管用哪种方案,都只需要修改DynamicLoadingApp的代码,完全不需要改动MyLib或者第三方依赖库,完美符合你的需求。

备注:内容来源于stack exchange,提问作者Mike Nakis

火山引擎 最新活动