Использование параллельных сборок для загрузки x64 или x32 версии DLL

У нас есть две версии управляемой сборки C++, одна для x86 и одна для x64. Эта сборка вызывается приложением.net, совместимым для AnyCPU. Мы внедряем наш код с помощью установки копии файла и хотели бы продолжить это делать.

Можно ли использовать манифест сборки Side-by-Side для загрузки сборки x86 или x64 соответственно, когда приложение динамически выбирает архитектуру своего процессора? Или есть другой способ сделать это в развертывании копии файла (например, не используя GAC)?

5 ответов

Решение

Я создал простое решение, способное загружать сборку для конкретной платформы из исполняемого файла, скомпилированного как AnyCPU. Используемая техника может быть кратко изложена следующим образом:

  1. Убедитесь, что механизм загрузки сборки.NET по умолчанию (механизм "Fusion") не может найти версию сборки для платформы x86 или x64
  2. Перед тем, как основное приложение попытается загрузить сборку для конкретной платформы, установите пользовательский распознаватель сборок в текущем домене приложений.
  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

Шаг 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 до загрузки зависимостей сборки.

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

Другие вопросы по тегам