原文:.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 }

.Net 程序在自定义位置查找托管/非托管 dll 的几种方法的更多相关文章

  1. 托管非托管Dll动态调用

    原文:托管非托管Dll动态调用 最近经常看到有人问托管非托管Dll调用的问题.对于动态库的调用其实很简单.网上很多代码都实现了Dll的静态调用方法.我主要谈论下动态库的动态加载. 对于托管动态库,实现 ...

  2. Chrome程序及数据位置变更到非系统盘

    Chrome浏览器在Windows系统上安装过程,没有设置安装位置的步骤,所以默认是安装在C盘的.并且,若Chrome作为主要浏览器使用,随着时间的积累,数据文件会非常多.增加系统盘的负荷. Wind ...

  3. C# 托管非托管资源释放

    1.C#几乎所有对象都为托管对象,不同点是有的对象封装了非托管资源. 2.C#大部分对象在进行垃圾回收时都可以回收,包括非托管资源,因为非托管资源都已经通过C#类进行了封装,会将非托管资源的释放放在析 ...

  4. oracle中查找和删除重复记录的几种方法总结

    平时工作中可能会遇到当试图对库表中的某一列或几列创建唯一索引时,系统提示 ORA-01452 :不能创建唯一索引,发现重复记录. 下面总结一下几种查找和删除重复记录的方法(以表CZ为例): 表CZ的结 ...

  5. MFC程序执行后台操作时不允许操作界面的一种方法

    在使用MFC编写界面程序时,有时候会遇到像点击按钮后,后台进行大量操作后才显示处理结果这种情况,在后台处理过程中,界面不应该被允许做任何操作,这里介绍一种方法. 解决办法 点击按钮后,弹出一个模态对话 ...

  6. [转载]SQL Server查找包含某关键字的存储过程3种方法

    存储过程都写在一个指定的表中了,我们只要使用like查询就可以实现查询当前这台SQL Server中所有存储过程中包括了指定关键字的存储过程并显示出来,下面一起来看看我总结了几条命令. 例子1 代码如 ...

  7. [UE4]自定义函数,快速增加输入参数的一种方法

  8. C#的托管与非托管大难点

    托管代码与非托管代码 众所周知,我们正常编程所用的高级语言,是无法被计算机识别的.需要先将高级语言翻译为机器语言,才能被机器理解和运行.在标准C/C++中,编译过程是这样的:源代码首先经过预处理器,对 ...

  9. C#的三大难点之二:托管与非托管

    相关文章: C#的三大难点之前传:什么时候应该使用C#?​C#的三大难点之一:byte与char,string与StringBuilderC#的三大难点之二:托管与非托管C#的三大难点之三:消息与事件 ...

随机推荐

  1. BZOJ 3786: 星系探索 欧拉游览树

    一个叫 Euler-Tour-Tree 的数据结构,说白了就是用 Splay_Tree 维护欧拉序 #include <cstring> #include <algorithm> ...

  2. python BeautifulSoup 获取页面多个子节点中的各个节点的内容

    页面html格式为 <tr bgcolor="#7bb5de"><td style="border-bottom: 1px solid #C9D8AD& ...

  3. Javascript中正则的 match、test、exec使用方法和区别

    总结: match 是str调用 test和exec是正则表达式调用 test只返回true或false, exec和match的结果是相同的,返回结果比较复杂

  4. [转载]-分布式之redis复习精讲

    原创地址:https://www.cnblogs.com/rjzheng/p/9096228.html 看这篇文章前,我看的是另一个人博客上的文章.看到最后(评论这一块)很多人就指出这并非原创而是抄袭 ...

  5. 20180929 北京大学 人工智能实践:Tensorflow笔记01

    北京大学 人工智能实践:Tensorflow笔记 https://www.bilibili.com/video/av22530538/?p=13 (完)

  6. Ehcache学习总结(2)--Ehcache整合spring配置

    首先需要的maven依赖为: [html] view plain copy <!--ehcache--> <dependency> <groupId>com.goo ...

  7. 3D打印技术之切片引擎(6)

    [此系列文章基于熔融沉积( fused depostion modeling, FDM )成形工艺] 这一篇文章说一下填充算法中的网格填充.网格填充在现有的较为成熟的引擎中是非常普遍的:skeinfo ...

  8. 【 Beginning iOS 7 Development《精通iOS7开发》】05 Autorotation and Autosizing

    一.旋转后相对位置不变 watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvZ29nbGVy/font/5a6L5L2T/fontsize/400/fill/I0 ...

  9. 使用powerdesigner建模时设置主键自增的问题

    研究了一下,其实只要双击表,选择columns,再双击在你所要设为自增型的键上(比如你的id)或者右键选择Properties,弹出一个ColumnProperties 对话框,我们看到有标识 ide ...

  10. 访问API的方式为:localhost/api/customers, 创建自定义JSON格式化器

    注意的是,访问API的方式为:localhost/api/customers,在实际中将要根据情况替换合适的端口,默认所有的WEB API都是通过/api根目录的方式访问的 创建自定义JSON格式化器 ...