Equivalente de cargadores de clase en .NET

¿Alguien sabe si es posible definir el equivalente de un “cargador de clases personalizado de Java” en .NET?

Para dar un poco de historia:

Estoy en el proceso de desarrollar un nuevo lenguaje de progtwigción que apunta al CLR, llamado “Liberty”. Una de las características del lenguaje es su capacidad para definir “constructores de tipo”, que son métodos que el comstackdor ejecuta en tiempo de comstackción y genera tipos como salida. Son una especie de generalización de los generics (el lenguaje sí contiene generics normales) y permiten escribir un código como este (en syntax “Liberty”):

var t as tuple; ti = 2; tj = 4; tk = 5; 

Donde “tupla” se define así:

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

En este ejemplo particular, la tuple constructor de tipos 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 eventualmente termine siendo emitido por el comstackdor se pueda compartir en múltiples ensamblajes. Por ejemplo, quiero

tuple definido en el conjunto A para terminar siendo del mismo tipo que tuple definido en el conjunto B.

El problema con esto, por supuesto, es que el ensamblaje A y el ensamblado B se comstackrán en diferentes momentos, lo que significa que ambos terminarán emitiendo sus propias versiones incompatibles del tipo de tupla.

Intenté usar algún tipo de “borrado de tipo” para hacer esto, de modo que tuviera una biblioteca compartida con varios tipos como este (esta es la syntax “Liberty”):

 class tuple { public Field1 as T; } class tuple { public Field2 as T; public Field2 as R; } 

y luego simplemente redirigir el acceso de los campos de tupla i, j y k a Field2 , Field3 y Field3 .

Sin embargo, esa no es realmente una opción viable. Esto significaría que en tiempo de comstackción tuple y tuple 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 usar “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 comstackción con los que el comstackdor pueda realizar la comprobación de tipos estáticos.

En Java, esto se puede hacer usando 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 comstackdor, y unificaría los tipos de tupla a través de los límites de comstackción.

Desafortunadamente, sin embargo, 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).

Entonces, lo que quiero saber es:

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

NOTA:

* De hecho, ya sé la respuesta a la pregunta, que proporciono como 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. *

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

El System.Reflection.Emit nombres 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 dynamic, 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 framework no puede cargar un ensamblado. 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 comstackdor que utiliza un tipo construido se referiría a un tipo en el ensamblado en tiempo de ejecución. Debido a que el ensamblado en tiempo de ejecución no existe realmente en el disco, el evento AssemblyResolve se desencadenará la primera vez que el código comstackdo intente acceder a un tipo construido. El identificador del evento generaría el ensamblaje dynamic 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 comstackdo. 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 todos los tipos a los que se hace referencia se inserten en el ensamblaje dynamic 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 ensamblado dynamic. Le da al manejador de eventos la oportunidad de definir el tipo dentro del ensamblaje dynamic 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 comstackdo 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 dynamic no contiene los tipos que necesitan y lanzará una excepción de carga de tipo.

Afortunadamente, CLR ofrece una solución para 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 serán cargados 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 esta publicación en el blog para obtener más información sobre constructores.

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

  1. La siguiente definición de clase, definida dentro de un “dll de tiempo de ejecución de idioma”, a la que hacen referencia todos los ensamblados producidos por el comstackdor (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(); } private static readonly Loader m_instance; private readonly ModuleBuilder m_dynamicModule; private readonly HashSet 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 contiene una referencia al ensamblaje dynamic en el que se crearán los tipos construidos. También contiene un “conjunto de almohadillas” que almacena el conjunto de tipos que ya se han generado dinámicamente, y finalmente define un miembro que puede ser utilizado 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. Conjuntos comstackdos que emiten las dos referencias siguientes (que se muestran en la syntax de 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 predefinida del lenguaje que incluye la clase “Loader” definida anteriormente y “$ Runtime” es el conjunto de tiempo de ejecución dynamic en el que se insertarán los tipos construidos.

  3. Un “constructor de módulos” dentro de cada ensamblaje comstackdo en el lenguaje.

    Hasta donde yo sé, no hay lenguajes .NET que permitan que los Constructores de Módulos se definan en origen. El comstackdor C ++ / CLI es el único comstackdor que conozco que los genera. En IL, se ven así, definidos 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 tenga que escribir IL personalizada para que esto funcione. Estoy escribiendo un comstackdor, por lo que la generación de código no es un problema.

    En el caso de un ensamblado que usó los tipos tuple y tuple el constructor del módulo necesitaría generar tipos como el siguiente (aquí en C # syntax):

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

    Las clases de tupla se generan como tipos generics para evitar problemas de accesibilidad. Eso permitiría que el código en el ensamblado comstackdo usara tuple , donde Foo era de tipo no público.

    El cuerpo del constructor del módulo que hizo esto (aquí solo muestra un tipo y está escrito en syntax 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  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(); } } 

Entonces, en cualquier caso, este fue el mecanismo que pude idear para habilitar el equivalente aproximado de los cargadores de clase personalizados en el CLR.

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

Creo que este es el tipo de cosas que el DLR debería proporcionar 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.