59 votes

Utilisation d'assemblages Side-by-Side pour charger la version x64 ou x32 d'une DLL

Nous avons deux versions d'un assemblage C++ géré, une pour x86 et une pour x64. Cet assemblage est appelé par une application .net compilée pour AnyCPU. Nous déployons notre code via une installation par copie de fichier, et nous aimerions continuer à le faire.

Est-il possible d'utiliser un manifeste d'assemblage Side-by-Side pour charger respectivement un assemblage x86 ou x64 lorsqu'une application choisit dynamiquement l'architecture de son processeur ? Ou existe-t-il un autre moyen d'y parvenir dans le cadre d'un déploiement par copie de fichier (par exemple, sans utiliser le GAC) ?

63voto

Milan Gardian Points 6596

J'ai créé une solution simple qui est capable de charger un assemblage spécifique à une plateforme à partir d'un exécutable compilé en AnyCPU. La technique utilisée peut être résumée comme suit :

  1. Assurez-vous que le mécanisme de chargement d'assemblage .NET par défaut (moteur "Fusion") ne trouve pas la version x86 ou x64 de l'assemblage spécifique à la plate-forme.
  2. Avant que l'application principale ne tente de charger l'assemblage spécifique à la plate-forme, installez un résolveur d'assemblage personnalisé dans l'AppDomain actuel.
  3. Maintenant, lorsque l'application principale a besoin de l'assemblage spécifique à la plate-forme, le moteur Fusion abandonne (à cause de l'étape 1) et appelle notre résolveur personnalisé (à cause de l'étape 2) ; dans le résolveur personnalisé, nous déterminons la plate-forme actuelle et utilisons la recherche par répertoire pour charger la DLL appropriée.

Pour démontrer cette technique, je joins un court tutoriel basé sur la ligne de commande. J'ai testé les binaires résultants sur Windows XP x86 et ensuite Vista SP1 x64 (en copiant les binaires, tout comme votre déploiement).

Note 1 : "csc.exe" est un compilateur C-sharp. Ce tutoriel suppose qu'il est dans votre chemin (mes tests utilisaient " C:\WINDOWS\Microsoft.NET\Framework\v3.5\csc.exe ")

Note 2 : Je vous recommande de créer un dossier temporaire pour les tests et de lancer la ligne de commande (ou powershell) dont le répertoire de travail courant est défini à cet emplacement, par exemple.

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

Étape 1 : L'assemblage spécifique à la plateforme est représenté par une simple bibliothèque de classes 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();
        }
    }
}

Étape 2 : Nous compilons des assemblages spécifiques à une plateforme en utilisant de simples commandes en ligne de commande :

(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

Étape 3 : Le programme principal est divisé en deux parties. Le "Bootstrapper" contient le point d'entrée principal de l'exécutable et enregistre un résolveur d'assemblage personnalisé dans le domaine d'application actuel :

// 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" est l'implémentation "réelle" de l'application (notez que App.Run a été invoqué à la fin de 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();
        }
    }
}

Étape 4 : Compiler l'application principale en ligne de commande :

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

Étape 5 : Nous avons maintenant terminé. La structure du répertoire que nous avons créé devrait être la suivante :

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

Si vous exécutez maintenant program.exe sur une plate-forme 32 bits, la plate-forme \x86\library.dll sera chargé ; si vous exécutez program.exe sur une plateforme 64 bits, la plateforme \amd64\library.dll sera chargé. Notez que j'ai ajouté Console.ReadLine() à la fin de la méthode Worker.Run afin que vous puissiez utiliser le gestionnaire de tâches/explorateur de processus pour examiner les DLL chargées, ou que vous puissiez utiliser Visual Studio/Windows Debugger pour vous attacher au processus et voir la pile d'appels, etc.

Lorsque program.exe est exécuté, notre résolveur d'assemblage personnalisé est attaché au domaine d'application actuel. Dès que .NET commence à charger la classe Program, il voit une dépendance sur l'assemblage 'library', donc il essaie de le charger. Cependant, aucun assemblage de ce type n'est trouvé (parce que nous l'avons caché dans les sous-répertoires platform/*). Heureusement, notre résolveur personnalisé connaît notre astuce et, en fonction de la plate-forme actuelle, il essaie de charger l'assemblage à partir du sous-répertoire platform/* approprié.

23voto

Yurik Points 2005

Ma version, similaire à celle de @Milan, mais avec plusieurs changements importants :

  • Fonctionne pour TOUTES les DLLs qui n'ont pas été trouvées

  • Peut être activé et désactivé

  • AppDomain.CurrentDomain.SetupInformation.ApplicationBase est utilisé à la place de Path.GetFullPath() car le répertoire courant peut être différent, par exemple dans les scénarios d'hébergement, Excel peut charger votre plugin mais le répertoire courant ne sera pas défini sur votre DLL.

  • Environment.Is64BitProcess est utilisé à la place de PROCESSOR_ARCHITECTURE Nous ne devrions pas dépendre du système d'exploitation, mais plutôt de la façon dont le processus a été lancé - il pourrait s'agir d'un processus x86 sur un système x64. Avant .NET 4, utilisez IntPtr.Size == 8 à la place.

Appelez ce code dans un constructeur statique d'une classe principale qui est chargée avant tout le reste.

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

3voto

wvd_vegt Points 121

Jetez un coup d'œil à SetDllDirectory. Je l'ai utilisé pour le chargement dynamique d'un assemblage IBM spss pour x64 et x86. Il a également résolu les problèmes de chemins d'accès pour les dll non supportées par les assemblages, comme c'était le cas pour les dll de spss.

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

2voto

Rob Walker Points 25840

Vous pouvez utiliser le corflags pour forcer un exe AnyCPU à se charger en tant qu'exécutable x86 ou x64, mais cela ne répond pas totalement à l'exigence de déploiement de la copie de fichier, sauf si vous choisissez l'exe à copier en fonction de la cible.

1voto

Kevin Marshall Points 79

Cette solution peut également fonctionner pour les assemblages non gérés. J'ai créé un exemple simple similaire à l'excellent exemple de Milan Gardian. L'exemple que j'ai créé charge dynamiquement une dll Managed C++ dans une dll C# compilée pour la plateforme Any CPU. La solution utilise le paquet nuget InjectModuleInitializer pour souscrire à l'événement AssemblyResolve avant que les dépendances de l'assemblage ne soient chargées.

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X