使用并行程序集加载DLL的x64或x32版本

61
我们有两个版本的托管C ++程序集,一个用于x86,另一个用于x64。该程序集由编译为AnyCPU的.NET应用程序调用。我们通过文件复制安装来部署我们的代码,并希望继续这样做。
在动态选择处理器架构时,是否可以使用“并排装配清单”分别加载x86或x64程序集?或者,在文件复制部署中是否有其他方法可以完成此操作(例如不使用GAC)?
5个回答

67
我创建了一个简单的解决方案,能够从编译为 AnyCPU 的可执行文件中加载特定于平台的程序集。所使用的技术可以概括如下:
  1. 确保默认的 .NET 程序集加载机制(“Fusion” 引擎)无法找到特定于 x86 或 x64 平台的程序集。
  2. 在主应用程序尝试加载特定于平台的程序集之前,在当前 AppDomain 中安装自定义程序集解析器。
  3. 现在,当主应用程序需要特定于平台的程序集时,由于步骤 1,Fusion 引擎将放弃查找,并调用我们的自定义解析器(由于步骤 2)。在自定义解析器中,我们确定当前平台并使用基于目录的搜索来加载适当的 DLL。
为了演示这种技术,我附上一个简短的基于命令行的教程。我在 Windows XP x86 上测试了生成的二进制文件,然后在 Vista SP1 x64 上测试了它们(通过复制二进制文件,就像您部署一样)。 注意 1:“csc.exe” 是 C# 编译器。本教程假设它在您的路径中(我的测试使用的是“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

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

// 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;
        }
    }
}

“程序”是应用程序的“真正”实现(请注意,在Bootstrapper.Main的末尾调用了App.Run):

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

步骤4:在命令行上编译主应用程序:

(cmd.exe from Note 2)
csc /reference:platform\x86\library.dll /out:program.exe program.cs bootstrapper.cs
第五步:我们现在完成了。我们创建的目录结构应该如下所示:
(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 Debugger附加到进程并查看调用堆栈等。

当运行program.exe时,我们的自定义程序集解析器被附加到当前应用程序域。 .NET一开始加载Program类时,发现对'library'程序集的依赖关系,因此尝试加载它。但是,找不到这样的程序集(因为我们已将其隐藏在platform/*子目录中)。幸运的是,我们的自定义解析器知道我们的诡计,并根据当前平台尝试从适当的platform/*子目录加载程序集。


1
我们使用类似的方法,但事件是附加在静态构造函数中 - 这样,在某些情况下,附加会在.NET尝试加载另一个程序集之前发生。 - Yuri Astrakhan
5
请更新代码,使用Environment.Is64BitProcess来检查进程是否为64位,因为这可能与计算机上的CPU不同。否则,回答得非常好,我们正在使用类似的方法。 - Yuri Astrakhan
1
PROCESSOR_ARCHITECTURE是正确的 - 它实际反映了进程而不是机器。 - Fowl
有人知道这是否适用于非托管程序集吗?我似乎无法让“AssemblyResolve”事件对非托管程序集触发。 - 9ee1

24

我的版本类似于@Milan的版本,但有一些重要的更改:

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

  • 使用Environment.Is64BitProcess而不是PROCESSOR_ARCHITECTURE,因为我们不应该依赖于操作系统是什么,而是依赖于此进程如何启动-它可能是在x64 OS上的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;
    }
}

1
PROCESSOR_ARCHITECTURE 实际上反映的是进程而不是机器;尝试 Start->Run %SYSTEMROOT%\SysWOW64\cmd /V:ON /q /c "echo This process is: !PROCESSOR_ARCHITECTURE! && pause" - Fowl
“它可能是在x64操作系统上运行的x86进程” - 这没有意义。显然,唯一确定这一点的是作为程序员如何选择编译自己的应用程序?如果不编译为“任何CPU”,那么首先使用有什么用呢? - Nyerguds
6
这是一个库,可以被不同的应用程序使用。如果创建实际exe的人决定不使用“任何CPU”,我们应该优雅地处理它。因此,一般来说,始终使用进程的位数而不是主机的位数。假设是有害的 :) - Yuri Astrakhan
有人知道这是否适用于非托管程序集吗?我似乎无法让“AssemblyResolve”事件对非托管程序集触发。 - 9ee1

4

与使用Resolver的解决方案不同的是,在我的SPSS案例中,汇编本身包装并加载了3个C dll,其中包含实际的SPSS代码。这些C dll是为x86或x64编译的(因此,x86和x64汇编都有自己的一组3个C dll)。在这种情况下,Resolver将无法工作(它只会选择正确的包装程序集,但无法处理程序集包装的dll)。 - wvd_vegt

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

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接