42

¿Alguien sabe si es posible definir el equivalente de un "cargador de clases personalizado de Java" en .NET?Equivalente de cargadores de clase en .NET

Para dar un poco de historia:

estoy en el proceso de desarrollar un nuevo lenguaje de programación que se dirige el CLR, llamado "Libertad". Una de las características del lenguaje es su capacidad para definir "constructores de tipo", que son métodos que el compilador ejecuta en tiempo de compilación y genera tipos como salida. Son una especie de generalización de los genéricos (el lenguaje tiene genéricos normales en ella), y permitir que un código como el que se escribirá (en "libertad" sintaxis):

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

Donde "tupla" se define como tal :

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

en este ejemplo particular, el constructor de tipo tuple proporciona algo similar a los tipos anónimos en VB y C#.

Sin embargo, a diferencia de los tipos anónimos, las "tuplas" tienen nombres y se pueden usar dentro de las firmas de métodos públicos.

Esto significa que necesito una forma para que el tipo que finalmente termine siendo emitido por el compilador se pueda compartir en varios ensambles. Por ejemplo, quiero

tuple<x as int> se define en la Asamblea de A a llegar a ser del mismo tipo que se define en la Asamblea tuple<x as int> B.

El problema con esto, por supuesto, es que la Asamblea A y B de la Asamblea van a compilarse en diferentes momentos, lo que significa que ambos terminarían emitiendo sus propias versiones incompatibles del tipo de tupla.

Miré en el uso de algún tipo de "tipo borrado" de hacer esto, por lo que tendría una biblioteca compartida con un montón de tipos como este (esta es la sintaxis "Libertad"):

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

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

y luego simplemente redirigir el acceso desde los campos tuple i, j y k al Field1, Field2 y Field3.

Sin embargo, esa no es realmente una opción viable. Esto significaría que en el tiempo de compilación tuple<x as int> y tuple<y as int> terminarían siendo tipos diferentes, mientras que en tiempo de ejecución serían tratados del mismo tipo. Eso causaría muchos problemas para cosas como igualdad y tipo de identidad. Eso es demasiado agujereado como una abstracción para mis gustos.

Otras opciones posibles serían utilizar "objetos de bolsa de estado". Sin embargo, usar una bolsa de estado vencería el propósito de tener soporte para "constructores de tipo" en el idioma. La idea es habilitar "extensiones de lenguaje personalizadas" para generar nuevos tipos en tiempo de compilación con los que el compilador pueda realizar la comprobación de tipos estáticos.

En Java, esto se puede hacer utilizando cargadores de clases personalizados. Básicamente, el código que usa tipos de tuplas podría emitirse sin definir realmente el tipo en el disco. Entonces se podría definir un "cargador de clases" personalizado que genere dinámicamente el tipo de tupla en el tiempo de ejecución. Eso permitiría la comprobación del tipo estático dentro del compilador, y unificaría los tipos de tupla a través de los límites de compilación.

Desafortunadamente, sin embargo, el CLR no proporciona soporte para la carga personalizada de clases.Toda la carga en el CLR se realiza en el nivel de ensamblaje. Sería posible definir un ensamblaje separado para cada "tipo construido", pero eso llevaría muy rápidamente a problemas de rendimiento (tener muchos ensambles con solo un tipo en ellos usaría demasiados recursos).

Por lo tanto, lo que yo quiero saber es:

¿Es posible simular algo parecido a Java Clase Cargadores en .NET, donde puedo emitir una referencia a un tipo no-existente y luego generar dinámicamente una referencia a ese tipo en tiempo de ejecución antes de que se ejecute el código que necesita usarlo?

NOTA:

* En realidad, ya sé la respuesta a la pregunta, que proporciono como una respuesta a continuación. Sin embargo, me tomó alrededor de 3 días de investigación, y un poco de pirateo IL para encontrar una solución. Pensé que sería una buena idea documentarlo aquí en caso de que alguien más se encontrara con el mismo problema. *

+0

Oh wow, primera publicación que alguna vez pensé que debería tener títulos de capítulo. Gran información! ¡Gracias por publicar! –

Respuesta

51

La respuesta es sí, pero la solución es un poco complicada.

El espacio de nombre System.Reflection.Emit define los tipos que permiten que los ensamblados se generen dinámicamente. También permiten que los ensamblados generados se definan incrementalmente. En otras palabras, es posible agregar tipos al ensamblaje dinámico, ejecutar el código generado y luego agregar más tipos al ensamblaje.

La clase System.AppDomain también define un evento AssemblyResolve que se activa siempre que el marco no puede cargar un conjunto. Al agregar un controlador para ese evento, es posible definir un único ensamblaje de "tiempo de ejecución" en el que se colocan todos los tipos "construidos". El código generado por el compilador que utiliza un tipo construido se referiría a un tipo en el ensamblado en tiempo de ejecución. Como el ensamblado de tiempo de ejecución no existe realmente en el disco, el evento AssemblyResolve se desencadenará la primera vez que el código compilado intente acceder a un tipo construido. El identificador del evento generaría el ensamblaje dinámico y lo devolvería al CLR.

Desafortunadamente, hay algunos puntos difíciles para hacer que esto funcione. El primer problema es garantizar que el controlador de eventos siempre se instalará antes de ejecutar el código compilado. Con una aplicación de consola, esto es fácil. El código para conectar el controlador de eventos solo se puede agregar al método Main antes de que se ejecute el otro código. Para las bibliotecas de clase, sin embargo, no hay un método principal. Una dll puede cargarse como parte de una aplicación escrita en otro idioma, por lo que no es posible suponer que siempre hay un método principal disponible para conectar el código del controlador de eventos.

El segundo problema es asegurarse de que los tipos a los que se hace referencia se inserten en el ensamblaje dinámico antes de utilizar cualquier código que haga referencia a ellos. La clase System.AppDomain también define un evento TypeResolve que se ejecuta siempre que el CLR no puede resolver un tipo en un ensamblaje dinámico. Le da al manejador de eventos la oportunidad de definir el tipo dentro del ensamblaje dinámico antes de que se ejecute el código que lo usa. Sin embargo, ese evento no funcionará en este caso. CLR no activará el evento para ensamblados a los que otros ensamblados "hacen referencia estáticamente", incluso si el ensamblaje al que se hace referencia se define dinámicamente. Esto significa que necesitamos una forma de ejecutar código antes de que se ejecute cualquier otro código en el ensamblado compilado y hacer que inyecte dinámicamente los tipos que necesita en el ensamblado de tiempo de ejecución si aún no se han definido.De lo contrario, cuando el CLR intente cargar esos tipos, notará que el ensamblaje dinámico no contiene los tipos que necesitan y lanzará una excepción de carga de tipo.

Afortunadamente, el CLR ofrece una solución a ambos problemas: Inicializadores de módulos. Un inicializador de módulo es el equivalente de un "constructor de clase estática", excepto que inicializa un módulo completo, no solo una clase. Baiscally, el CLR:

  1. Ejecute el constructor del módulo antes de acceder a cualquier tipo dentro del módulo.
  2. Garantizar que solo aquellos tipos directamente accedidos por el constructor del módulo se cargarán mientras se está ejecutando
  3. No permita que el código fuera del módulo acceda a ninguno de sus miembros hasta después de que el constructor haya finalizado.

Lo hace para todos los ensamblados, incluidas las bibliotecas de clases y los ejecutables, y para EXE ejecutará el constructor del módulo antes de ejecutar el método Main.

Consulte este blog post para obtener más información acerca de los constructores.

En cualquier caso, una solución completa a mi problema requiere varias piezas:

  1. La siguiente definición de clase, que se define dentro de una "DLL de tiempo de ejecución de lenguaje", que hace referencia a todas las asambleas producidos por el compilador (este es el código 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 clase define un singleton que mantiene una referencia al ensamblado dinámico que los tipos construidos serán creados en. También lleva a cabo un "conjunto de hash" que almacena el conjunto de tipos que ya se han generado de forma dinámica, y finalmente define un miembro que se puede usar para definir el tipo. Este ejemplo simplemente devuelve una instancia de System.Reflection.Emit.TypeBuilder que luego se puede usar para definir la clase que se está generando. En un sistema real, el método probablemente tomaría una representación AST de la clase, y simplemente haría la generación en sí misma.

  2. ensamblados compilados que emiten las dos referencias siguientes (mostrados en la sintaxis ILASM):

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

    aquí "SharedLib" es la biblioteca de tiempo de ejecución predefinido de la lengua que incluye la clase "Loader" definido anteriormente y "$ Runtime "es el ensamblaje de tiempo de ejecución dinámico en el que se insertarán los tipos construidos.

  3. Un "constructor de módulos" dentro de cada ensamblaje compilado en el idioma.

    Hasta donde yo sé, no hay lenguajes .NET que permitan que los constructores de módulos se definan en origen. El compilador C++/CLI es el único compilador que conozco que los genera. En IL, se ven así, que se define directamente en el módulo y no dentro de las definiciones de tipo:

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

    Para mí, no es un problema que tengo que escribir de encargo IL a conseguir que esto funcione. Estoy escribiendo un compilador, por lo que la generación de código no es un problema.

    En el caso de un montaje que utiliza los tipos tuple<i as int, j as int> y tuple<x as double, y as double, z as double> el constructor módulo necesitaría generar tipos como la siguiente (aquí en C# sintaxis):

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

    Las clases de tupla se generan como tipos genéricos para evitar problemas de accesibilidad. Eso permitiría que el código en el ensamblado compilado use tuple<x as Foo>, donde Foo era de tipo no público.

    El cuerpo del constructor módulo que hizo esto (aquí sólo se muestra un tipo, y escrito en sintaxis de C#) se vería así:

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

Así que, en cualquier caso, este es el mecanismo Pude idear para habilitar el equivalente aproximado de cargadores de clase personalizados en el CLR.

¿Alguien sabe de una manera más fácil de hacer esto?

+0

Ugh, ¿cómo es la definición del constructor de su módulo diferente del constructor de clase ordinario? ¿La diferencia en el uso de 'privatescope' en comparación con' private hidebysig'? –

+0

Ahh, lo descubrí. No hay diferencia, excepto que el módulo cctor no se coloca en ningún tipo particular. No sabía que incluso podrías hacer eso :) –

-5

Creo que este es el tipo de cosa que se supone que proporciona el DLR en C# 4.0. Aún es difícil encontrar información, pero tal vez aprendamos más en PDC08. Esperando ansiosamente para ver tu solución C# 3 ... supongo que usa tipos anónimos.