首页 > 代码库 > .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 }
View Code
  1. DEFAULT_IMPLEMENT - 这个算是比较“正统”的方式。通过 AssemblyResolve 事件将程序集 dll 文件读入内存后加载。以调用该方法时的目录状态为准,如果在调用方法之后目录或其内dll文件发生了变化,将导致加载失败。
  2. DEFAULT_IMPLEMENT2 - 关键细节与前一种方式相同,只是使用方式不同,并且在每一次事件调用中都会在文件系统中进行查找。
  3. 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 }
View Code

如果需要多个自定义目录:

技术分享
 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 }
View Code

针对非托管 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 }
View Code

最合适的地方是放在【启动类】的【静态构造】函数里面,这样可以保证在进入 Main 入口点之前已经设置好了自定义的 dll 查找目录。

四、代码中用到的其他代码

  1. 检测 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 }
    View Code
  2. 当前 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 }
    View Code

.Net 程序在自定义位置查找托管/非托管 dll 的几种方法