【问题标题】:Using Side-by-Side assemblies to load the x64 or x32 version of a DLL使用 Side-by-Side 程序集加载 x64 或 x32 版本的 DLL
【发布时间】:2010-09-11 15:49:20
【问题描述】:

我们有两个版本的托管 C++ 程序集,一个用于 x86,一个用于 x64。该程序集由为 AnyCPU 编译的 .net 应用程序调用。我们正在通过文件复制安装部署我们的代码,并希望继续这样做。

当应用程序动态选择其处理器架构时,是否可以使用并行程序集清单分别加载 x86 或 x64 程序集?或者是否有其他方法可以在文件复制部署中完成此操作(例如,不使用 GAC)?

【问题讨论】:

    标签: c# .net 64-bit


    【解决方案1】:

    我创建了一个简单的解决方案,它能够从编译为 AnyCPU 的可执行文件中加载特定于平台的程序集。使用的技术可以总结如下:

    1. 确保默认的 .NET 程序集加载机制(“Fusion”引擎)找不到特定于平台的程序集的 x86 或 x64 版本
    2. 在主应用程序尝试加载特定于平台的程序集之前,请在当前 AppDomain 中安装自定义程序集解析器
    3. 现在,当主应用程序需要特定于平台的程序集时,Fusion 引擎将放弃(因为第 1 步)并调用我们的自定义解析器(因为第 2 步);在自定义解析器中,我们确定当前平台并使用基于目录的查找来加载适当的 DLL。

    为了演示这种技术,我附上了一个简短的基于命令行的教程。我在 Windows XP x86 和 Vista SP1 x64 上测试了生成的二进制文件(通过复制二进制文件,就像您的部署一样)。

    注意 1:“csc.exe”是一个 C-sharp 编译器。本教程假设它在您的路径中(我的测试使用“C:\WINDOWS\Microsoft.NET\Framework\v3.5\csc.exe”)

    注意 2:我建议您为测试创建一个临时文件夹并运行当前工作目录设置为该位置的命令行(或 powershell),例如

    (cmd.exe)
    C:
    mkdir \TEMP\CrossPlatformTest
    cd \TEMP\CrossPlatformTest
    

    第 1 步:特定于平台的程序集由一个简单的 C# 类库表示:

    // file 'library.cs' in C:\TEMP\CrossPlatformTest
    namespace Cross.Platform.Library
    {
        public static class Worker
        {
            public static void Run()
            {
                System.Console.WriteLine("Worker is running");
                System.Console.WriteLine("(Enter to continue)");
                System.Console.ReadLine();
            }
        }
    }
    

    第 2 步:我们使用简单的命令行命令编译特定于平台的程序集:

    (cmd.exe from Note 2)
    mkdir platform\x86
    csc /out:platform\x86\library.dll /target:library /platform:x86 library.cs
    mkdir platform\amd64
    csc /out:platform\amd64\library.dll /target:library /platform:x64 library.cs
    

    第三步:主程序分为两部分。 “Bootstrapper”包含可执行文件的主入口点,它在当前应用程序域中注册了一个自定义程序集解析器:

    // file 'bootstrapper.cs' in C:\TEMP\CrossPlatformTest
    namespace Cross.Platform.Program
    {
        public static class Bootstrapper
        {
            public static void Main()
            {
                System.AppDomain.CurrentDomain.AssemblyResolve += CustomResolve;
                App.Run();
            }
    
            private static System.Reflection.Assembly CustomResolve(
                object sender,
                System.ResolveEventArgs args)
            {
                if (args.Name.StartsWith("library"))
                {
                    string fileName = System.IO.Path.GetFullPath(
                        "platform\\"
                        + System.Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")
                        + "\\library.dll");
                    System.Console.WriteLine(fileName);
                    if (System.IO.File.Exists(fileName))
                    {
                        return System.Reflection.Assembly.LoadFile(fileName);
                    }
                }
                return null;
            }
        }
    }
    

    “程序”是应用程序的“真正”实现(注意 App.Run 是在 Bootstrapper.Main 结束时调用的):

    // file 'program.cs' in C:\TEMP\CrossPlatformTest
    namespace Cross.Platform.Program
    {
        public static class App
        {
            public static void Run()
            {
                Cross.Platform.Library.Worker.Run();
            }
        }
    }
    

    第四步:在命令行编译主应用程序:

    (cmd.exe from Note 2)
    csc /reference:platform\x86\library.dll /out:program.exe program.cs bootstrapper.cs
    

    第 5 步:我们现在完成了。我们创建的目录结构应该如下:

    (C:\TEMP\CrossPlatformTest, root dir)
        platform (dir)
            amd64 (dir)
                library.dll
            x86 (dir)
                library.dll
        program.exe
        *.cs (source files)
    

    如果您现在在 32 位平台上运行 program.exe,则会加载 platform\x86\library.dll;如果您在 64 位平台上运行 program.exe,则会加载 platform\amd64\library.dll。请注意,我在 Worker.Run 方法的末尾添加了 Console.ReadLine(),以便您可以使用任务管理器/进程资源管理器来调查加载的 DLL,或者您可以使用 Visual Studio/Windows 调试器附加到进程以查看调用栈等

    当 program.exe 运行时,我们的自定义程序集解析器会附加到当前的 appdomain。一旦 .NET 开始加载 Program 类,它就会看到对“库”程序集的依赖,因此它会尝试加载它。但是,没有找到这样的程序集(因为我们已将其隐藏在 platform/* 子目录中)。幸运的是,我们的自定义解析器知道我们的诡计,并根据当前平台尝试从适当的 platform/* 子目录加载程序集。

    【讨论】:

    • 我们使用类似的方法,但事件附加在静态构造函数中 - 这种方式在某些情况下发生在 .NET 尝试加载另一个程序集之前。
    • 请更新以使用 Environment.Is64BitProcess -- 因为它可能与机器上的 CPU 不同。否则 - 很好回答 - 我们正在使用类似的东西。
    • PROCESSOR_ARCHITECTURE 是正确的 - 它实际上反映的是过程而不是机器。
    • 有谁知道这是否适用于非托管程序集?我似乎无法为非托管程序集触发 AssemblyResolve 事件。
    【解决方案2】:

    我的版本,类似于@Milan,但有几个重要的变化:

    • 适用于所有未找到的 DLL
    • 可以开启和关闭
    • AppDomain.CurrentDomain.SetupInformation.ApplicationBase 用于代替Path.GetFullPath(),因为当前目录可能不同,例如在托管场景中,Excel 可能会加载您的插件,但不会将当前目录设置为您的 DLL。

    • Environment.Is64BitProcess 被用来代替PROCESSOR_ARCHITECTURE,因为我们不应该依赖于操作系统是什么,而是这个进程是如何启动的——它可能是 x64 操作系统上的 x86 进程。在 .NET 4 之前,请改用 IntPtr.Size == 8。

    在某个主类的静态构造函数中调用此代码,该构造函数首先加载。

    public static class MultiplatformDllLoader
    {
        private static bool _isEnabled;
    
        public static bool Enable
        {
            get { return _isEnabled; }
            set
            {
                lock (typeof (MultiplatformDllLoader))
                {
                    if (_isEnabled != value)
                    {
                        if (value)
                            AppDomain.CurrentDomain.AssemblyResolve += Resolver;
                        else
                            AppDomain.CurrentDomain.AssemblyResolve -= Resolver;
                        _isEnabled = value;
                    }
                }
            }
        }
    
        /// Will attempt to load missing assembly from either x86 or x64 subdir
        private static Assembly Resolver(object sender, ResolveEventArgs args)
        {
            string assemblyName = args.Name.Split(new[] {','}, 2)[0] + ".dll";
            string archSpecificPath = Path.Combine(AppDomain.CurrentDomain.SetupInformation.ApplicationBase,
                                                   Environment.Is64BitProcess ? "x64" : "x86",
                                                   assemblyName);
    
            return File.Exists(archSpecificPath)
                       ? Assembly.LoadFile(archSpecificPath)
                       : null;
        }
    }
    

    【讨论】:

    • PROCESSOR_ARCHITECTURE实际上反映的是进程而不是机器;试试Start->Runing %SYSTEMROOT%\SysWOW64\cmd /V:ON /q /c "echo This process is: !PROCESSOR_ARCHITECTURE! && pause"
    • “它可能是 x64 操作系统上的 x86 进程” - 这没有任何意义。当然,唯一决定这一点的是你作为程序员如何选择编译你自己的应用程序?如果你不编译为“任何CPU”,那么首先有什么用?
    • 这是一个图书馆。它可以被不同的应用程序使用。如果创建实际 exe 的人决定不使用“任何 CPU”,我们应该优雅地处理它。因此,根据经验,始终使用进程的位数,而不是主机。假设是邪恶的:)
    • 有谁知道这是否适用于非托管程序集?我似乎无法为非托管程序集触发 AssemblyResolve 事件。
    【解决方案3】:

    看看 SetDllDirectory。我在动态加载 x64 和 x86 的 IBM spss 程序集时使用了它。它还解决了在我的情况下由程序集加载的非程序集支持 dll 的路径是 spss dll 的情况。

    http://msdn.microsoft.com/en-us/library/ms686203%28VS.85%29.aspx

    【讨论】:

    • 使用 Resolver 的解决方案的不同之处在于,在我的 SPSS 案例中,程序集本身包装并加载了包含实际 SPSS 代码的 3 C dll。 C dll 是为 x86 或 x64 编译的(因此 x86 和 x64 程序集都有自己的一组 3 个 C dll)。此处解析器将不起作用(它只会选择正确的包装程序集,但不适用于 dll 的程序集包装)。
    【解决方案4】:

    您可以使用corflags 实用程序强制将 AnyCPU exe 加载为 x86 或 x64 可执行文件,但这并不完全满足文件复制部署要求,除非您根据目标选择要复制的 exe。

    【讨论】:

      【解决方案5】:

      此解决方案也适用于非托管程序集。我创建了一个类似于 Milan Gardian 的出色示例的简单示例。我创建的示例将托管 C++ dll 动态加载到为 Any CPU 平台编译的 C# dll 中。该解决方案利用 InjectModuleInitializer nuget 包在加载程序集的依赖项之前订阅 AssemblyResolve 事件。

      https://github.com/kevin-marshall/Managed.AnyCPU.git

      【讨论】:

        猜你喜欢
        • 2022-12-02
        • 1970-01-01
        • 1970-01-01
        • 2020-03-23
        • 2010-11-27
        • 2014-03-22
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多