web-dev-qa-db-fra.com

Utilisation d'assemblages côte à côte pour charger la version x64 ou x32 d'un DLL

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

Est-il possible d'utiliser un manifeste d'assemblage côte à côte pour charger un assemblage x86 ou x64 respectivement lorsqu'une application sélectionne dynamiquement son architecture de processeur? Ou existe-t-il une autre façon de procéder dans un déploiement de copie de fichiers (par exemple, sans utiliser le GAC)?

58
Adam L

J'ai créé une solution simple qui est capable de charger l'assembly spécifique à la plate-forme à 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 par défaut de l'assembly .NET (moteur "Fusion") ne trouve pas la version x86 ou x64 de l'assembly spécifique à la plate-forme
  2. Avant que l'application principale ne tente de charger l'assembly spécifique à la plate-forme, installez un résolveur d'assembly personnalisé dans le domaine d'application actuel
  3. Maintenant, lorsque l'application principale a besoin de l'assemblage spécifique à la plate-forme, le moteur Fusion abandonnera (à cause de l'étape 1) et appellera 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 basée sur l'annuaire pour charger la DLL appropriée.

Pour illustrer cette technique, je joins un court didacticiel en ligne de commande. J'ai testé les binaires résultants sous Windows XP x86 puis 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 se trouve sur 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 d'exécuter la ligne de commande (ou powershell) dont le répertoire de travail actuel est défini à cet emplacement, par ex.

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

Étape 1: l'assembly 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 assemblys spécifiques à la plate-forme à l'aide 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: Le programme principal est divisé en deux parties. "Bootstrapper" contient le point d'entrée principal de l'exécutable et il 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 la "vraie" implémentation 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: Compilez 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éé doit ê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, platform\x86\library.dll sera chargé; si vous exécutez program.exe sur une plate-forme 64 bits, platform\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/l'Explorateur de processus pour enquêter sur les DLL chargées, ou vous pouvez utiliser Visual Studio/Windows Debugger pour attacher au processus pour voir le 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 à l'assembly "bibliothèque", il essaie donc de le charger. Cependant, aucun tel assembly n'est trouvé (car nous l'avons caché dans les sous-répertoires platform/*). Heureusement, notre résolveur personnalisé connaît notre ruse et en fonction de la plate-forme actuelle, il essaie de charger l'assembly à partir du sous-répertoire platform/* approprié.

63
Milan Gardian

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

  • Fonctionne pour TOUTES les DLL introuvables
  • Peut être allumé et éteint
  • AppDomain.CurrentDomain.SetupInformation.ApplicationBase Est utilisé à la place de Path.GetFullPath() car le répertoire courant peut être différent, par ex. dans les scénarios d'hébergement, Excel peut charger votre plugin mais le répertoire actuel ne sera pas défini sur votre DLL.

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

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

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

Jetez un œil à SetDllDirectory. Je l'ai utilisé pour le chargement dynamique d'un assemblage IBM SPSS pour x64 et x86. Il a également résolu des chemins pour les dll de support non Assembly chargés par les assemblys dans mon cas était le cas avec les dll spss.

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

3
wvd_vegt

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

2
Rob Walker

Cette solution peut également fonctionner pour les assemblys non gérés. J'ai créé un exemple simple similaire au grand exemple de Milan Gardian. L'exemple que j'ai créé charge dynamiquement une DLL C++ gérée dans une DLL C # compilée pour la plate-forme Any CPU. La solution utilise le package de nuget InjectModuleInitializer pour s'abonner à l'événement AssemblyResolve avant le chargement des dépendances de l'assembly.

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

1
Kevin Marshall