.net - specification - common language runtime detectó un programa no válido
Equivalente de cargadores de clase en.NET (2)
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.
¿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 programació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 compilador ejecuta en tiempo de compilación y genera tipos como salida. Son una especie de generalización de los genéricos (el lenguaje sí contiene genéricos normales) y permiten escribir un código como este (en sintaxis "Liberty"):
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 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 compilador se pueda compartir en múltiples ensamblajes. Por ejemplo, quiero
tuple<x as int>
definido en el conjunto A para terminar siendo del mismo tipo que tuple<x as int>
definido en el conjunto B.
El problema con esto, por supuesto, es que el ensamblaje A y el ensamblado B se compilará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 sintaxis "Liberty"):
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 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 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 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 compilación con los que el compilador 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 compilador, y unificaría los tipos de tupla a través de los límites de compilació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 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 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 compilador 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 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 todos 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 ensamblado 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, 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:
- Ejecute el constructor del módulo antes de acceder a cualquier tipo dentro del módulo.
- Garantizar que solo aquellos tipos directamente accedidos por el constructor del módulo serán cargados mientras se está ejecutando
- 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:
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 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 contiene una referencia al ensamblaje dinámico 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.
Conjuntos compilados que emiten las dos referencias siguientes (que se muestran en la sintaxis 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 dinámico en el que se insertarán los tipos construidos.
Un "constructor de módulos" dentro de cada ensamblaje compilado en el lenguaje.
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í, 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 compilador, por lo que la generación de código no es un problema.
En el caso de un ensamblado que usó los tipos
tuple<i as int, j as int>
ytuple<x as double, y as double, z as double>
el constructor del módulo necesitaría generar tipos como el 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 usara
tuple<x as Foo>
, 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 sintaxis 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(); } }
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?