我们有两个版本的托管C++程序集,一个用于x86,另一个用于x64.此程序集由符合AnyCPU的.net应用程序调用.我们正在通过文件复制安装部署我们的代码,并希望继续这样做.
当应用程序动态选择其处理器体系结构时,是否可以使用并排程序集清单分别加载x86或x64程序集?或者是否有另一种方法可以在文件复制部署中完成此操作(例如,不使用GAC)?
我创建了一个简单的解决方案,能够从编译为AnyCPU的可执行文件加载特定于平台的程序集.使用的技术可归纳如下:
确保默认的.NET程序集加载机制("Fusion"引擎)找不到特定于平台的程序集的x86或x64版本
在主应用程序尝试加载特定于平台的程序集之前,请在当前AppDomain中安装自定义程序集解析程序
现在当主应用程序需要特定于平台的程序集时,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
第3步:主程序分为两部分."Bootstrapper"包含可执行文件的主入口点,它在当前appdomain中注册自定义程序集解析程序:
// 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; } } }
"Program"是应用程序的"真实"实现(请注意,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(); } } }
第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调试程序附加到进程以查看调用栈等
运行program.exe时,我们的自定义程序集解析程序将附加到当前的appdomain.一旦.NET开始加载Program类,它就会看到对'library'程序集的依赖,所以它会尝试加载它.但是,没有找到这样的程序集(因为我们将它隐藏在platform/*子目录中).幸运的是,我们的自定义解析器知道我们的技巧,并且基于当前平台,它尝试从适当的平台/*子目录加载程序集.
我的版本,类似于@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; } }