Domanda

Qualcuno sa se è possibile definire l'equivalente di un " loader di classe personalizzata java " in .NET?

Per dare un piccolo sfondo:

Sono in procinto di sviluppare un nuovo linguaggio di programmazione destinato al CLR, chiamato " Liberty " ;. Una delle caratteristiche del linguaggio è la sua capacità di definire & Quot; costruttori di tipi & Quot ;, che sono metodi che vengono eseguiti dal compilatore in fase di compilazione e generano tipi come output. Sono una sorta di generalizzazione dei generici (il linguaggio ha dei generici normali) e consentono di scrivere codice come questo (in & Quot; Liberty & Quot; sintassi):

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

Where " tuple " è definito in questo modo:

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

In questo esempio particolare, il costruttore di tipi tuple fornisce qualcosa di simile ai tipi anonimi in VB e C #.

Tuttavia, a differenza dei tipi anonimi, " tuples " hanno nomi e possono essere utilizzati all'interno delle firme dei metodi pubblici.

Ciò significa che ho bisogno di un modo per il tipo che alla fine viene emesso dal compilatore per essere condivisibile su più assiemi. Ad esempio, voglio

tuple<x as int> definito nell'Assemblea A per finire con lo stesso tipo di Field1 definito nell'Assemblea B.

Il problema con questo, ovviamente, è che l'Assemblea A e l'Assemblea B verranno compilate in momenti diversi, il che significa che entrambi finiranno per emettere le proprie versioni incompatibili del tipo tupla.

Ho cercato di usare una sorta di " type erasure " per fare questo, in modo da avere una libreria condivisa con un sacco di tipi come questo (questo è " Liberty " sintassi):

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

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

e quindi reindirizzare l'accesso dai campi tupla i, je k a Field2, Field3 e tuple<y as int>.

Tuttavia, questa non è in realtà un'opzione praticabile. Ciò significherebbe che in fase di compilazione <=> e <=> finirebbero per essere tipi diversi, mentre in fase di runtime verrebbero trattati come lo stesso tipo. Ciò causerebbe molti problemi per cose come l'uguaglianza e l'identità del tipo. È un'estrazione troppo per i miei gusti.

Altre possibili opzioni sarebbero usare " oggetti bag di stato " ;. Tuttavia, l'uso di una borsa di stato annullerebbe l'intero scopo di avere il supporto per & Quot; type constructors & Quot; nella lingua. L'idea è quella di abilitare & Quot; estensioni di lingua personalizzate & Quot; per generare nuovi tipi in fase di compilazione con cui il compilatore può eseguire il controllo statico dei tipi.

In Java, questo potrebbe essere fatto usando caricatori di classi personalizzati. Fondamentalmente il codice che utilizza i tipi di tupla potrebbe essere emesso senza effettivamente definire il tipo su disco. Un & Quot personalizzato; class loader & Quot; potrebbe quindi essere definito che genererebbe dinamicamente il tipo di tupla in fase di esecuzione. Ciò consentirebbe il controllo del tipo statico all'interno del compilatore e unirebbe i tipi di tupla oltre i limiti della compilazione.

Sfortunatamente, tuttavia, il CLR non fornisce supporto per il caricamento di classi personalizzate. Tutto il caricamento nel CLR viene eseguito a livello di assieme. Sarebbe possibile definire un assembly separato per ogni & Quot; tipo costruito & Quot ;, ma ciò porterebbe molto rapidamente a problemi di prestazioni (avere molti assembly con un solo tipo in essi userebbe troppe risorse).

Quindi, quello che voglio sapere è:

È possibile simulare qualcosa come Java Class Loaders in .NET, dove posso emettere un riferimento a un tipo inesistente e quindi generare dinamicamente un riferimento a quel tipo in fase di esecuzione prima che il codice che deve usare venga eseguito ?

Nota:

* In realtà conosco già la risposta alla domanda, che fornisco come risposta di seguito. Tuttavia, ci sono voluti circa 3 giorni di ricerche e un bel po 'di hacking IL per trovare una soluzione. Ho pensato che sarebbe stata una buona idea documentarlo qui nel caso in cui qualcun altro avesse riscontrato lo stesso problema. *

È stato utile?

Soluzione

La risposta è sì, ma la soluzione è un po 'complicata.

Lo spazio dei nomi System.Reflection.Emit definisce i tipi che consente di generare dinamicamente gli assiemi. Consentono inoltre di definire in modo incrementale gli assiemi generati. In altre parole, è possibile aggiungere tipi all'assembly dinamico, eseguire il codice generato e quindi aggiungere altri tipi all'assembly.

La System.AppDomain definisce anche una AssemblyResolve che viene generato ogni volta che il framework non riesce a caricare un assembly. Aggiungendo un gestore per quell'evento, è possibile definire un singolo & Quot; runtime & Quot; assieme in cui tutti " costruito " i tipi sono posizionati. Il codice generato dal compilatore che utilizza un tipo costruito farebbe riferimento a un tipo nell'assieme di runtime. Poiché l'assembly di runtime in realtà non esiste sul disco, AssemblyResolve < L'evento / a> verrebbe generato la prima volta che il codice compilato ha tentato di accedere a un tipo costruito. L'handle per l'evento genererebbe quindi l'assembly dinamico e lo restituirà al CLR.

Sfortunatamente, ci sono alcuni punti difficili per farlo funzionare. Il primo problema è garantire che il gestore eventi sia sempre installato prima dell'esecuzione del codice compilato. Con un'applicazione console è facile. Il codice per collegare il gestore eventi può essere aggiunto al metodo Main prima dell'esecuzione dell'altro codice. Per le librerie di classi, tuttavia, non esiste un metodo principale. Una dll può essere caricata come parte di un'applicazione scritta in un'altra lingua, quindi non è proprio possibile supporre che sia sempre disponibile un metodo principale per collegare il codice del gestore eventi.

Il secondo problema è garantire che tutti i tipi a cui viene fatto riferimento vengano inseriti nell'assemblaggio dinamico prima di utilizzare qualsiasi codice che li fa riferimento. La classe TypeResolve definisce anche una tuple<i as int, j as int> che viene eseguito ogni volta che il CLR non è in grado di risolvere un tipo in un assieme dinamico. Offre al gestore di eventi l'opportunità di definire il tipo all'interno dell'assembly dinamico prima che venga eseguito il codice che lo utilizza. Tuttavia, quell'evento non funzionerà in questo caso. Il CLR non genererà l'evento per gli assembly che sono & Quot; quotato in modo statico & Quot; da altri assembly, anche se l'assembly di riferimento è definito in modo dinamico. Ciò significa che abbiamo bisogno di un modo per eseguire il codice prima che venga eseguito qualsiasi altro codice nell'assembly compilato e che inietti dinamicamente i tipi necessari nell'assembly di runtime se non sono già stati definiti. Altrimenti quando il CLR tenterà di caricare quei tipi noterà che l'assemblaggio dinamico non contiene i tipi di cui hanno bisogno e genererà un'eccezione di caricamento del tipo.

Fortunatamente, il CLR offre una soluzione a entrambi i problemi: inizializzatori di moduli. Un inizializzatore di moduli è l'equivalente di un & Quot; costruttore di classi statiche & Quot ;, tranne per il fatto che inizializza un intero modulo, non solo una singola classe. Baiscally, il CLR:

  1. Esegui il costruttore del modulo prima di accedere a qualsiasi tipo all'interno del modulo.
  2. Garantire che solo quei tipi a cui accede direttamente il costruttore del modulo verranno caricati durante l'esecuzione
  3. Non consentire al codice esterno al modulo di accedere a nessuno dei suoi membri fino al termine del costruttore.

Lo fa per tutti gli assembly, comprese le librerie di classi e gli eseguibili, e perr Gli EXE eseguiranno il costruttore del modulo prima di eseguire il metodo Main.

Vedi questo post di blog per ulteriori informazioni su costruttori.

In ogni caso, una soluzione completa al mio problema richiede diversi pezzi:

  1. La seguente definizione di classe, definita all'interno di una " language runtime dll " ;, a cui fa riferimento tutti gli assembly prodotti dal compilatore (questo è il codice 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 definisce un singleton che contiene un riferimento all'assembly dinamico in cui verranno creati i tipi costruiti. Contiene anche un " hash set " che memorizza l'insieme di tipi che sono già stati generati dinamicamente e infine definisce un membro che può essere utilizzato per definire il tipo. Questo esempio restituisce solo un'istanza System.Reflection.Emit.TypeBuilder che può quindi essere utilizzata per definire la classe che viene generata. In un sistema reale, il metodo probabilmente prenderebbe una rappresentazione AST della classe e farebbe solo la generazione stessa.

  2. Assiemi compilati che emettono i seguenti due riferimenti (mostrati nella sintassi ILASM):

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

    Qui " SharedLib " è la libreria di runtime predefinita del linguaggio che include il " Loader " classe definita sopra e " $ Runtime " è l'assembly di runtime dinamico in cui verranno inseriti i tipi costrutti.

  3. A " costruttore del modulo " all'interno di ogni assemblea compilata nella lingua.

    Per quanto ne so, non esistono linguaggi .NET che consentano di definire i costruttori di moduli in origine. Il compilatore C ++ / CLI è l'unico compilatore che conosco che li genera. In IL, sembrano così, definiti direttamente nel modulo e non all'interno di nessuna definizione di tipo:

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

    Per me, non è un problema che devo scrivere IL personalizzato per farlo funzionare. Sto scrivendo un compilatore, quindi la generazione del codice non è un problema.

    Nel caso di un assembly che utilizzava i tipi tuple<x as double, y as double, z as double> e tuple<x as Foo> il costruttore del modulo avrebbe bisogno di generare tipi come il seguente (qui nella sintassi 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;
    }
    

    Le classi tuple sono generate come tipi generici per aggirare i problemi di accessibilità. Ciò consentirebbe al codice nell'assieme compilato di utilizzare <=>, dove Foo era di tipo non pubblico.

    Il corpo del costruttore del modulo che ha fatto questo (qui mostra solo un tipo e scritto nella sintassi C #) sarebbe simile a questo:

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

Quindi, in ogni caso, questo era il meccanismo che sono stato in grado di inventare per abilitare l'equivalente approssimativo dei caricatori di classi personalizzati nel CLR.

Qualcuno conosce un modo più semplice per farlo?

Altri suggerimenti

Penso che questo sia il tipo di cosa che il DLR dovrebbe fornire in C # 4.0. È un po 'difficile trovare informazioni, ma forse impareremo di più su PDC08. Aspettando con impazienza di vedere la tua soluzione C # 3 però ... Immagino che usi tipi anonimi.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top