原文:.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.Linq;
11 using System.Reflection;
12 using X.Linq;
13 using X.Reflection;
14
15 public static partial class AppUtil
16 {
17 #region Common Parts
18 #if DEFAULT_IMPLEMENT || DEFAULT_IMPLEMENT2
19 public static string AssemblyExtension { get; set; } = "dll";
20 private static IEnumerable<Tuple<AssemblyName, string>> ScanDirs(IList<string> dirNames)
21 => (0 == dirNames.Count ? new[] { "dlls" } : dirNames)
22 .SelectMany(dir => Directory
23 .GetFiles(Path.IsPathRooted(dir) ? dir : AppExeDir + dir, "*." + AssemblyExtension)
24 .SelectIfCalc(f => f.GetLoadableAssemblyName(), a => null != a, (f, a) => Tuple.Create(a, f))
25 );
26 private static Assembly LoadAssemblyFromList(AssemblyName an, IEnumerable<Tuple<AssemblyName, string>> al)
27 {
28 foreach (var a in al.Where(aa => aa.Item1.Name == an.Name && aa.Item1.Version == an.Version && aa.Item1.CultureName == an.CultureName))
29 return LoadAssembly(a.Item2);
30 foreach (var a in al.Where(aa => aa.Item1.Name == an.Name && aa.Item1.Version == an.Version))
31 return LoadAssembly(a.Item2);
32
33 foreach (var a in al.Where(aa => aa.Item1.Name == an.Name && aa.Item1.Version > an.Version && aa.Item1.CultureName == an.CultureName).OrderBy(aa => aa.Item1.Version))
34 return LoadAssembly(a.Item2);
35 foreach (var a in al.Where(aa => aa.Item1.Name == an.Name && aa.Item1.Version > an.Version).OrderBy(aa => aa.Item1.Version))
36 return LoadAssembly(a.Item2);
37
38 foreach (var a in al.Where(aa => aa.Item1.Name == an.Name && aa.Item1.Version < an.Version && aa.Item1.CultureName == an.CultureName).OrderByDescending(aa => aa.Item1.Version))
39 return LoadAssembly(a.Item2);
40 foreach (var a in al.Where(aa => aa.Item1.Name == an.Name && aa.Item1.Version < an.Version).OrderByDescending(aa => aa.Item1.Version))
41 return LoadAssembly(a.Item2);
42
43 return null;
44 }
45 private static Assembly LoadAssembly(string path)
46 => Assembly.Load(File.ReadAllBytes(path));
47 #endif
48 #endregion
49
50 #region DEFAULT_IMPLEMENT
51 #if DEFAULT_IMPLEMENT
52 private static IEnumerable<Tuple<AssemblyName, string>> dlls;
53 /// <summary>
54 /// 以调用该方法时的目录状态为准,如果在调用方法之后目录或其内dll文件发生了变化,将导致加载失败。
55 /// 不传入任何参数则默认为 dlls 子目录。
56 /// </summary>
57 /// <param name="dirNames">相对路径将从入口exe所在目录展开为完整路径</param>
58 public static void SetPrivateBinPath(params string[] dirNames)
59 {
60 if (null != dlls) return;
61 dlls = ScanDirs(dirNames);
62 AppDomain.CurrentDomain.AssemblyResolve += AppDomain_AssemblyResolve_DEFAULT_IMPLEMENT;
63 }
64 private static Assembly AppDomain_AssemblyResolve_DEFAULT_IMPLEMENT(object sender, ResolveEventArgs args)
65 => LoadAssemblyFromList(new AssemblyName(args.Name), dlls);
66 #endif
67 #endregion
68
69 #region DEFAULT_IMPLEMENT2
70 #if DEFAULT_IMPLEMENT2
71 public static List<string> PrivateDllDirs { get; } = new List<string> { "dlls" };
72 private static bool enablePrivateDllDirs;
73 public static bool EnablePrivateDllDirs
74 {
75 get => enablePrivateDllDirs;
76 set
77 {
78 if (value == enablePrivateDllDirs) return;
79 if (value) AppDomain.CurrentDomain.AssemblyResolve += AppDomain_AssemblyResolve_DEFAULT_IMPLEMENT2;
80 else AppDomain.CurrentDomain.AssemblyResolve -= AppDomain_AssemblyResolve_DEFAULT_IMPLEMENT2;
81 enablePrivateDllDirs = value;
82 }
83 }
84 private static Assembly AppDomain_AssemblyResolve_DEFAULT_IMPLEMENT2(object sender, ResolveEventArgs args)
85 => LoadAssemblyFromList(new AssemblyName(args.Name), ScanDirs(PrivateDllDirs));
86 #endif
87 #endregion
88
89 #region HACK_UPDATECONTEXTPROPERTY
90 #if HACK_UPDATECONTEXTPROPERTY
91 public static void SetPrivateBinPathHack2(params string[] dirNames)
92 {
93 const string privateBinPathKeyName = "PrivateBinPathKey";
94 const string methodName_UpdateContextProperty = "UpdateContextProperty";
95 const string methodName_GetFusionContext = "GetFusionContext";
96
97 for (var i = 0; i < dirNames.Length; ++i)
98 if (!Path.IsPathRooted(dirNames[i]))
99 dirNames[i] = AppExeDir + dirNames[i];
100
101 var privateBinDirectories = string.Join(";", dirNames);
102 var curApp = AppDomain.CurrentDomain;
103 var appDomainType = typeof(AppDomain);
104 var appDomainSetupType = typeof(AppDomainSetup);
105 var privateBinPathKey = appDomainSetupType
106 .GetProperty(privateBinPathKeyName, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.GetProperty)
107 .GetValue(null)
108 .ToString();
109 curApp.SetData(privateBinPathKey, privateBinDirectories);
110 appDomainSetupType
111 .GetMethod(methodName_UpdateContextProperty, BindingFlags.NonPublic | BindingFlags.Static)
112 .Invoke(null, new[]
113 {
114 appDomainType
115 .GetMethod(methodName_GetFusionContext, BindingFlags.NonPublic | BindingFlags.Instance)
116 .Invoke(curApp, null),
117 privateBinPathKey,
118 privateBinDirectories
119 });
120 }
121 #endif
122 #endregion
123 }
124 }
  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 }

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

 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(IEnumerable<string> x64DirNames, IEnumerable<string> x86DirNames)
27 {
28 if (null == x64DirNames && null == x86DirNames)
29 throw new ArgumentNullException($"Must set at least one of {nameof(x64DirNames)} or {nameof(x86DirNames)}");
30
31 if (!SetDefaultDllDirectories())
32 throw new System.ComponentModel.Win32Exception(nameof(SetDefaultDllDirectories));
33
34 AddDllDirs(IntPtr.Size == 8 ? x64DirNames ?? x64DefaultDllDirs : x86DirNames ?? x86DefaultDllDirs);
35 }
36
37 public static void AddDllDirs(IEnumerable<string> dirNames)
38 {
39 foreach (var dn in dirNames)
40 {
41 var dir = Path.IsPathRooted(dn) ? dn : AppExeDir + dn;
42 #if ALLOW_REMOVE_DLL_DIRS
43 if (!DllDirs.ContainsKey(dir))
44 DllDirs[dir] =
45 #endif
46 AddDllDirectory(dir);
47 }
48 }
49 public static void AddDllDirs(params string[] dirNames) => AddDllDirs(dirNames);
50
51 #if ALLOW_REMOVE_DLL_DIRS
52 public static void RemoveDllDirs(IEnumerable<string> dirNames)
53 {
54 foreach (var dn in dirNames)
55 {
56 var dir = Path.IsPathRooted(dn) ? dn : AppExeDir + dn;
57 if (DllDirs.TryGetValue(dir, out IntPtr cookie))
58 RemoveDllDirectory(cookie);
59 }
60 }
61 public static void RemoveDllDirs(params string[] dirNames) => RemoveDllDirs(dirNames);
62 #endif
63 }
64 }

针对非托管 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 查找目录。

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

  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 }
  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 }
  3. SelectIfCalc
     1 namespace X.Linq
    2 {
    3 using System;
    4 using System.Collections.Generic;
    5
    6 public static partial class LinqX
    7 {
    8 public static IEnumerable<TResult> SelectIfCalc<TSource, TCalculated, TResult>(this IEnumerable<TSource> source, Func<TSource, TCalculated> calculator, Func<TCalculated, bool> predicate, Func<TCalculated, TResult> selector)
    9 {
    10 foreach (var s in source)
    11 {
    12 var c = calculator(s);
    13 if (predicate(c)) yield return selector(c);
    14 }
    15 }
    16 public static IEnumerable<TResult> SelectIfCalc<TSource, TCalculated, TResult>(this IEnumerable<TSource> source, Func<TSource, TCalculated> calculator, Func<TCalculated, bool> predicate, Func<TSource, TCalculated, TResult> selector)
    17 {
    18 foreach (var s in source)
    19 {
    20 var c = calculator(s);
    21 if (predicate(c)) yield return selector(s, c);
    22 }
    23 }
    24 public static IEnumerable<TResult> SelectIfCalc<TSource, TCalculated, TResult>(this IEnumerable<TSource> source, Func<TSource, TCalculated> calculator, Func<TSource, TCalculated, bool> predicate, Func<TCalculated, TResult> selector)
    25 {
    26 foreach (var s in source)
    27 {
    28 var c = calculator(s);
    29 if (predicate(s, c)) yield return selector(c);
    30 }
    31 }
    32 public static IEnumerable<TResult> SelectIfCalc<TSource, TCalculated, TResult>(this IEnumerable<TSource> source, Func<TSource, TCalculated> calculator, Func<TSource, TCalculated, bool> predicate, Func<TSource, TCalculated, TResult> selector)
    33 {
    34 foreach (var s in source)
    35 {
    36 var c = calculator(s);
    37 if (predicate(s, c)) yield return selector(s, c);
    38 }
    39 }
    40 }
    41 }

最新文章

  1. BCM94352HMB蓝牙BCM20702A0在Ubuntu 14.04下的驱动方法
  2. 【Javascript】—— 1 方法function的高级特性
  3. InLineHookSSDT
  4. hdoj 5371 Hotaru&#39;s problem
  5. Extjs-4.2.1(一)——编辑 hello word
  6. bzoj2259
  7. Maven Integration for Eclipse 正确地址
  8. android开发基本流程
  9. Group Commit of Binary Log
  10. android代码实现关机
  11. java static关键字和代码块
  12. Python给照片换底色(蓝底换红底)
  13. 实现首字母或拼音检索-sql语句方式
  14. leetcode python 030 Substring with Concatenation of All Words
  15. rtsp 学习之路一
  16. zk可视化工具
  17. Java潜在的坑持续总结
  18. Unknown column in &#39;field list&#39;
  19. python中的文件操作
  20. ionic跳转(一)

热门文章

  1. appium使用教程(一 环境搭建)-------------1.准备阶段
  2. [NOIP2009提高组]靶形数独
  3. Vue代理&amp;跨域
  4. Java,泛型类型通配符和C#对照
  5. Servlet具体解释
  6. 独立python环境之virtualenv和virtualenvwrapper
  7. Aizu - 2555 Everlasting Zero 模拟
  8. 将本地的代码上传到网上SVN库
  9. 记录一下sql两个表关联的查询使用方法
  10. 联想 Thinkserver TS250服务器RAID1 重建测试