Assembly dinamici persistenti in .NET
Questo articolo fornisce osservazioni supplementari alla documentazione di riferimento per questa API.
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. Novità di .NET 9, la PersistedAssemblyBuilder classe aggiunge 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 PersistedAssemblyBuilder
di , è possibile eseguire solo un assembly generato e non salvarlo. Poiché l'assembly era solo in memoria, era difficile eseguire il debug. 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 PersistedAssemblyBuilder(AssemblyName, Assembly, IEnumerable<CustomAttributeBuilder>) 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 che verrà eseguito solo nella stessa versione di runtime della versione di runtime in cui è in esecuzione il compilatore (in genere in-proc), l'assembly principale può essere semplicementetypeof(object).Assembly
. L'esempio seguente illustra come creare e salvare un assembly in un flusso ed eseguirlo con l'assembly di runtime corrente:public static void CreateSaveAndRunAssembly() { 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 })); }
Se
Reflection.Emit
viene usato per generare un assembly destinato a un TFM specifico, aprire gli assembly di riferimento per il TFM specificato usandoMetadataLoadContext
e usare il valore della proprietà MetadataLoadContext.CoreAssembly percoreAssembly
. 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. È consigliabile usare i tipi restituiti dall'istanzaMetadataLoadContext
quando si fa riferimento ai tipi di core. Ad esempio, invece ditypeof(int)
, trovare ilSystem.Int32
tipo in perMetadataLoadContext.CoreAssembly
nome:public static void CreatePersistedAssemblyBuilderCoreAssemblyWithMetadataLoadContext(string refAssembliesPath) { PathAssemblyResolver resolver = new PathAssemblyResolver(Directory.GetFiles(refAssembliesPath, "*.dll")); using MetadataLoadContext context = new MetadataLoadContext(resolver); Assembly coreAssembly = context.CoreAssembly; PersistedAssemblyBuilder ab = new PersistedAssemblyBuilder(new AssemblyName("MyDynamicAssembly"), coreAssembly); TypeBuilder typeBuilder = ab.DefineDynamicModule("MyModule").DefineType("Test", TypeAttributes.Public); MethodBuilder methodBuilder = typeBuilder.DefineMethod("Method", MethodAttributes.Public, coreAssembly.GetType(typeof(int).FullName), Type.EmptyTypes); // .. add members and save the assembly }
Impostare il punto di ingresso per un eseguibile
Per impostare il punto di ingresso per un eseguibile o per 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:
public static void SetEntryPoint()
{
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);
}
Generare simboli e generare PDB
I metadati dei simboli vengono popolati nel pdbBuilder
parametro out quando si chiama il metodo in un'istanza GenerateMetadata(BlobBuilder, BlobBuilder)PersistedAssemblyBuilder
di . Per creare un assembly con un PDB portatile:
- Creare ISymbolDocumentWriter istanze con il ModuleBuilder.DefineDocument(String, Guid, Guid, Guid) metodo . Durante l'emissione del codice IL del metodo, genera anche le informazioni sul simbolo corrispondenti.
- Creare un'istanza PortablePdbBuilder usando l'istanza
pdbBuilder
prodotta dal GenerateMetadata(BlobBuilder, BlobBuilder) metodo . - Serializzare in
PortablePdbBuilder
un Blobe scrivere inBlob
un flusso di file PDB (solo se si genera un PDB autonomo). - Creare un'istanza DebugDirectoryBuilder di e aggiungere un DebugDirectoryBuilder.AddCodeViewEntry PDB autonomo o DebugDirectoryBuilder.AddEmbeddedPortablePdbEntry.
- Impostare l'argomento facoltativo
debugDirectoryBuilder
durante la creazione dell'istanza PEBuilder .
Nell'esempio seguente viene illustrato come generare informazioni sui simboli e generare un file PDB.
static void GenerateAssemblyWithPdb()
{
PersistedAssemblyBuilder ab = new PersistedAssemblyBuilder(new AssemblyName("MyAssembly"), typeof(object).Assembly);
ModuleBuilder mb = ab.DefineDynamicModule("MyModule");
TypeBuilder tb = mb.DefineType("MyType", TypeAttributes.Public | TypeAttributes.Class);
MethodBuilder mb1 = tb.DefineMethod("SumMethod", MethodAttributes.Public | MethodAttributes.Static, typeof(int), [typeof(int), typeof(int)]);
ISymbolDocumentWriter srcDoc = mb.DefineDocument("MySourceFile.cs", SymLanguageType.CSharp);
ILGenerator il = mb1.GetILGenerator();
LocalBuilder local = il.DeclareLocal(typeof(int));
local.SetLocalSymInfo("myLocal");
il.MarkSequencePoint(srcDoc, 7, 0, 7, 11);
...
il.Emit(OpCodes.Ret);
MethodBuilder entryPoint = tb.DefineMethod("Main", MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.Static);
ILGenerator il2 = entryPoint.GetILGenerator();
il2.BeginScope();
...
il2.EndScope();
...
tb.CreateType();
MetadataBuilder metadataBuilder = ab.GenerateMetadata(out BlobBuilder ilStream, out _, out MetadataBuilder pdbBuilder);
MethodDefinitionHandle entryPointHandle = MetadataTokens.MethodDefinitionHandle(entryPoint.MetadataToken);
DebugDirectoryBuilder debugDirectoryBuilder = GeneratePdb(pdbBuilder, metadataBuilder.GetRowCounts(), entryPointHandle);
ManagedPEBuilder peBuilder = new ManagedPEBuilder(
header: new PEHeaderBuilder(imageCharacteristics: Characteristics.ExecutableImage, subsystem: Subsystem.WindowsCui),
metadataRootBuilder: new MetadataRootBuilder(metadataBuilder),
ilStream: ilStream,
debugDirectoryBuilder: debugDirectoryBuilder,
entryPoint: entryPointHandle);
BlobBuilder peBlob = new BlobBuilder();
peBuilder.Serialize(peBlob);
using var fileStream = new FileStream("MyAssembly.exe", FileMode.Create, FileAccess.Write);
peBlob.WriteContentTo(fileStream);
}
static DebugDirectoryBuilder GeneratePdb(MetadataBuilder pdbBuilder, ImmutableArray<int> rowCounts, MethodDefinitionHandle entryPointHandle)
{
BlobBuilder portablePdbBlob = new BlobBuilder();
PortablePdbBuilder portablePdbBuilder = new PortablePdbBuilder(pdbBuilder, rowCounts, entryPointHandle);
BlobContentId pdbContentId = portablePdbBuilder.Serialize(portablePdbBlob);
// In case saving PDB to a file
using FileStream fileStream = new FileStream("MyAssemblyEmbeddedSource.pdb", FileMode.Create, FileAccess.Write);
portablePdbBlob.WriteContentTo(fileStream);
DebugDirectoryBuilder debugDirectoryBuilder = new DebugDirectoryBuilder();
debugDirectoryBuilder.AddCodeViewEntry("MyAssemblyEmbeddedSource.pdb", pdbContentId, portablePdbBuilder.FormatVersion);
// In case embedded in PE:
// debugDirectoryBuilder.AddEmbeddedPortablePdbEntry(portablePdbBlob, portablePdbBuilder.FormatVersion);
return debugDirectoryBuilder;
}
È inoltre possibile aggiungere CustomDebugInformation chiamando il MetadataBuilder.AddCustomDebugInformation(EntityHandle, GuidHandle, BlobHandle) metodo dall'istanza pdbBuilder
per aggiungere informazioni avanzate PDB di incorporamento e indicizzazione dell'origine.
private static void EmbedSource(MetadataBuilder pdbBuilder)
{
byte[] sourceBytes = File.ReadAllBytes("MySourceFile2.cs");
BlobBuilder sourceBlob = new BlobBuilder();
sourceBlob.WriteBytes(sourceBytes);
pdbBuilder.AddCustomDebugInformation(MetadataTokens.DocumentHandle(1),
pdbBuilder.GetOrAddGuid(new Guid("0E8A571B-6926-466E-B4AD-8AB04611F5FE")), pdbBuilder.GetOrAddBlob(sourceBlob));
}
Aggiungere risorse con PersistedAssemblyBuilder
È possibile chiamare MetadataBuilder.AddManifestResource(ManifestResourceAttributes, StringHandle, EntityHandle, UInt32) per aggiungere tutte le risorse necessarie. Flussi deve essere concatenato in un BlobBuilder oggetto passato all'argomento ManagedPEBuilder . Nell'esempio seguente viene illustrato come creare risorse e collegarlo all'assembly creato.
public static void SetResource()
{
PersistedAssemblyBuilder ab = new PersistedAssemblyBuilder(new AssemblyName("MyAssembly"), typeof(object).Assembly);
ab.DefineDynamicModule("MyModule");
MetadataBuilder metadata = ab.GenerateMetadata(out BlobBuilder ilStream, out _);
using MemoryStream stream = new MemoryStream();
ResourceWriter myResourceWriter = new ResourceWriter(stream);
myResourceWriter.AddResource("AddResource 1", "First added resource");
myResourceWriter.AddResource("AddResource 2", "Second added resource");
myResourceWriter.AddResource("AddResource 3", "Third added resource");
myResourceWriter.Close();
BlobBuilder resourceBlob = new BlobBuilder();
resourceBlob.WriteBytes(stream.ToArray());
metadata.AddManifestResource(ManifestResourceAttributes.Public, metadata.GetOrAddString("MyResource"), default, (uint)resourceBlob.Count);
ManagedPEBuilder peBuilder = new ManagedPEBuilder(
header: new PEHeaderBuilder(imageCharacteristics: Characteristics.ExecutableImage | Characteristics.Dll),
metadataRootBuilder: new MetadataRootBuilder(metadata),
ilStream: ilStream,
managedResources: resourceBlob);
BlobBuilder blob = new BlobBuilder();
peBuilder.Serialize(blob);
using var fileStream = new FileStream("MyAssemblyWithResource.dll", FileMode.Create, FileAccess.Write);
blob.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.