首页 > 代码库 > .Net 程序在自定义位置查找托管/非托管 dll 的几种方法
.Net 程序在自定义位置查找托管/非托管 dll 的几种方法
一、自定义托管 dll 程序集的查找位置
目前(.Net4.7)能用的有2种:
1 #define DEFAULT_IMPLEMENT 2 //#define DEFAULT_IMPLEMENT2 3 //#define HACK_UPDATECONTEXTPROPERTY 4 5 namespace X.Utility 6 { 7 using System; 8 using System.Collections.Generic; 9 using System.IO; 10 using System.Reflection; 11 using X.Reflection; 12 13 public static partial class AppUtil 14 { 15 #region Common Parts 16 #if DEFAULT_IMPLEMENT || DEFAULT_IMPLEMENT2 17 public static string AssemblyExtension { get; set; } = "dll"; 18 #endif 19 #endregion 20 21 #region DEFAULT_IMPLEMENT 22 #if DEFAULT_IMPLEMENT 23 private static Dictionary<string, List<string>> dlls; 24 private static void ScanDirs(string[] dirNames) 25 { 26 dlls = new Dictionary<string, List<string>>(); 27 if (0 == dirNames.Length) ScanDir(AppExeDir + "dlls"); 28 else foreach (var dn in dirNames) ScanDir(Path.IsPathRooted(dn) ? dn : AppExeDir + dn); 29 } 30 private static void ScanDir(string dir) 31 { 32 foreach (var f in Directory.GetFiles(dir, "*." + AssemblyExtension)) 33 { 34 var an = f.GetLoadableAssemblyName(); 35 if (null == an) continue; 36 var anf = an.FullName; 37 if (!dlls.ContainsKey(anf)) dlls.Add(anf, new List<string>()); 38 dlls[anf].Add(f); 39 } 40 } 41 /// <summary> 42 /// 以调用该方法时的目录状态为准,如果在调用方法之后目录或其内dll文件发生了变化,将导致加载失败。 43 /// 不传入任何参数则默认为 dlls 子目录。 44 /// </summary> 45 /// <param name="dirNames">相对路径将从入口exe所在目录展开为完整路径</param> 46 public static void SetPrivateBinPath(params string[] dirNames) 47 { 48 if (null != dlls) return; 49 ScanDirs(dirNames); 50 AppDomain.CurrentDomain.AssemblyResolve += AppDomain_AssemblyResolve_DEFAULT_IMPLEMENT; 51 } 52 private static Assembly AppDomain_AssemblyResolve_DEFAULT_IMPLEMENT(object sender, ResolveEventArgs args) 53 => dlls.ContainsKey(args.Name) 54 ? Assembly.Load(File.ReadAllBytes(dlls[args.Name][0])) 55 : null; 56 #endif 57 #endregion 58 59 #region DEFAULT_IMPLEMENT2 60 #if DEFAULT_IMPLEMENT2 61 public static List<string> PrivateDllDirs { get; } = new List<string> { "dlls" }; 62 private static bool enablePrivateDllDirs; 63 public static bool EnablePrivateDllDirs 64 { 65 get => enablePrivateDllDirs; 66 set 67 { 68 if (value =http://www.mamicode.com/= enablePrivateDllDirs) return; 69 if (value) AppDomain.CurrentDomain.AssemblyResolve += AppDomain_AssemblyResolve_DEFAULT_IMPLEMENT2; 70 else AppDomain.CurrentDomain.AssemblyResolve -= AppDomain_AssemblyResolve_DEFAULT_IMPLEMENT2; 71 enablePrivateDllDirs = value; 72 } 73 } 74 private static Assembly AppDomain_AssemblyResolve_DEFAULT_IMPLEMENT2(object sender, ResolveEventArgs args) 75 { 76 foreach (var dn in PrivateDllDirs) 77 { 78 var dir = Path.IsPathRooted(dn) ? dn : AppExeDir + dn; 79 foreach (var f in Directory.GetFiles(dir, "*." + AssemblyExtension)) 80 { 81 var an = f.GetLoadableAssemblyName(); 82 if (null == an) continue; 83 if (an.FullName == args.Name) 84 return Assembly.Load(File.ReadAllBytes(f)); 85 } 86 } 87 return null; 88 } 89 #endif 90 #endregion 91 92 #region HACK_UPDATECONTEXTPROPERTY 93 #if HACK_UPDATECONTEXTPROPERTY 94 public static void SetPrivateBinPathHack2(params string[] dirNames) 95 { 96 const string privateBinPathKeyName = "PrivateBinPathKey"; 97 const string methodName_UpdateContextProperty = "UpdateContextProperty"; 98 const string methodName_GetFusionContext = "GetFusionContext"; 99 100 for (var i = 0; i < dirNames.Length; ++i) 101 if (!Path.IsPathRooted(dirNames[i])) 102 dirNames[i] = AppExeDir + dirNames[i]; 103 104 var privateBinDirectories = string.Join(";", dirNames); 105 var curApp = AppDomain.CurrentDomain; 106 var appDomainType = typeof(AppDomain); 107 var appDomainSetupType = typeof(AppDomainSetup); 108 var privateBinPathKey = appDomainSetupType 109 .GetProperty(privateBinPathKeyName, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.GetProperty) 110 .GetValue(null) 111 .ToString(); 112 curApp.SetData(privateBinPathKey, privateBinDirectories); 113 appDomainSetupType 114 .GetMethod(methodName_UpdateContextProperty, BindingFlags.NonPublic | BindingFlags.Static) 115 .Invoke(null, new[] 116 { 117 appDomainType 118 .GetMethod(methodName_GetFusionContext, BindingFlags.NonPublic | BindingFlags.Instance) 119 .Invoke(curApp, null), 120 privateBinPathKey, 121 privateBinDirectories 122 }); 123 } 124 #endif 125 #endregion 126 } 127 }
- DEFAULT_IMPLEMENT - 这个算是比较“正统”的方式。通过 AssemblyResolve 事件将程序集 dll 文件读入内存后加载。以调用该方法时的目录状态为准,如果在调用方法之后目录或其内dll文件发生了变化,将导致加载失败。
- DEFAULT_IMPLEMENT2 - 关键细节与前一种方式相同,只是使用方式不同,并且在每一次事件调用中都会在文件系统中进行查找。
- HACK_UPDATECONTEXTPROPERTY - 来源于 AppDomain.AppendPrivatePath 方法的框架源码,其实就是利用反射把这个方法做的事做了一遍。该方法已经被M$废弃,因为这个方法会在程序集加载后改变程序集的行为(其实就是改变查找后续加载的托管dll的位置)。目前(.Net4.7)还是可以用的,但是已经被标记为“已过时”了,后续版本不知道什么时候就会取消了。
M$ 对 AppDomain.AppendPrivatePath 的替代推荐是涉及到 AppDomainSetup 的一系列东西,很麻烦,必须在 AppDomain 加载前设置好参数,但是当前程序已经在运行了所以这种方法对自定义查找托管dll路径的目的无效。
通常来说,不推荐采用 Hack 的方法,毕竟是非正规的途径,万一哪天 M$ 改了内部的实现就抓瞎了。
DEFAULT_IMPLEMENT 的方法可以手动加个文件锁,或者直接用 Assembly.LoadFile 方法加载,这样就会锁定文件。
注意:这些方法只适用于托管dll程序集,对 DllImport 特性引入的非托管 dll 不起作用。
.Net 开发组关于取消 AppDomain.AppendPrivatePath 方法的博客,下面有一些深入的讨论,可以看看:
https://blogs.msdn.microsoft.com/dotnet/2009/05/14/why-is-appdomain-appendprivatepath-obsolete/
在访客评论和开发组的讨论中,提到了一个关于 AssemblyResolve 事件的细节:.Net 不会对同一个程序集触发两次该事件,因此在事件代码当中没有必要手动去做一些额外的防止多次载入同一程序集的措施,也不需要手动缓存从磁盘读取的程序集二进制数据。
二、自定义非托管 dll 查找位置
如果只需要一个自定义目录:
1 namespace X.Utility 2 { 3 using System; 4 using System.IO; 5 using System.Runtime.InteropServices; 6 7 public static partial class AppUtil 8 { 9 [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] 10 private static extern bool SetDllDirectory(string dir); 11 12 public static void Set64Or32BitDllDir(string x64DirName = @"dlls\x64", string x86DirName = @"dlls\x86") 13 { 14 var dir = IntPtr.Size == 8 ? x64DirName : x86DirName; 15 if (!Path.IsPathRooted(dir)) dir = AppEntryExeDir + dir; 16 if (!SetDllDirectory(dir)) 17 throw new System.ComponentModel.Win32Exception(nameof(SetDllDirectory)); 18 } 19 } 20 }
如果需要多个自定义目录:
1 //#define ALLOW_REMOVE_DLL_DIRS 2 3 namespace X.Utility 4 { 5 using System; 6 using System.Collections.Generic; 7 using System.IO; 8 using System.Runtime.InteropServices; 9 10 public static partial class AppUtil 11 { 12 [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] 13 private static extern bool SetDefaultDllDirectories(int flags = 0x1E00); 14 [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] 15 private static extern IntPtr AddDllDirectory(string dir); 16 #if ALLOW_REMOVE_DLL_DIRS 17 [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] 18 private static extern bool RemoveDllDirectory(IntPtr cookie); 19 20 public static Dictionary<string, IntPtr> DllDirs { get; } = new Dictionary<string, IntPtr>(); 21 #endif 22 23 public static readonly string[] x64DefaultDllDirs = new[] { @"dlls\x64" }; 24 public static readonly string[] x86DefaultDllDirs = new[] { @"dlls\x86" }; 25 26 public static void Set64Or32BitDllDirs(string[] x64DirNames, string[] x86DirNames) 27 { 28 if (null == x64DirNames) throw new ArgumentNullException(nameof(x64DirNames)); 29 if (null == x86DirNames) throw new ArgumentNullException(nameof(x86DirNames)); 30 31 if (!SetDefaultDllDirectories()) 32 throw new System.ComponentModel.Win32Exception(nameof(SetDefaultDllDirectories)); 33 34 AddDllDirs(IntPtr.Size == 8 ? x64DirNames : x86DirNames); 35 } 36 37 public static void AddDllDirs(params string[] dirNames) 38 { 39 foreach (var dn in dirNames) 40 { 41 var dir = Path.IsPathRooted(dn) ? dn : AppEntryExeDir + dn; 42 #if ALLOW_REMOVE_DLL_DIRS 43 if (!DllDirs.ContainsKey(dir)) 44 DllDirs[dir] = 45 #endif 46 AddDllDirectory(dir); 47 } 48 } 49 50 #if ALLOW_REMOVE_DLL_DIRS 51 public static void RemoveDllDirs(params string[] dirNames) 52 { 53 foreach (var dn in dirNames) 54 { 55 var dir = Path.IsPathRooted(dn) ? dn : AppEntryExeDir + dn; 56 if (DllDirs.TryGetValue(dir, out IntPtr cookie)) 57 RemoveDllDirectory(cookie); 58 } 59 } 60 #endif 61 } 62 }
针对非托管 dll 自定义查找路径是用 Windows 原生 API 提供的功能来完成。
#define ALLOW_REMOVE_DLL_DIRS //取消这行注释可以打开【移除自定义查找路径】的功能
三、比较重要的是用法
1 public partial class App 2 { 3 static App() 4 { 5 AppUtil.SetPrivateBinPath(); 6 AppUtil.Set64Or32BitDllDir(); 7 } 8 [STAThread] 9 public static void Main() 10 { 11 //do something... 12 } 13 }
最合适的地方是放在【启动类】的【静态构造】函数里面,这样可以保证在进入 Main 入口点之前已经设置好了自定义的 dll 查找目录。
四、代码中用到的其他代码
- 检测 dll 程序集是否可加载到当前进程
1 namespace X.Reflection 2 { 3 using System; 4 using System.Reflection; 5 6 public static partial class ReflectionX 7 { 8 private static readonly ProcessorArchitecture CurrentProcessorArchitecture = IntPtr.Size == 8 ? ProcessorArchitecture.Amd64 : ProcessorArchitecture.X86; 9 public static AssemblyName GetLoadableAssemblyName(this string dllPath) 10 { 11 try 12 { 13 var an = AssemblyName.GetAssemblyName(dllPath); 14 switch (an.ProcessorArchitecture) 15 { 16 case ProcessorArchitecture.MSIL: return an; 17 case ProcessorArchitecture.Amd64: 18 case ProcessorArchitecture.X86: return CurrentProcessorArchitecture == an.ProcessorArchitecture ? an : null; 19 } 20 } 21 catch { } 22 return null; 23 } 24 } 25 }
- 当前 exe 路径和目录
1 namespace X.Utility 2 { 3 using System; 4 using System.IO; 5 using System.Reflection; 6 public static partial class AppUtil 7 { 8 public static string AppExePath { get; } = Assembly.GetEntryAssembly().Location; 9 public static string AppExeDir { get; } = Path.GetDirectoryName(AppExePath) + Path.DirectorySeparatorChar; 10 11 #if DEBUG 12 public static string AppExePath1 { get; } = Path.GetFullPath(Assembly.GetEntryAssembly().CodeBase.Substring(8)); 13 public static string AppExeDir1 { get; } = AppDomain.CurrentDomain.BaseDirectory; 14 public static string AppExeDir2 { get; } = AppDomain.CurrentDomain.SetupInformation.ApplicationBase; 15 16 static AppUtil() 17 { 18 System.Diagnostics.Debug.Assert(AppExePath == AppExePath1); 19 System.Diagnostics.Debug.Assert(AppExeDir == AppExeDir1); 20 System.Diagnostics.Debug.Assert(AppExeDir1 == AppExeDir2); 21 } 22 #endif 23 } 24 }
.Net 程序在自定义位置查找托管/非托管 dll 的几种方法