Équivalent des chargeurs de classe in.NET

Est-ce que quelqu'un sait s'il est possible de définir l'équivalent d'un "java custom class loader" dans. net?

Pour donner un peu de contexte:

Je suis en train de développer un nouveau langage de programmation qui cible le CLR, appelé "Liberty". L'une des caractéristiques du langage est sa capacité à définir des "constructeurs de type", qui sont des méthodes qui sont exécutées par le compilateur au moment de la compilation et génèrent des types en sortie. Ils sont en quelque sorte une généralisation de generics (le langage contient des génériques normaux), et permet d'écrire du code comme celui-ci (dans la syntaxe" Liberty"):

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 fournit quelque chose de similaire aux types anonymes dans VB et C#.

Cependant, contrairement aux types anonymes ,les "tuples" ont des noms et peuvent être utilisés dans les signatures de méthodes publiques.

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

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

Le problème avec ceci, bien sûr, est que L'assemblage A et L'assemblage B vont être compilés à des moments différents, ce qui signifie qu'ils finiraient tous deux par émettre leurs propres versions incompatibles du type tuple.

J'ai cherché à utiliser une sorte de "type erasure" pour le faire, donc que j'aurais une bibliothèque partagée avec un tas de types comme celui-ci (c'est la syntaxe "Liberty"):

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 l'accès des champs de tuple i, j et k vers Field1, Field2, et Field3.

Cependant ce n'est pas vraiment une option viable. Cela signifierait qu'au moment de la compilation tuple<x as int> et tuple<y as int> finiraient par être des types différents, alors qu'au moment de l'exécution, ils seraient traités comme le même type. Cela causerait beaucoup de problèmes pour des choses comme l'égalité et l'identité de type. C'est trop fuyant d'une abstraction à mon goût.

Autres options possibles serait d'utiliser "état sac des objets". Cependant, l'utilisation d'un sac d'état irait à l'encontre de tout le but d'avoir un support pour les "constructeurs de type" dans la langue. L'idée est d'activer les "extensions de langage personnalisées" pour générer de nouveaux types au moment de la compilation avec lesquels le compilateur peut effectuer une vérification de type statique.

En Java, cela peut être fait en utilisant des chargeurs de classe personnalisés. Fondamentalement, le code qui utilise les types tuple pourrait être émis sans réellement définir le type de disque. Un "chargeur de classe" personnalisé pourrait alors être défini qui générerait dynamiquement le type de tuple à l'exécution. Cela permettrait la vérification de type statique à l'intérieur du compilateur et unifierait les types de tuple à travers les limites de compilation.

Malheureusement, le CLR ne prend pas en charge le chargement des classes personnalisées. Tout le chargement dans le CLR se fait au niveau de l'assemblage. Il serait possible de définir un assemblage distinct pour chaque "type construit" , mais cela conduirait très rapidement à des problèmes de performance (avoir de nombreux assemblys avec un seul type utiliserait trop de ressources).

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

Est-il possible de simuler quelque chose comme les chargeurs de classe Java dans. NET, où je peux émettre une référence à un type non existant, puis générer dynamiquement une référence à ce type à l'exécution avant que le code ne l'utilise pistes?

REMARQUE:

*en fait, je connais déjà la réponse à la question, que je fournis 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 le documenter ici au cas où quelqu'un d'autre rencontrerait le même problème. *

43
demandé sur Robert 2008-10-09 07:31:03

2 réponses

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

Le System.Reflection.Emit namespace définit les types qui permettent aux assemblys d'être générés dynamiquement. Ils permettent également de définir les assemblages générés de manière incrémentielle. En d'autres termes, il est possible d'ajouter des types à l'assemblage dynamique, d'exécuter le code généré, puis d'ajouter d'autres types à l'assemblage.

Le System.AppDomain la classe définit également un AssemblyResolve évènement est déclenché chaque fois que le framework ne parvient pas à charger un assembly. En ajoutant un gestionnaire pour cet événement, il est possible de définir un seul assembly " runtime "dans lequel tous les types" construits " sont placés. Le code généré par le compilateur qui utilise un type construit ferait référence à un type dans l'assembly d'exécution. Étant donné que l'assembly d'exécution n'existe pas réellement sur le disque, l'événement AssemblyResolve serait déclenché la première fois que le code compilé tenterait d'accéder à un type construit. La poignée pour l' l'événement générerait alors l'assemblage dynamique et le renverrait au CLR.

Malheureusement, il y a quelques points difficiles à faire fonctionner. Le premier problème consiste à s'assurer que le gestionnaire d'événements sera toujours installé avant l'exécution du code compilé. Avec une application console, c'est facile. Le code pour brancher le gestionnaire d'événements peut simplement être ajouté à la méthode Main avant que l'autre code ne s'exécute. Pour les bibliothèques de classes, cependant, il n'y a pas de méthode principale. Une dll peut être chargée dans le cadre de une application écrite dans une autre langue, il est donc pas vraiment possible de supposer qu "il ya toujours une méthode principale disponible pour brancher le code du gestionnaire d" événements.

Le deuxième problème consiste à s'assurer que les types référencés sont tous insérés dans l'assembly dynamique avant d'utiliser tout code qui les Référence. Le 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 un assembly dynamique. Il donne au gestionnaire d'événements le possibilité de définir le type à l'intérieur de l'assembly dynamique avant que le code qui l'utilise ne s'exécute. Toutefois, cela ne fonctionnera pas dans ce cas. Le CLR ne déclenche pas l'événement pour les assemblys qui sont "référencés statiquement" par d'autres assemblys, même si l'assembly référencé est défini dynamiquement. Cela signifie que nous avons besoin d'un moyen d'exécuter du code avant que tout autre code de l'assembly compilé ne s'exécute et qu'il injecte dynamiquement les types dont il a besoin dans l'assembly d'exécution s'ils ne l'ont pas déjà été définir. Sinon, lorsque le CLR a essayé de charger ces types, il remarquera que l'assembly dynamique ne contient pas les types dont ils ont besoin et lancera une exception de charge de type.

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

  1. exécutez le constructeur de module avant tout type à l'intérieur du module sont accessibles.
  2. garantir que seuls les types directement accessibles par le constructeur de module seront chargés pendant l'exécution
  3. ne pas autoriser le code en dehors du module à accéder à l'un de ses membres tant que le constructeur n'a pas terminé.

Il le fait pour tous les assemblys, y compris les bibliothèques de classes et les exécutables, et for EXEs exécutera le constructeur de module avant d'exécuter la méthode principale.

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

Dans tous les cas, une solution complète à mon problème nécessite plusieurs pièces:

  1. La définition de classe suivante, définie dans une "dll d'exécution de langage", qui est référencée par tous les assemblys produits par le compilateur (c'est du 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 à l'assembly dynamique dans lequel les types construits seront créés. Il contient également un "hash set" qui stocke l'ensemble de types qui ont déjà été générés dynamiquement, et définit enfin un membre qui peut être utilisé pour définir le type. Cet exemple renvoie simplement un Système.Réflexion.Émettre.Instance TypeBuilder qui peut ensuite être utilisée pour définir la classe générée. Dans un système réel, la méthode prendrait probablement une représentation AST de la classe, et ferait simplement la génération elle-même.

  2. Assemblys compilés qui émettent les deux références suivantes (indiquées dans ILASM la syntaxe):

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

    Ici "SharedLib" est la bibliothèque d'exécution prédéfinie du langage qui inclut la classe" Loader " définie ci-dessus et "$Runtime" est l'assembly d'exécution dynamique dans lequel les types consructed seront insérés.

  3. Un "constructeur de module" à l'intérieur de chaque assembly compilé dans le langage.

    Pour autant que je sache, il n'y a pas de langages.net qui permettent aux constructeurs de modules d'être définis dans source. Le compilateur C++ / CLI est le seul compilateur que je connaisse qui les génère. Dans IL, ils ressemblent à ceci, défini directement dans le module et non dans aucune définition de type:

    .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 je doive écrire il personnalisé pour que cela fonctionne. J'écris un compilateur, donc la génération de code n'est pas un problème.

    Dans le cas d'un assembly qui utilisait les types tuple<i as int, j as int> et tuple<x as double, y as double, z as double>, le constructeur de module devrait générer des types comme ceux-ci (ici en 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;
    }
    

    Les classes tuple sont générées comme types génériques pour contourner les problèmes d'accessibilité. Cela permettrait au code dans l'assembly compilé d'utiliser tuple<x as Foo>, où Foo était un type non public.

    Le corps du constructeur de module qui a fait ceci (ici ne montrant qu'un type, et écrit en syntaxe C#) ressemblerait à 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, c'était le mécanisme que j'ai pu trouver pour activer l'équivalent approximatif des chargeurs de classe personnalisés dans le CLR.

Est-ce que quelqu'un connaît un plus facile façon de le faire?

51
répondu Scott Wisniewski 2014-01-24 08:41:35

Je pense que c'est le type de chose que le DLR est censé fournir en C# 4.0. C'est difficile de trouver des informations pour le moment, mais peut-être que nous en apprendrons plus sur PDC08. Attendant avec impatience de voir votre solution C# 3 cependant... Je suppose qu'il utilise des types anonymes.

-5
répondu Kevin Dostalek 2008-10-09 03:38:42