.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 == 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 }

  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(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 }

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

时间: 2024-10-09 20:10:06

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

C#调用非托管C++DLL的两种方法

C#编写的代码属于跨平台的托管代码,C++语言可以编写托管(managed)和非托管(native)代码.在C#与C++的混合编程中,经常会使用C#来调用native C++的DLL,下面有两种常用的调用方法供大家参考. 使用P/Invoke直接调用native C++ Dll里面的函数.(注:此方法只能调用函数,不能调用class). C#通过C++ CLR(托管的C++)来调用nativeC++ DLL, C++ CLR作为链接C#与native之间的纽带. 方法一的详细过程如下: (1)打

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

平时工作中可能会遇到当试图对库表中的某一列或几列创建唯一索引时,系统提示 ORA-01452 :不能创建唯一索引,发现重复记录. 下面总结一下几种查找和删除重复记录的方法(以表CZ为例): 表CZ的结构如下: 1 SQL> desc cz 2 Name Null? Type 3 ------------------------------------------------------------------- 4 C1 NUMBER(10) 5 C10 NUMBER(5) 6 C20 VARC

汇编--查找第一个非0字符的五种方法

第一种 #include <windows.h> #include <stdio.h> void main(void) { int m = 0, cout = 0; char cStr2Find[1024] = {0}; printf("请输入一段字符串"); scanf("%s", cStr2Find); __asm{ pushad; //保存所有寄存器 lea esi,cStr2Find; //esi指向查找字符串 xor ecx,ecx

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

相关文章: C#的三大难点之前传:什么时候应该使用C#??C#的三大难点之一:byte与char,string与StringBuilderC#的三大难点之二:托管与非托管C#的三大难点之三:消息与事件 托管代码与非托管代码 众所周知,我们正常编程所用的高级语言,是无法被计算机识别的.需要先将高级语言翻译为机器语言,才能被机器理解和运行.在标准C/C++中,编译过程是这样的:源代码首先经过预处理器,对头文件以及宏进行解析,然后经过编译器,生成汇编代码,接着,经过汇编,生成机器指令,最后将所有文件连

解决C#程序只允许运行一个实例的几种方法详解

方法一:使用线程互斥变量. 通过定义互斥变量来判断是否已运行实例.把program.cs文件里的Main()函数改为如下代码: using System; using System.Windows.Forms; using System.Runtime.InteropServices; namespace NetTools { static class Program { [DllImport("user32.dll")] private static extern bool Flas

struts中调用servlet的两种方法——IcC方式和非IoC方式的代码demo

1 package com.java1234.action;//所在的包 2 3 import java.sql.Connection;//数据库连接的类 4 import java.util.ArrayList;//ArrayList和List是在private List<DataDic> dataDicList=new 5 6 ArrayList<DataDic>();中用到 7 import java.util.List;//ArrayList和List是在private L

托管程序与非托管程序的区别

介绍托管程序与非托管程序的区别 本文主要介绍托管程序与非托管程序的概念,以及两者之间的区别.希望对你有帮助,一起来看. AD:[线下活动]三大新锐HTML 5企业汇聚51CTO—大话移动前端技术 托管代码是一microsoft的中间语言,他主要的作用是在.NET FRAMEWORK的CLR执行代码前去编译源代码,也就是说托管代码充当着翻译的作用.下面介绍托管代码和非托管代码. 什么是托管代码? 托管代码就是Visual Basic .NET和C#编译器编译出来的代码.编译器把代码编译成中间语言(

将WinForm程序(含多个非托管Dll)合并成一个exe的方法

开发程序的时候经常会引用一些第三方的DLL,然后编译生成的exe文件就不能脱离这些DLL独立运行了. ILMerge能把托管dll跟exe合并起来生成一个新的exe,但是当我们在项目中使用了非托管的dll,也就是使用了第三方dll时,合并虽然成功但是却无法运行,提示“不是有效的win32应用程序“ 这时候我们需要用到一款名为Fody.Costura的工具.Fody.Costura是一个Fody框架下的插件,可通过Nuget安装到VS工程中.安装之后,就可以将项目所依赖的DLL(甚至PDB)文件全

编写高质量代码改善C#程序的157个建议——建议50:在Dispose模式中应区别对待托管资源和非托管资源

建议50:在Dispose模式中应区别对待托管资源和非托管资源 真正资源释放代码的那个虚方法是带一个bool参数的,带这个参数,是因为我们在资源释放时要区别对待托管资源和非托管资源. 提供给调用者调用的显式释放资源的无参Dispose方法中,调用参数是true: public void Dispose() { //必须为true Dispose(true); //省略其他代码 } 这表明,这时候代码要同时处理托管资源和非托管资源. 在供垃圾回收器调用的隐式清理资源的终结器中,调用的是false: