动态加载含原生库依赖的程序集时,如何仅修改加载端解决托管包装库找不到原生库的问题?
动态加载含原生库依赖的程序集时,如何仅修改加载端解决托管包装库找不到原生库的问题?
这个坑我太熟悉了!动态加载程序集时遇到原生依赖找不到的问题,尤其是还不能碰被加载的库,确实让人头大。咱们先搞清楚问题根源,再一步步解决它。
问题根源
像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




