44 votes

Équivalent des chargeurs de classes dans .NET

Personne ne sait si il est possible de définir l'équivalent d'un "java class loader personnalisé" dans .NET?

Pour donner un peu de contexte:

Je suis dans le processus de développement d'une nouvelle programmation de la langue que les objectifs de la CLR, appelé "Liberté". Une des caractéristiques de la langue est sa capacité à définir de type "constructeurs", qui sont des méthodes qui sont exécutées par le compilateur au moment de la compilation et de générer des types de sortie. Ils sont en quelque sorte une généralisation des génériques (la langue n'est normale, les génériques), et permettre à ce type de code à écrire (dans "la Liberté" de la syntaxe):

var t as tuple<i as int, j as int, k as int>;
t.i = 2;
t.j = 4;
t.k = 5;

Où "tuple" est défini comme suit:

public type tuple(params variables as VariableDeclaration[]) as TypeDeclaration
{
   //...
}

Dans cet exemple particulier, le constructeur de type "tuple" offre quelque chose de similaire pour les types anonymes en VB et C#.

Cependant, à la différence des types anonymes, "tuples" ont des noms et peut être utilisé à l'intérieur d'publique signatures de méthode.

Cela signifie que j'ai besoin d'un moyen pour le type qui finalement finit par être émis par le compilateur pour être partageable entre plusieurs assemblées. Par exemple, je veux

tuple<x as int> défini à l'Assemblée à la fin du même type que tuple<x as int> défini dans l'Assemblage B.

Le problème avec cela, bien sûr, est que l'Assemblage et de Montage B vont être recueillies à des moments différents, ce qui signifie qu'ils seraient tous les deux finissent par émettant leurs propres versions incompatibles de la tuple de type.

J'ai regardé dans l'aide de une sorte de type "effacement" pour ce faire, de sorte que j'aurais une bibliothèque partagée avec un tas de types comme ça (c'est la "Liberté" de la syntaxe):

class tuple<T>
{
    public Field1 as T;
}

class tuple<T, R>
{
    public Field2 as T;
    public Field2 as R;
}

et puis il suffit de rediriger les accès à partir du i, j, et k n-uplet champs "Champ1", "Champ2", et "Field3".

Cependant ce n'est pas vraiment une option viable. Cela signifie qu'au moment de la compilation, tuple<x as int> et tuple<y as int> finirait par être de différents types, alors que lors de l'exécution en temps, elles seront considérées comme un même type. Que serait la cause de nombreux problèmes pour des choses comme l'égalité et le type d'identité. C'est trop percé d'une abstraction à mon goût.

Une autre option possible serait d'utiliser "état sac des objets". Cependant, à l'aide d'un état sac encontre de l'objectif d'une prise en charge de type "constructeurs" dans la langue. L'idée est de permettre à "la coutume des extensions de langage" pour générer de nouveaux types au moment de la compilation, le compilateur peut faire vérifier le type statique avec.

En Java, ce qui pourrait être fait en utilisant des chargeurs de classe. Fondamentalement, le code qui utilise tuple types pourraient être émis sans réellement définir le type de disque. Une coutume "class loader" pourrait alors être définies pour générer dynamiquement le tuple de type à l'exécution. Qui permettrait de vérifier le type statique à l'intérieur du compilateur, et permettrait d'unifier le tuple types à la compilation des limites.

Malheureusement, cependant, le CLR ne fournit pas de support pour la classe personnalisée de chargement. Le chargement dans le CLR est fait au niveau de l'assemblage. Il serait possible de définir une autre assemblée pour chaque "type construit", mais qui serait très rapidement conduire à des problèmes de performances (ayant de nombreuses assemblées avec un seul type dans entre eux utilisent trop de ressources).

Donc, ce que je veux savoir, c'est:

Est-il possible de simuler quelque chose comme Classe Java Chargeurs de .NET, où je peux émettre une référence à un non-type existant, puis de générer dynamiquement une référence de ce type à l'exécution avant que le code les besoins de l'utilisation qu'il fonctionne?

NOTE:

*En fait je connais déjà la réponse à la question, qui je fournir comme réponse ci-dessous. Cependant, il m'a fallu environ 3 jours de recherche, et un peu de IL le piratage afin de trouver une solution. J'ai pensé que ce serait une bonne idée de les consigner ici au cas où quelqu'un d'autre a couru dans le même problème. *

52voto

Scott Wisniewski Points 14420

La réponse est oui, mais la solution est un peu délicat.

L' System.Reflection.Emit de l'espace de noms définit les types qui permet assemblées être généré dynamiquement. Ils permettent également à l'généré assemblées être défini de manière incrémentale. En d'autres termes, il est possible d'ajouter des types de la dynamique de l'assemblage, l'exécution du code généré, et puis en dernier ajouter plus de types à l'assemblée.

L' System.AppDomain classe définit également un AssemblyResolve événement qui déclenche à chaque fois que le cadre ne parvient pas à charger un assembly. Par l'ajout d'un gestionnaire pour l'événement, il est possible de définir un "runtime" ensemble dans lequel tous les "construits" types sont placés. Le code généré par le compilateur qui utilise un type construit ferait référence à un type à l'exécution de l'assemblée. Parce que le moteur d'exécution de l'assemblée n'existe pas réellement sur le disque, le AssemblyResolve événement serait tiré la première fois le code compilé tenté d'accéder à un type construit. La poignée de l'événement produirait alors la dynamique de l'assemblage et de la renvoyer à la CLR.

Malheureusement, il y a un peu délicat des points pour l'obtention de ce travail. Le premier problème est de s'assurer que le gestionnaire d'événements va toujours être installé avant le code compilé est exécuté. Avec une application de console c'est facile. Le code pour le raccordement au gestionnaire d'événements peuvent être ajoutés à l' Main méthode avant de l'autre code s'exécute. Pour les bibliothèques de classe, cependant, il n'existe pas de méthode main. Une dll peut être chargé dans le cadre d'une application écrite dans une autre langue, il n'est donc pas vraiment possible de supposer qu'il y a toujours une méthode principale disponible pour brancher le code de gestionnaire d'événements.

Le deuxième problème est de s'assurer que l'référencé tous les types d'obtenir insérée dans la dynamique de l'assemblage avant tout code qui fait référence est utilisée. L' System.AppDomain classe définit également un TypeResolve événement qui est exécuté à chaque fois que le CLR est incapable de résoudre un type dans une dynamique de l'assemblage. Il donne le gestionnaire d'événement, l'occasion de définir le type à l'intérieur de la dynamique de l'assemblage avant le code qui l'utilise s'exécute. Toutefois, cela ne fonctionnera pas dans ce cas. Le CLR ne se déclenchera pas le cas pour les assemblys qui sont "statiquement référencé" par d'autres assemblées, même si l'assemblage est défini de façon dynamique. Cela signifie que nous avons besoin d'un moyen d'exécuter du code avant tout autre code dans l'assembly compilé s'exécute et ont de manière dynamique à injecter les types de besoins dans l'exécution de l'assemblée s'ils n'ont pas déjà été défini. Sinon, lorsque le CLR essayé de charger ces types, il remarque que la dynamique de l'assemblage ne contient pas les types dont ils ont besoin et va jeter un type de charge exception.

Heureusement, le CLR offre une solution à ces deux problèmes: Module d'Initialiseurs. Un module d'initialiseur est l'équivalent d'un "statique constructeur de la classe", sauf qu'il initialise un module entier, pas juste une seule classe. Baiscally, le CLR:

  1. Exécuter le module constructeur avant tout les types à l'intérieur du module sont accessibles.
  2. Garantir que seuls les types directement accessibles par le module constructeur sera chargé alors qu'il est en cours d'exécution
  3. Permettent pas de code à l'extérieur du module pour accéder à l'un de ses membres jusqu'à ce que après le constructeur a fini.

Il le fait pour toutes les assemblées, y compris les bibliothèques de classe et de fichiers exécutables, et pour Exe va exécuter le module constructeur avant l'exécution de la méthode main.

Voir ce billet de blog pour plus d'informations sur les constructeurs.

Dans tous les cas, une solution à mon problème nécessite plusieurs morceaux:

  1. La définition de classe suivante, définie à l'intérieur d'un "language runtime dll", qui est référencé par toutes les assemblées produit par le compilateur (c'est le code C#).

    using System;
    using System.Collections.Generic;
    using System.Reflection;
    using System.Reflection.Emit;
    
    namespace SharedLib
    {
        public class Loader
        {
            private Loader(ModuleBuilder dynamicModule)
            {
                m_dynamicModule = dynamicModule;
                m_definedTypes = new HashSet<string>();
            }
    
            private static readonly Loader m_instance;
            private readonly ModuleBuilder m_dynamicModule;
            private readonly HashSet<string> m_definedTypes;
    
            static Loader()
            {
                var name = new AssemblyName("$Runtime");
                var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run);
                var module = assemblyBuilder.DefineDynamicModule("$Runtime");
                m_instance = new Loader(module);
                AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
            }
    
            static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
            {
                if (args.Name == Instance.m_dynamicModule.Assembly.FullName)
                {
                    return Instance.m_dynamicModule.Assembly;
                }
                else
                {
                    return null;
                }
            }
    
            public static Loader Instance
            {
                get
                {
                    return m_instance;
                }
            }
    
            public bool IsDefined(string name)
            {
                return m_definedTypes.Contains(name);
            }
    
            public TypeBuilder DefineType(string name)
            {
                //in a real system we would not expose the type builder.
                //instead a AST for the type would be passed in, and we would just create it.
                var type = m_dynamicModule.DefineType(name, TypeAttributes.Public);
                m_definedTypes.Add(name);
                return type;
            }
        }
    }
    

    La classe définit un singleton qui contient une référence à la dynamique de l'assemblage que les types construits seront créés dans. Il est également titulaire d'un "hash ensemble" qui stocke l'ensemble des types qui ont déjà été générées de façon dynamique, et, enfin, définit un membre qui peut être utilisé pour définir le type. Cet exemple renvoie simplement un Système.De la réflexion.En émettent.TypeBuilder instance qui peut ensuite être utilisé pour définir la classe générée. Dans un système réel, la méthode serait probablement prendre dans un AST de représentation de la classe et de la génération, c'est l'auto.

  2. Compilé assemblées qui émettent les deux références suivantes (montré dans ILASM syntaxe):

    .assembly extern $Runtime
    {
        .ver 0:0:0:0
    }
    .assembly extern SharedLib
    {
        .ver 1:0:0:0
    }
    

    Ici "SharedLib" est la Langue prédéfinie de la bibliothèque d'exécution qui comprend le "Loader" de la classe définies ci-dessus et "$Runtime" est la dynamique d'exécution de l'assemblée que le consructed types seront insérés dans.

  3. Un module "constructeur" à l'intérieur de chaque assemblée compilé dans la langue.

    Autant que je sache, il n'y a pas .NET languages qui permettent Module Constructeurs être défini dans la source. Le C++ /CLI compilateur est le seul compilateur, je sais de qui les génère. Dans IL, ils ressemblent à ceci, directement défini dans le module et non pas à l'intérieur de n'importe quel type de définitions:

    .method privatescope specialname rtspecialname static 
            void  .cctor() cil managed
    {
        //generate any constructed types dynamically here...
    }
    

    Pour moi, Ce n'est pas un problème que j'ai à écrire personnalisé IL, à obtenir que cela fonctionne. Je suis en train d'écrire un compilateur, donc la génération de code n'est pas un problème.

    Dans le cas d'une assemblée qui a utilisé les types de tuple<i as int, j as int> et tuple<x as double, y as double, z as double> le module constructeur besoin de générer des types comme suit (ici dans la syntaxe C#):

    class Tuple_i_j<T, R>
    {
        public T i;
        public R j;
    }
    
    class Tuple_x_y_z<T, R, S>
    {
        public T x;
        public R y;
        public S z;
    }
    

    Le tuple classes sont générées que les types génériques de contourner les problèmes d'accessibilité. Qui permettrait de code dans l'assembly compilé pour utiliser tuple<x as Foo>, où Foo est certains non-public type.

    Le corps du module constructeur qui a fait ceci (ici seulement montrant un type, et écrit en syntaxe C#) devrait ressembler à ceci:

    var loader = SharedLib.Loader.Instance;
    lock (loader)
    {
        if (! loader.IsDefined("$Tuple_i_j"))
        {
            //create the type.
            var Tuple_i_j = loader.DefineType("$Tuple_i_j");
            //define the generic parameters <T,R>
           var genericParams = Tuple_i_j.DefineGenericParameters("T", "R");
           var T = genericParams[0];
           var R = genericParams[1];
           //define the field i
           var fieldX = Tuple_i_j.DefineField("i", T, FieldAttributes.Public);
           //define the field j
           var fieldY = Tuple_i_j.DefineField("j", R, FieldAttributes.Public);
           //create the default constructor.
           var constructor= Tuple_i_j.DefineDefaultConstructor(MethodAttributes.Public);
    
           //"close" the type so that it can be used by executing code.
           Tuple_i_j.CreateType();
        }
    }
    

Donc dans tous les cas, ce fut le mécanisme, j'ai pu trouver pour permettre l'équivalent de la classe personnalisée chargeurs dans le CLR.

Personne ne sait d'une façon plus facile de faire cela?

-5voto

Kevin Dostalek Points 483

Je pense que c'est le genre de chose que le DLR est supposé fournir en C # 4.0. Difficile de trouver des informations pour le moment, mais nous en apprendrons peut-être plus à PDC08. J'attends avec impatience de voir votre solution C # 3 ... Je suppose qu'elle utilise des types anonymes.

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