.Net 程序在自定義位置查找託管/非託管 dll 的幾種方法

1、自定義託管 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 }
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路徑的目的無效。cookie

 

一般來講,不推薦採用 Hack 的方法,畢竟是非正規的途徑,萬一哪天 M$ 改了內部的實現就抓瞎了。app

DEFAULT_IMPLEMENT 的方法能夠手動加個文件鎖,或者直接用 Assembly.LoadFile 方法加載,這樣就會鎖定文件。框架

 

 

注意:這些方法只適用於託管dll程序集,對 DllImport 特性引入的非託管 dll 不起做用。dom

 

.Net 開發組關於取消 AppDomain.AppendPrivatePath 方法的博客,下面有一些深刻的討論,能夠看看:
https://blogs.msdn.microsoft.com/dotnet/2009/05/14/why-is-appdomain-appendprivatepath-obsolete/
在訪客評論和開發組的討論中,提到了一個關於 AssemblyResolve 事件的細節:.Net 不會對同一個程序集觸發兩次該事件,所以在事件代碼當中沒有必要手動去作一些額外的防止屢次載入同一程序集的措施,也不須要手動緩存從磁盤讀取的程序集二進制數據。ide

2、自定義非託管 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

若是須要多個自定義目錄:this

 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 }
View Code

針對非託管 dll 自定義查找路徑是用 Windows 原生 API 提供的功能來完成。spa

#define ALLOW_REMOVE_DLL_DIRS //取消這行註釋能夠打開【移除自定義查找路徑】的功能3d

3、比較重要的是用法

 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 查找目錄。

4、代碼中用到的其餘代碼

  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
  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 }
    View Code
相關文章
相關標籤/搜索