1. 程式人生 > >.Net 程序在自定義位置查找托管/非托管 dll 的幾種方法

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

bsp aps assembly ntp nba ddd lex += directory

原文:.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 }
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(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 提供的功能來完成。

#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
  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

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