Question

Est-ce que quelqu'un sait s'il est possible de définir l'équivalent d'un & "; chargeur de classes personnalisé java &"; dans .NET?

Pour donner un peu de contexte:

Je suis en train de développer un nouveau langage de programmation ciblant le CLR, appelé & "Liberty &"; Une des caractéristiques du langage est sa capacité à définir & "; Les constructeurs de type &" ;, 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. Il s’agit d’une sorte de généralisation des génériques (le langage contient des génériques normaux) et permet d’écrire un tel code (dans la syntaxe & Quot; Liberty & Quot;):

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

Cependant, contrairement aux types anonymes, & "; Tuples" & "; ont des noms et peuvent être utilisés dans les signatures de méthode publique.

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

tuple<x as int> défini dans l’Assemblée A pour finir par être du même type que Field1 défini dans l’Assemblée B.

Le problème avec ceci, bien sûr, est que les assemblées A et B vont être compilées à des moments différents, ce qui signifie qu’elles finiraient par émettre leurs propres versions incompatibles du type tuple.

J'ai envisagé d'utiliser une sorte de & "type effacement &"; faire cela, afin que je dispose d’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;
}

puis redirigez simplement l'accès des champs de tuple i, j et k vers Field2, Field3 et tuple<y as int>.

Cependant, ce n’est pas vraiment une option viable. Cela signifierait qu'au moment de la compilation <=> et <=> deviendraient des types différents, alors qu'au moment de l'exécution, ils seraient traités comme du même type. Cela causerait beaucoup de problèmes pour des choses comme l'égalité et l'identité de type. C'est trop fuite d'une abstraction à mon goût.

D'autres options possibles consisteraient à utiliser & "les objets de sac d'état &"; Cependant, utiliser un sac d'état irait à l'encontre de l'objectif de support des & "Constructeurs de type &"; dans la langue. L’idée est d’activer & Quot; extensions de langage personnalisées & Quot; 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 pourrait être fait en utilisant des chargeurs de classes personnalisés. Fondamentalement, le code qui utilise les types de tuples peut être émis sans définir le type sur le disque. Un chargeur de classe & Personnalisé & Quot; peut alors être défini pour générer dynamiquement le type de tuple au moment de l’exécution. Cela permettrait une vérification statique du type à l'intérieur du compilateur et unifierait les types de tuple au-delà des limites de la compilation.

Malheureusement, le CLR ne fournit pas de support pour le chargement de classes personnalisées. Tout le chargement dans le CLR est effectué au niveau de l’assemblage. Il serait possible de définir un assemblage distinct pour chaque & "Type construit &"; Mais cela entraînerait très rapidement des problèmes de performances (plusieurs assemblys contenant un seul type utiliseraient trop de ressources).

Ce que je veux savoir, c'est:

Est-il possible de simuler quelque chose comme des chargeurs de classes Java dans .NET, où je peux émettre une référence à un type inexistant puis générer dynamiquement une référence à ce type au moment de l'exécution, avant que le code utilisé ne soit utilisé ?

REMARQUE:

* En fait, je connais déjà la réponse à la question, à laquelle je réponds ci-dessous. Cependant, il m'a fallu environ 3 jours de recherche et assez de piratage informatique pour 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. *

Était-ce utile?

La solution

The answer is yes, but the solution is a little tricky.

The System.Reflection.Emit namespace defines types that allows assemblies to be generated dynamically. They also allow the generated assemblies to be defined incrementally. In other words it is possible to add types to the dynamic assembly, execute the generated code, and then latter add more types to the assembly.

The System.AppDomain class also defines an AssemblyResolve event that fires whenever the framework fails to load an assembly. By adding a handler for that event, it is possible to define a single "runtime" assembly into which all "constructed" types are placed. The code generated by the compiler that uses a constructed type would refer to a type in the runtime assembly. Because the runtime assembly doesn't actually exist on disk, the AssemblyResolve event would be fired the first time the compiled code tried to access a constructed type. The handle for the event would then generate the dynamic assembly and return it to the CLR.

Unfortunately, there are a few tricky points to getting this to work. The first problem is ensuring that the event handler will always be installed before the compiled code is run. With a console application this is easy. The code to hookup the event handler can just be added to the Main method before the other code runs. For class libraries, however, there is no main method. A dll may be loaded as part of an application written in another language, so it's not really possible to assume there is always a main method available to hookup the event handler code.

The second problem is ensuring that the referenced types all get inserted into the dynamic assembly before any code that references them is used. The System.AppDomain class also defines a TypeResolve event that is executed whenever the CLR is unable to resolve a type in a dynamic assembly. It gives the event handler the opportunity to define the type inside the dynamic assembly before the code that uses it runs. However, that event will not work in this case. The CLR will not fire the event for assemblies that are "statically referenced" by other assemblies, even if the referenced assembly is defined dynamically. This means that we need a way to run code before any other code in the compiled assembly runs and have it dynamically inject the types it needs into the runtime assembly if they have not already been defined. Otherwise when the CLR tried to load those types it will notice that the dynamic assembly does not contain the types they need and will throw a type load exception.

Fortunately, the CLR offers a solution to both problems: Module Initializers. A module initializer is the equivalent of a "static class constructor", except that it initializes an entire module, not just a single class. Baiscally, the CLR will:

  1. Run the module constructor before any types inside the module are accessed.
  2. Guarantee that only those types directly accessed by the module constructor will be loaded while it is executing
  3. Not allow code outside the module to access any of it's members until after the constructor has finished.

It does this for all assemblies, including both class libraries and executables, and for EXEs will run the module constructor before executing the Main method.

See this blog post for more information about constructors.

In any case, a complete solution to my problem requires several pieces:

  1. The following class definition, defined inside a "language runtime dll", that is referenced by all assemblies produced by the compiler (this is C# code).

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

    The class defines a singleton that holds a reference to the dynamic assembly that the constructed types will be created in. It also holds a "hash set" that stores the set of types that have already been dynamically generated, and finally defines a member that can be used to define the type. This example just returns a System.Reflection.Emit.TypeBuilder instance that can then be used to define the class being generated. In a real system, the method would probably take in an AST representation of the class, and just do the generation it's self.

  2. Compiled assemblies that emit the following two references (shown in ILASM syntax):

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

    Here "SharedLib" is the Language's predefined runtime library that includes the "Loader" class defined above and "$Runtime" is the dynamic runtime assembly that the consructed types will be inserted into.

  3. A "module constructor" inside every assembly compiled in the language.

    As far as I know, there are no .NET languages that allow Module Constructors to be defined in source. The C++ /CLI compiler is the only compiler I know of that generates them. In IL, they look like this, defined directly in the module and not inside any type definitions:

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

    For me, It's not a problem that I have to write custom IL to get this to work. I'm writing a compiler, so code generation is not an issue.

    In the case of an assembly that used the types tuple<i as int, j as int> and tuple<x as double, y as double, z as double> the module constructor would need to generate types like the following (here in C# syntax):

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

    The tuple classes are generated as generic types to get around accessibility issues. That would allow code in the compiled assembly to use tuple<x as Foo>, where Foo was some non-public type.

    The body of the module constructor that did this (here only showing one type, and written in C# syntax) would look like this:

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

So in any case, this was the mechanism I was able to come up with to enable the rough equivalent of custom class loaders in the CLR.

Does anyone know of an easier way to do this?

Autres conseils

I think this is the type of thing the DLR is supposed to provide in C# 4.0. Kind of hard to come by information yet, but perhaps we'll learn more at PDC08. Eagerly waiting to see your C# 3 solution though... I'm guessing it uses anonymous types.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top