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

一、自定义托管 dll 程序集的查找位置

目前(.Net4.7)能用的有2种:

#define DEFAULT_IMPLEMENT
//#define DEFAULT_IMPLEMENT2
//#define HACK_UPDATECONTEXTPROPERTY

namespace X.Utility
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using X.Linq;
    using X.Reflection;

    public static partial class AppUtil
    {
        #region Common Parts
#if DEFAULT_IMPLEMENT || DEFAULT_IMPLEMENT2
        public static string AssemblyExtension { get; set; } = "dll";
        private static IEnumerable<Tuple<AssemblyName, string>> ScanDirs(IList<string> dirNames)
            => (0 == dirNames.Count ? new[] { "dlls" } : dirNames)
            .SelectMany(dir => Directory
            .GetFiles(Path.IsPathRooted(dir) ? dir : AppExeDir + dir, "*." + AssemblyExtension)
            .SelectIfCalc(f => f.GetLoadableAssemblyName(), a => null != a, (f, a) => Tuple.Create(a, f))
            );
        private static Assembly LoadAssemblyFromList(AssemblyName an, IEnumerable<Tuple<AssemblyName, string>> al)
        {
            foreach (var a in al.Where(aa => aa.Item1.Name == an.Name && aa.Item1.Version == an.Version && aa.Item1.CultureName == an.CultureName))
                return LoadAssembly(a.Item2);
            foreach (var a in al.Where(aa => aa.Item1.Name == an.Name && aa.Item1.Version == an.Version))
                return LoadAssembly(a.Item2);

            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))
                return LoadAssembly(a.Item2);
            foreach (var a in al.Where(aa => aa.Item1.Name == an.Name && aa.Item1.Version > an.Version).OrderBy(aa => aa.Item1.Version))
                return LoadAssembly(a.Item2);

            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))
                return LoadAssembly(a.Item2);
            foreach (var a in al.Where(aa => aa.Item1.Name == an.Name && aa.Item1.Version < an.Version).OrderByDescending(aa => aa.Item1.Version))
                return LoadAssembly(a.Item2);

            return null;
        }
        private static Assembly LoadAssembly(string path)
            => Assembly.Load(File.ReadAllBytes(path));
#endif
        #endregion

        #region DEFAULT_IMPLEMENT
#if DEFAULT_IMPLEMENT
        private static IEnumerable<Tuple<AssemblyName, string>> dlls;
        /// <summary>
        /// 以调用该方法时的目录状态为准,如果在调用方法之后目录或其内dll文件发生了变化,将导致加载失败。
        /// 不传入任何参数则默认为 dlls 子目录。
        /// </summary>
        /// <param name="dirNames">相对路径将从入口exe所在目录展开为完整路径</param>
        public static void SetPrivateBinPath(params string[] dirNames)
        {
            if (null != dlls) return;
            dlls = ScanDirs(dirNames);
            AppDomain.CurrentDomain.AssemblyResolve += AppDomain_AssemblyResolve_DEFAULT_IMPLEMENT;
        }
        private static Assembly AppDomain_AssemblyResolve_DEFAULT_IMPLEMENT(object sender, ResolveEventArgs args)
            => LoadAssemblyFromList(new AssemblyName(args.Name), dlls);
#endif
        #endregion

        #region DEFAULT_IMPLEMENT2
#if DEFAULT_IMPLEMENT2
        public static List<string> PrivateDllDirs { get; } = new List<string> { "dlls" };
        private static bool enablePrivateDllDirs;
        public static bool EnablePrivateDllDirs
        {
            get => enablePrivateDllDirs;
            set
            {
                if (value == enablePrivateDllDirs) return;
                if (value) AppDomain.CurrentDomain.AssemblyResolve += AppDomain_AssemblyResolve_DEFAULT_IMPLEMENT2;
                else AppDomain.CurrentDomain.AssemblyResolve -= AppDomain_AssemblyResolve_DEFAULT_IMPLEMENT2;
                enablePrivateDllDirs = value;
            }
        }
        private static Assembly AppDomain_AssemblyResolve_DEFAULT_IMPLEMENT2(object sender, ResolveEventArgs args)
            => LoadAssemblyFromList(new AssemblyName(args.Name), ScanDirs(PrivateDllDirs));
#endif
        #endregion

        #region HACK_UPDATECONTEXTPROPERTY
#if HACK_UPDATECONTEXTPROPERTY
        public static void SetPrivateBinPathHack2(params string[] dirNames)
        {
            const string privateBinPathKeyName = "PrivateBinPathKey";
            const string methodName_UpdateContextProperty = "UpdateContextProperty";
            const string methodName_GetFusionContext = "GetFusionContext";

            for (var i = 0; i < dirNames.Length; ++i)
                if (!Path.IsPathRooted(dirNames[i]))
                    dirNames[i] = AppExeDir + dirNames[i];

            var privateBinDirectories = string.Join(";", dirNames);
            var curApp = AppDomain.CurrentDomain;
            var appDomainType = typeof(AppDomain);
            var appDomainSetupType = typeof(AppDomainSetup);
            var privateBinPathKey = appDomainSetupType
                .GetProperty(privateBinPathKeyName, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.GetProperty)
                .GetValue(null)
                .ToString();
            curApp.SetData(privateBinPathKey, privateBinDirectories);
            appDomainSetupType
                .GetMethod(methodName_UpdateContextProperty, BindingFlags.NonPublic | BindingFlags.Static)
                .Invoke(null, new[]
                {
                    appDomainType
                        .GetMethod(methodName_GetFusionContext, BindingFlags.NonPublic | BindingFlags.Instance)
                        .Invoke(curApp, null),
                    privateBinPathKey,
                    privateBinDirectories
                });
        }
#endif
        #endregion
    }
}
  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 查找位置

如果只需要一个自定义目录:

namespace X.Utility
{
    using System;
    using System.IO;
    using System.Runtime.InteropServices;

    public static partial class AppUtil
    {
        [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
        private static extern bool SetDllDirectory(string dir);

        public static void Set64Or32BitDllDir(string x64DirName = @"dllsx64", string x86DirName = @"dllsx86")
        {
            var dir = IntPtr.Size == 8 ? x64DirName : x86DirName;
            if (!Path.IsPathRooted(dir)) dir = AppEntryExeDir + dir;
            if (!SetDllDirectory(dir))
                throw new System.ComponentModel.Win32Exception(nameof(SetDllDirectory));
        }
    }
}

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

//#define ALLOW_REMOVE_DLL_DIRS

namespace X.Utility
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Runtime.InteropServices;

    public static partial class AppUtil
    {
        [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
        private static extern bool SetDefaultDllDirectories(int flags = 0x1E00);
        [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
        private static extern IntPtr AddDllDirectory(string dir);
#if ALLOW_REMOVE_DLL_DIRS
        [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
        private static extern bool RemoveDllDirectory(IntPtr cookie);

        public static Dictionary<string, IntPtr> DllDirs { get; } = new Dictionary<string, IntPtr>();
#endif

        public static readonly string[] x64DefaultDllDirs = new[] { @"dllsx64" };
        public static readonly string[] x86DefaultDllDirs = new[] { @"dllsx86" };

        public static void Set64Or32BitDllDirs(IEnumerable<string> x64DirNames, IEnumerable<string> x86DirNames)
        {
            if (null == x64DirNames && null == x86DirNames)
                throw new ArgumentNullException($"Must set at least one of {nameof(x64DirNames)} or {nameof(x86DirNames)}");

            if (!SetDefaultDllDirectories())
                throw new System.ComponentModel.Win32Exception(nameof(SetDefaultDllDirectories));

            AddDllDirs(IntPtr.Size == 8 ? x64DirNames ?? x64DefaultDllDirs : x86DirNames ?? x86DefaultDllDirs);
        }

        public static void AddDllDirs(IEnumerable<string> dirNames)
        {
            foreach (var dn in dirNames)
            {
                var dir = Path.IsPathRooted(dn) ? dn : AppExeDir + dn;
#if ALLOW_REMOVE_DLL_DIRS
                if (!DllDirs.ContainsKey(dir))
                    DllDirs[dir] =
#endif
                AddDllDirectory(dir);
            }
        }
        public static void AddDllDirs(params string[] dirNames) => AddDllDirs(dirNames);

#if ALLOW_REMOVE_DLL_DIRS
        public static void RemoveDllDirs(IEnumerable<string> dirNames)
        {
            foreach (var dn in dirNames)
            {
                var dir = Path.IsPathRooted(dn) ? dn : AppExeDir + dn;
                if (DllDirs.TryGetValue(dir, out IntPtr cookie))
                    RemoveDllDirectory(cookie);
            }
        }
        public static void RemoveDllDirs(params string[] dirNames) => RemoveDllDirs(dirNames);
#endif
    }
}

 [DefaultDllImportSearchPaths(DllImportSearchPath.UseDllDirectoryForDependencies)] 建议加上此标签

针对非托管 dll 自定义查找路径是用 Windows 原生 API 提供的功能来完成。

#define ALLOW_REMOVE_DLL_DIRS //取消这行注释可以打开【移除自定义查找路径】的功能

三、比较重要的是用法

public partial class App
{
    static App()
    {
        AppUtil.SetPrivateBinPath();
        AppUtil.Set64Or32BitDllDir();
    }
    [STAThread]
    public static void Main()
    {
        //do something...
    }
}

最合适的地方是放在【启动类】的【静态构造】函数里面,这样可以保证在进入 Main 入口点之前已经设置好了自定义的 dll 查找目录。

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

  1. 检测 dll 程序集是否可加载到当前进程
namespace X.Reflection
{
    using System;
    using System.Reflection;

    public static partial class ReflectionX
    {
        private static readonly ProcessorArchitecture CurrentProcessorArchitecture = IntPtr.Size == 8 ? ProcessorArchitecture.Amd64 : ProcessorArchitecture.X86;
        public static AssemblyName GetLoadableAssemblyName(this string dllPath)
        {
            try
            {
                var an = AssemblyName.GetAssemblyName(dllPath);
                switch (an.ProcessorArchitecture)
                {
                    case ProcessorArchitecture.MSIL: return an;
                    case ProcessorArchitecture.Amd64:
                    case ProcessorArchitecture.X86: return CurrentProcessorArchitecture == an.ProcessorArchitecture ? an : null;
                }
            }
            catch { }
            return null;
        }
    }
}

当前 exe 路径和目录

namespace X.Utility
{
    using System;
    using System.IO;
    using System.Reflection;
    public static partial class AppUtil
    {
        public static string AppExePath { get; } = Assembly.GetEntryAssembly().Location;
        public static string AppExeDir { get; } = Path.GetDirectoryName(AppExePath) + Path.DirectorySeparatorChar;

#if DEBUG
        public static string AppExePath1 { get; } = Path.GetFullPath(Assembly.GetEntryAssembly().CodeBase.Substring(8));
        public static string AppExeDir1 { get; } = AppDomain.CurrentDomain.BaseDirectory;
        public static string AppExeDir2 { get; } = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;

        static AppUtil()
        {
            System.Diagnostics.Debug.Assert(AppExePath == AppExePath1);
            System.Diagnostics.Debug.Assert(AppExeDir == AppExeDir1);
            System.Diagnostics.Debug.Assert(AppExeDir1 == AppExeDir2);
        }
#endif
    }
}
原文地址:https://www.cnblogs.com/MuNet/p/11492320.html