42

Qualcuno sa se è possibile definire l'equivalente di un "caricatore di classi personalizzate java" in .NET?Equivalente di caricatori di classi in .NET

Per dare un po 'di storia:

Io sono nel processo di sviluppo di un nuovo linguaggio di programmazione che gli obiettivi del CLR, chiamato "Liberty". Una delle caratteristiche del linguaggio è la sua capacità di definire "costruttori di tipi", che sono metodi che vengono eseguiti dal compilatore in fase di compilazione e generano tipi come output. Essi sono una sorta di una generalizzazione dei farmaci generici (la lingua non ha generici normali in esso), e consentire il codice come questo da scrivere (in "Libertà" sintassi):

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

Dove "tupla" è definito in questo modo :

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

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

Tuttavia, a differenza dei tipi anonimi, "tuple" 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 finisce per essere emesso dal compilatore per essere condivisibile tra più assiemi. Per esempio, io voglio

tuple<x as int> definito Montaggio A finire per essere dello stesso tipo di tuple<x as int> definito in Assemblea B.

Il problema con questo, naturalmente, è che Assemblea A e B Assemblea stanno per essere compilati in momenti diversi, il che significa che entrambi finiranno per emettere le proprie versioni incompatibili del tipo di tupla.

Ho guardato in utilizzando una sorta di "cancellazione del tipo" per fare questo, in modo che io avrei una libreria condivisa con un gruppo di tipi come questo (questa è la sintassi "Libertà"):

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 di tupla i, j, k a Field1, Field2 e Field3.

Tuttavia, questa non è un'opzione praticabile. Ciò significherebbe che al momento della compilazione tuple<x as int> e tuple<y as int> risulterebbero di tipo diverso, mentre in fase di runtime sarebbero trattati come lo stesso tipo. Ciò causerebbe molti problemi per cose come l'uguaglianza e l'identità del tipo. Questo è troppo dispersivo di un'astrazione per i miei gusti.

Altre opzioni possibili sarebbero utilizzare "oggetti borsa di stato". Tuttavia, l'uso di una borsa di stato vanificherebbe l'intero scopo di avere il supporto per "costruttori di tipi" nella lingua. L'idea è di abilitare "estensioni del linguaggio personalizzate" per generare nuovi tipi in fase di compilazione che il compilatore può eseguire con il controllo di tipo statico.

In Java, questo può essere fatto utilizzando caricatori di classe personalizzati. Fondamentalmente il codice che usa i tipi di tuple potrebbe essere emesso senza effettivamente definire il tipo sul disco. Si potrebbe quindi definire un "caricatore di classi" personalizzato che genererebbe dinamicamente il tipo di tupla in fase di runtime. Ciò consentirebbe il controllo di tipo statico all'interno del compilatore e unificerebbe i tipi di tupla attraverso i limiti di compilazione.

Sfortunatamente, tuttavia, il CLR non fornisce supporto per il caricamento personalizzato della classe.Tutto il carico nel CLR viene eseguito a livello di assemblaggio. Sarebbe possibile definire un assembly separato per ogni "tipo costruito", ma ciò comporterebbe molto rapidamente problemi di prestazioni (se molti assiemi con un solo tipo utilizzassero troppe risorse).

Quindi, quello che voglio sapere è:

E 'possibile simulare qualcosa di simile a Java classe di pale in .NET, dove posso emettere un riferimento a un tipo non-esistente e quindi generare dinamicamente un riferimento a quel tipo in fase di esecuzione prima del codice in cui è necessario eseguirlo?

NOTA:

* Io in realtà conosco già la risposta alla domanda, che fornisco come una risposta di seguito. Tuttavia, mi ci sono voluti circa 3 giorni di ricerca e un bel po 'di hacking su IL per trovare una soluzione. Ho pensato che sarebbe stata una buona idea documentarlo qui nel caso in cui qualcun altro si fosse imbattuto nello stesso problema. *

+0

Oh wow, il primo post che ho mai pensato dovrebbe avere titoli di capitolo. Grande informazione! Grazie per la pubblicazione! –

risposta

51

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

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

La classe System.AppDomain definisce anche un evento AssemblyResolve che si attiva ogni volta che il framework non riesce a caricare un assieme. Aggiungendo un gestore per quell'evento, è possibile definire un singolo assembly "runtime" in cui sono collocati tutti i tipi "costruiti". Il codice generato dal compilatore che utilizza un tipo costruito fa riferimento a un tipo nell'assieme di runtime. Poiché l'assembly di runtime in realtà non esiste sul disco, l'evento AssemblyResolve viene generato la prima volta che il codice compilato tenta 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 delicati per farlo funzionare. Il primo problema è garantire che il gestore eventi venga sempre installato prima dell'esecuzione del codice compilato. Con un'applicazione per console questo è 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 è realmente possibile presumere che ci sia sempre un metodo principale disponibile per collegare il codice del gestore di eventi.

Il secondo problema è garantire che i tipi di riferimento vengano tutti inseriti nell'assembly dinamico prima che venga utilizzato qualsiasi codice che li fa riferimento. La classe System.AppDomain definisce anche un evento TypeResolve che viene eseguito ogni volta che CLR non è in grado di risolvere un tipo in un assembly dinamico. Fornisce al gestore di eventi l'opportunità di definire il tipo all'interno dell'assembly dinamico prima dell'esecuzione del codice che lo utilizza. Tuttavia, quell'evento non funzionerà in questo caso. Il CLR non genererà l'evento per gli assembly che sono "referenziati staticamente" da altri assembly, anche se l'assembly di riferimento è definito dinamicamente. Ciò significa che abbiamo bisogno di un modo per eseguire il codice prima dell'esecuzione di qualsiasi altro codice nell'assieme compilato e di farlo iniettare dinamicamente i tipi necessari nell'assieme di runtime se non sono già stati definiti.Altrimenti, quando CLR ha provato a caricare questi tipi, noterà che l'assembly dinamico non contiene i tipi di cui ha bisogno e genererà un'eccezione di caricamento del tipo.

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

  1. Eseguire il costruttore del modulo prima di accedere a qualsiasi tipo all'interno del modulo.
  2. garanzia che solo i tipi direttamente accessibili dal costruttore modulo verranno caricati mentre è in esecuzione
  3. Non consentire codice esterno al modulo per accedere ai suoi membri fino a dopo il costruttore ha terminato.

Fa questo per tutti gli assembly, comprese le librerie di classi e gli eseguibili, e per EXE esegue il costruttore del modulo prima di eseguire il metodo Main.

Vedere questo blog post per ulteriori informazioni sui costruttori.

In ogni caso, una soluzione completa al mio problema richiede parecchie parti:

  1. La seguente definizione di classe, definite all'interno di una "dll runtime linguaggio", a cui fa riferimento tutti i complessi prodotte 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 al montaggio dinamico che i tipi costruiti saranno creati. Contiene anche un "set hash" che memorizza l'insieme dei tipi già generato 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ò essere utilizzata per definire la classe generata. In un sistema reale, il metodo probabilmente prenderebbe una rappresentazione AST della classe, e solo fare la generazione è di per sé.

  2. assembly compilati che emettono i seguenti due riferimenti (indicati nella sintassi ILASM):

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

    Qui "SharedLib" è una libreria di runtime predefinita della lingua che comprende la classe "Loader" sopra definito e "$ Runtime "è l'assembly dinamico di runtime in cui verranno inseriti i tipi consolidati.

  3. Un "costruttore di moduli" all'interno di ogni assembly compilato nella lingua.

    Per quanto ne so, non ci sono linguaggi .NET che consentano di definire i Costruttori di moduli nell'origine. Il compilatore C++/CLI è l'unico compilatore che conosco che li generi. In IL, appaiono come questo, definito direttamente nel modulo e non all'interno eventuali definizioni del tipo:

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

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

    Nel caso di un assieme che utilizza i tipi tuple<i as int, j as int> e tuple<x as double, y as double, z as double> costruttore modulo dovrebbe generare tipi come segue (qui in C# sintassi):

    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 tupla sono generate come tipi generici per aggirare i problemi di accessibilità. Ciò consentirebbe al codice nell'assieme compilato di utilizzare tuple<x as Foo>, dove Foo era un tipo non pubblico.

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

    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 è stato il meccanismo Sono stato in grado di fornire l'equivalente approssimativo dei caricatori di classi personalizzate nel CLR.

Qualcuno sa di un modo più semplice per farlo?

+0

Ugh, come è diversa la definizione del costruttore del modulo rispetto al normale costruttore di classi? La differenza nell'uso di 'privatescope' rispetto a' private hidebysig'? –

+0

Ah, l'ho appena capito. Nessuna differenza tranne che il modulo cctor non è inserito in alcun tipo particolare. Non sapevo che potresti anche farlo :) –

-5

Penso che questo sia il tipo di cosa che il DLR deve fornire in C# 4.0. È difficile arrivare ancora con le informazioni, ma forse impareremo di più su PDC08. Aspettatamente aspettando di vedere la tua soluzione C# 3 però ... Immagino che usi tipi anonimi.