Использование параллельных сборок для загрузки x64 или x32 версии DLL
У нас есть две версии управляемой сборки C++, одна для x86 и одна для x64. Эта сборка вызывается приложением.net, совместимым для AnyCPU. Мы внедряем наш код с помощью установки копии файла и хотели бы продолжить это делать.
Можно ли использовать манифест сборки Side-by-Side для загрузки сборки x86 или x64 соответственно, когда приложение динамически выбирает архитектуру своего процессора? Или есть другой способ сделать это в развертывании копии файла (например, не используя GAC)?
5 ответов
Я создал простое решение, способное загружать сборку для конкретной платформы из исполняемого файла, скомпилированного как AnyCPU. Используемая техника может быть кратко изложена следующим образом:
- Убедитесь, что механизм загрузки сборки.NET по умолчанию (механизм "Fusion") не может найти версию сборки для платформы x86 или x64
- Перед тем, как основное приложение попытается загрузить сборку для конкретной платформы, установите пользовательский распознаватель сборок в текущем домене приложений.
- Теперь, когда основному приложению требуется сборка для конкретной платформы, механизм 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" содержит основную точку входа для исполняемого файла и регистрирует пользовательский преобразователь сборок в текущем домене приложения:
// 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();
}
}
}
Шаг 4: Скомпилируйте основное приложение в командной строке:
(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)
Если вы теперь запустите program.exe на 32-битной платформе, будет загружена платформа \x86\library.dll; если вы запустите program.exe на 64-битной платформе, будет загружена платформа \amd64\library.dll. Обратите внимание, что я добавил Console.ReadLine() в конце метода Worker.Run, чтобы вы могли использовать диспетчер задач / проводник процесса для исследования загруженных библиотек DLL или использовать отладчик Visual Studio/Windows для подключения к процессу, чтобы увидеть стек вызовов и т. д.
При запуске program.exe наш пользовательский распознаватель сборок присоединяется к текущему домену приложения. Как только.NET начинает загружать класс Program, он видит зависимость от сборки библиотеки, поэтому он пытается загрузить ее. Однако, такая сборка не найдена (потому что мы скрыли ее в подкаталогах platform/*). К счастью, наш пользовательский распознаватель знает нашу хитрость и на основе текущей платформы пытается загрузить сборку из соответствующей подкаталога platform/*.
Моя версия похожа на @Milan, но с несколькими важными изменениями:
- Работает для ВСЕХ DLL, которые не были найдены
- Может быть включен и выключен
AppDomain.CurrentDomain.SetupInformation.ApplicationBase
используется вместоPath.GetFullPath()
поскольку текущий каталог может быть другим, например, в сценариях хостинга, Excel может загрузить ваш плагин, но текущий каталог не будет установлен на вашу DLL.Environment.Is64BitProcess
используется вместоPROCESSOR_ARCHITECTURE
, поскольку мы не должны зависеть от того, что это за ОС, а от того, как этот процесс был запущен - это мог быть процесс x86 на ОС x64. До.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;
}
}
Посмотрите на SetDllDirectory. Я использовал его для динамической загрузки сборки IBM spss для x64 и x86. Это также решило пути для несборочных библиотек поддержки DLL, загруженных сборками, в моем случае это был случай с spss dll.
http://msdn.microsoft.com/en-us/library/ms686203%28VS.85%29.aspx
Вы можете использовать утилиту corflags, чтобы принудительно загрузить exe- файл AnyCPU в качестве исполняемого файла x86 или x64, но это не полностью соответствует требованию развертывания копии файла, если только вы не выберете, какой exe-файл копировать в зависимости от цели.
Это решение может работать и для неуправляемых сборок. Я создал простой пример, подобный великому примеру Милана Гардиана. Пример, который я создал, динамически загружает dll Managed C++ в dll C#, скомпилированную для платформы Any CPU. В решении используется пакет nuget InjectModuleInitializer для подписки на событие AssemblyResolve до загрузки зависимостей сборки.