Classe System.Reflection.Emit.AssemblyBuilder

Questo articolo fornisce osservazioni supplementari alla documentazione di riferimento per questa API.

Un assembly dinamico è un assembly creato usando le API Reflection Emit. Un assembly dinamico può fare riferimento a tipi definiti in un altro assembly dinamico o statico. È possibile usare AssemblyBuilder per generare assembly dinamici in memoria ed eseguirne il codice durante la stessa esecuzione dell'applicazione. In .NET 9 è stato aggiunto un nuovo PersistedAssemblyBuilder con implementazione completamente gestita di reflection emit che consente di salvare l'assembly in un file. In .NET Framework è possibile eseguire entrambe le operazioni, eseguire l'assembly dinamico e salvarlo in un file. L'assembly dinamico creato per il salvataggio viene chiamato assembly persistente, mentre l'assembly regolare di sola memoria viene chiamato temporaneo o eseguibile. In .NET Framework un assembly dinamico può essere costituito da uno o più moduli dinamici. In .NET Core e .NET 5+, un assembly dinamico può essere costituito solo da un modulo dinamico.

Il modo in cui si crea un'istanza AssemblyBuilder è diversa per ogni implementazione, ma altri passaggi per la definizione di un modulo, un tipo, un metodo o un'enumerazione e per la scrittura di IL sono molto simili.

Assembly dinamici eseguibili in .NET

Per ottenere un oggetto eseguibile AssemblyBuilder , utilizzare il AssemblyBuilder.DefineDynamicAssembly metodo . È possibile creare assembly dinamici usando una delle modalità di accesso seguenti:

La modalità di accesso deve essere specificata specificando il valore appropriato AssemblyBuilderAccess nella chiamata al AssemblyBuilder.DefineDynamicAssembly metodo quando viene definito l'assembly dinamico e non può essere modificato in un secondo momento. Il runtime usa la modalità di accesso di un assembly dinamico per ottimizzare la rappresentazione interna dell'assembly.

Nell'esempio seguente viene illustrato come creare ed eseguire un assembly:

public void CreateAndRunAssembly(string assemblyPath)
{
    AssemblyBuilder ab = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("MyAssembly"), AssemblyBuilderAccess.Run);
    ModuleBuilder mob = ab.DefineDynamicModule("MyModule");
    TypeBuilder tb = mob.DefineType("MyType", TypeAttributes.Public | TypeAttributes.Class);
    MethodBuilder mb = tb.DefineMethod("SumMethod", MethodAttributes.Public | MethodAttributes.Static,
                                                                   typeof(int), new Type[] {typeof(int), typeof(int)});
    ILGenerator il = mb.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldarg_1);
    il.Emit(OpCodes.Add);
    il.Emit(OpCodes.Ret);

    Type type = tb.CreateType();

    MethodInfo method = type.GetMethod("SumMethod");
    Console.WriteLine(method.Invoke(null, new object[] { 5, 10 }));
}

Assembly dinamici persistenti in .NET

L'API AssemblyBuilder.Save non è stata originariamente convertito in .NET (Core) perché l'implementazione dipendeva in larga misura dal codice nativo specifico di Windows che non è stato convertito. In .NET 9 è stata aggiunta un'implementazione completamente gestita Reflection.Emit che supporta il salvataggio. Questa implementazione non ha alcuna dipendenza dall'implementazione preesistente specifica del Reflection.Emit runtime. Ciò significa che in .NET sono disponibili due implementazioni diverse, eseguibili e persistenti. Per eseguire l'assembly persistente, salvarlo prima in un flusso di memoria o in un file, quindi caricarlo di nuovo. Prima di PersistedAssemblyBuilder, poiché era possibile eseguire solo un assembly generato e non salvarlo, era difficile eseguire il debug di questi assembly in memoria. I vantaggi del salvataggio di un assembly dinamico in un file sono:

  • È possibile verificare l'assembly generato con strumenti come ILVerify o decompilarlo e esaminarlo manualmente con strumenti come ILSpy.
  • L'assembly salvato può essere caricato direttamente, non è necessario compilarlo di nuovo, riducendo così il tempo di avvio dell'applicazione.

Per creare un'istanza PersistedAssemblyBuilder di , usare il public PersistedAssemblyBuilder(AssemblyName name, Assembly coreAssembly, IEnumerable<CustomAttributeBuilder>? assemblyAttributes = null) costruttore . Il coreAssembly parametro viene usato per risolvere i tipi di runtime di base e può essere usato per risolvere il controllo delle versioni degli assembly di riferimento:

  • Se Reflection.Emit viene usato per generare un assembly destinato a un TFM specifico, aprire gli assembly di riferimento per il TFM specificato usando MetadataLoadContext e usare il valore della proprietà MetadataLoadContext.CoreAssembly per coreAssembly. Questo valore consente al generatore di essere eseguito in una versione di runtime .NET e di specificare come destinazione una versione di runtime .NET diversa.

  • Se Reflection.Emit viene usato per generare un assembly che verrà eseguito solo nella stessa versione di runtime della versione di runtime in cui il compilatore è in esecuzione (in genere in-proc), l'assembly principale può essere typeof(object).Assembly. Gli assembly di riferimento non sono necessari in questo caso.

Nell'esempio seguente viene illustrato come creare e salvare un assembly in un flusso ed eseguirlo:

public void CreateSaveAndRunAssembly(string assemblyPath)
{
    PersistedAssemblyBuilder ab = new PersistedAssemblyBuilder(new AssemblyName("MyAssembly"), typeof(object).Assembly);
    ModuleBuilder mob = ab.DefineDynamicModule("MyModule");
    TypeBuilder tb = mob.DefineType("MyType", TypeAttributes.Public | TypeAttributes.Class);
    MethodBuilder meb = tb.DefineMethod("SumMethod", MethodAttributes.Public | MethodAttributes.Static,
                                                                   typeof(int), new Type[] {typeof(int), typeof(int)});
    ILGenerator il = meb.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldarg_1);
    il.Emit(OpCodes.Add);
    il.Emit(OpCodes.Ret);

    tb.CreateType();

    using var stream = new MemoryStream();
    ab.Save(stream);  // or pass filename to save into a file
    stream.Seek(0, SeekOrigin.Begin);
    Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(stream);
    MethodInfo method = assembly.GetType("MyType").GetMethod("SumMethod");
    Console.WriteLine(method.Invoke(null, new object[] { 5, 10 }));
}

Per impostare il punto di ingresso per un eseguibile e/o impostare altre opzioni per il file di assembly, è possibile chiamare il public MetadataBuilder GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder mappedFieldData) metodo e usare i metadati popolati per generare l'assembly con le opzioni desiderate, ad esempio:

PersistedAssemblyBuilder ab = new PersistedAssemblyBuilder(new AssemblyName("MyAssembly"), typeof(object).Assembly);
TypeBuilder tb = ab.DefineDynamicModule("MyModule").DefineType("MyType", TypeAttributes.Public | TypeAttributes.Class);
// ...
MethodBuilder entryPoint = tb.DefineMethod("Main", MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.Static);
ILGenerator il2 = entryPoint.GetILGenerator();
// ...
il2.Emit(OpCodes.Ret);
tb.CreateType();

MetadataBuilder metadataBuilder = ab.GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder fieldData);
PEHeaderBuilder peHeaderBuilder = new PEHeaderBuilder(imageCharacteristics: Characteristics.ExecutableImage);

ManagedPEBuilder peBuilder = new ManagedPEBuilder(
                header: peHeaderBuilder,
                metadataRootBuilder: new MetadataRootBuilder(metadataBuilder),
                ilStream: ilStream,
                mappedFieldData: fieldData,
                entryPoint: MetadataTokens.MethodDefinitionHandle(entryPoint.MetadataToken));

BlobBuilder peBlob = new BlobBuilder();
peBuilder.Serialize(peBlob);

// in case saving to a file:
using var fileStream = new FileStream("MyAssembly.exe", FileMode.Create, FileAccess.Write);
peBlob.WriteContentTo(fileStream); 

Nota

I token di metadati per tutti i membri vengono popolati nell'operazione Save . Non usare i token di un tipo generato e i relativi membri prima del salvataggio, perché avranno valori predefiniti o generano eccezioni. È sicuro usare i token per i tipi a cui viene fatto riferimento, non generato.

Alcune API che non sono importanti per l'emissione di un assembly non vengono implementate; ad esempio, GetCustomAttributes() non viene implementato. Con l'implementazione del runtime, è stato possibile usare queste API dopo aver creato il tipo. Per l'oggetto persistente AssemblyBuilder, generano NotSupportedException o NotImplementedException. Se si ha uno scenario che richiede tali API, inviare un problema nel repository dotnet/runtime.

Per un modo alternativo per generare file di assembly, vedere MetadataBuilder.

Assembly dinamici persistenti in .NET Framework

In .NET Framework gli assembly dinamici e i moduli possono essere salvati in file. Per supportare questa funzionalità, l'enumerazione AssemblyBuilderAccess dichiara due campi aggiuntivi: Save e RunAndSave.

I moduli dinamici nell'assembly dinamico persistente vengono salvati quando l'assembly dinamico viene salvato usando il Save metodo . Per generare un eseguibile, è necessario chiamare il SetEntryPoint metodo per identificare il metodo che rappresenta il punto di ingresso dell'assembly. Gli assembly vengono salvati come DLL per impostazione predefinita, a meno che il SetEntryPoint metodo non richieda la generazione di un'applicazione console o di un'applicazione basata su Windows.

Nell'esempio seguente viene illustrato come creare, salvare ed eseguire un assembly usando .NET Framework.

public void CreateRunAndSaveAssembly(string assemblyPath)
{
    AssemblyBuilder ab = Thread.GetDomain().DefineDynamicAssembly(new AssemblyName("MyAssembly"), AssemblyBuilderAccess.RunAndSave);
    ModuleBuilder mob = ab.DefineDynamicModule("MyAssembly.dll");
    TypeBuilder tb = mob.DefineType("MyType", TypeAttributes.Public | TypeAttributes.Class);
    MethodBuilder meb = tb.DefineMethod("SumMethod", MethodAttributes.Public | MethodAttributes.Static,
                                                                   typeof(int), new Type[] {typeof(int), typeof(int)});
    ILGenerator il = meb.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldarg_1);
    il.Emit(OpCodes.Add);
    il.Emit(OpCodes.Ret);

    Type type = tb.CreateType();

    MethodInfo method = type.GetMethod("SumMethod");
    Console.WriteLine(method.Invoke(null, new object[] { 5, 10 }));
    ab.Save("MyAssembly.dll");
}

Alcuni metodi della classe base Assembly , ad esempio GetModules e GetLoadedModules, non funzioneranno correttamente quando vengono chiamati da AssemblyBuilder oggetti . È possibile caricare l'assembly dinamico definito e chiamare i metodi nell'assembly caricato. Ad esempio, per assicurarsi che i moduli delle risorse siano inclusi nell'elenco dei moduli restituiti, chiamare GetModules sull'oggetto caricato Assembly . Se un assembly dinamico contiene più moduli dinamici, il nome del file manifesto dell'assembly deve corrispondere al nome del modulo specificato come primo argomento del DefineDynamicModule metodo.

La firma di un assembly dinamico tramite KeyPair non è efficace fino a quando l'assembly non viene salvato su disco. Pertanto, i nomi sicuri non funzioneranno con assembly dinamici temporanei.

Gli assembly dinamici possono fare riferimento a tipi definiti in un altro assembly. Un assembly dinamico temporaneo può fare riferimento in modo sicuro a tipi definiti in un altro assembly dinamico temporaneo, a un assembly dinamico persistente o a un assembly statico. Tuttavia, Common Language Runtime non consente a un modulo dinamico persistente di fare riferimento a un tipo definito in un modulo dinamico temporaneo. Questo perché quando il modulo dinamico persistente viene caricato dopo essere stato salvato su disco, il runtime non può risolvere i riferimenti ai tipi definiti nel modulo dinamico temporaneo.

Restrizioni relative all'emissione di domini applicazione remoti

Alcuni scenari richiedono la creazione e l'esecuzione di un assembly dinamico in un dominio applicazione remoto. Reflection emit non consente l'emissione di un assembly dinamico direttamente in un dominio applicazione remoto. La soluzione consiste nel generare l'assembly dinamico nel dominio applicazione corrente, salvare l'assembly dinamico generato su disco e quindi caricare l'assembly dinamico nel dominio applicazione remoto. I domini remoti e dell'applicazione sono supportati solo in .NET Framework.