Persistierte dynamische Baugruppen in .NET
Dieser Artikel enthält ergänzende Hinweise zur Referenzdokumentation für diese API.
Die AssemblyBuilder.Save-API wurde ursprünglich nicht zu .NET (Core) portiert, da die Implementierung stark von windowsspezifischem systemeigenem Code abhängt, der ebenfalls nicht portiert wurde. Neu in .NET 9 fügt die PersistedAssemblyBuilder Klasse eine vollständig verwaltete Reflection.Emit
Implementierung hinzu, die das Speichern unterstützt. Diese Implementierung hat keine Abhängigkeit von der bereits vorhandenen, laufzeitspezifischen Reflection.Emit
Implementierung. Das heißt, es gibt nun zwei verschiedene Implementierungen in .NET, die ausgeführt und beibehalten werden können. Wenn Sie die persistierte Assembly ausführen möchten, speichern Sie sie zuerst in einem Speicherstrom oder einer Datei, dann laden Sie sie wieder.
Vor PersistedAssemblyBuilder
konnten Sie nur eine generierte Assembly ausführen und nicht speichern. Da die Assembly nur im Arbeitsspeicher war, war es schwierig zu debuggen. Vorteile des Speicherns einer dynamischen Assembly in einer Datei sind:
- Sie können die generierte Assembly mit Tools wie ILVerify überprüfen oder dekompilieren und manuell mit Tools wie ILSpy untersuchen.
- Die gespeicherte Assembly kann direkt geladen werden, sie muss nicht erneut kompiliert werden, wodurch die Startzeit der Anwendung verringert werden kann.
Verwenden Sie den PersistedAssemblyBuilder(AssemblyName, Assembly, IEnumerable<CustomAttributeBuilder>)-Konstruktor, um eine PersistedAssemblyBuilder
Instanz zu erstellen. DercoreAssembly
-Parameter wird zur Auflösung von Basis-Laufzeittypen verwendet und kann zur Auflösung von Referenz-Assembly-Versionen verwendet werden:
Wenn
Reflection.Emit
verwendet wird, um eine Assembly zu erzeugen, die nur auf derselben Laufzeitversion ausgeführt wird wie die Laufzeitversion, auf der der Compiler läuft (typischerweise in-proc), kann die Core-Assembly einfach seintypeof(object).Assembly
. Das folgende Beispiel zeigt, wie eine Baugruppe erstellt und in einem Stream gespeichert und mit der aktuellen Laufzeitbaugruppe ausgeführt wird: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 })); }
Wenn
Reflection.Emit
verwendet wird, um eine Assembly zu generieren, die auf ein bestimmtes TFM abzielt, öffnen Sie die Referenz-Assemblies für das angegebene TFM mitMetadataLoadContext
und verwenden Sie den Wert der Eigenschaft MetadataLoadContext.CoreAssemblyfürcoreAssembly
. Mit diesem Wert kann der Generator auf einer .NET-Laufzeitversion ausgeführt und auf eine andere .NET-Laufzeitversion ausgerichtet werden. Sie sollten Typen verwenden, die von derMetadataLoadContext
Instanz beim Verweisen auf Kerntypen zurückgegeben werden. Zum Beispiel, statttypeof(int)
, finden Sie dieSystem.Int32
inMetadataLoadContext.CoreAssembly
nach Namen: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 }
Einstiegspunkt für eine ausführbare Datei festlegen
Um den Einstiegspunkt für eine ausführbare Datei festzulegen oder andere Optionen für die Assemblydatei festzulegen, können Sie die public MetadataBuilder GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder mappedFieldData)
-Methode aufrufen und die aufgefüllten Metadaten verwenden, um die Assembly mit den gewünschten Optionen zu generieren, z. B.:
public static void SetEntryPoint()
{
PersistedAssemblyBuilder ab = new(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);
ManagedPEBuilder peBuilder = new(
header: PEHeaderBuilder.CreateExecutableHeader(),
metadataRootBuilder: new MetadataRootBuilder(metadataBuilder),
ilStream: ilStream,
mappedFieldData: fieldData,
entryPoint: MetadataTokens.MethodDefinitionHandle(entryPoint.MetadataToken));
BlobBuilder peBlob = new();
peBuilder.Serialize(peBlob);
// Create the executable:
using FileStream fileStream = new("MyAssembly.exe", FileMode.Create, FileAccess.Write);
peBlob.WriteContentTo(fileStream);
}
Symbole ausgeben und PDB generieren
Die Metadaten der Symbole werden in den pdbBuilder
out-Parameter eingetragen, wenn Sie dieGenerateMetadata(BlobBuilder, BlobBuilder)-Methode für einePersistedAssemblyBuilder
-Instanz aufrufen. So erstellen Sie eine Baugruppe mit einer portablen PDB:
- Erstellen SieISymbolDocumentWriter Instanzen mit derModuleBuilder.DefineDocument(String, Guid, Guid, Guid) Methode. Bei der Ausgabe der AWL der Methode werden auch die entsprechenden Symbolinformationen ausgegeben.
- Erstellen Sie eine PortablePdbBuilder-Instanz unter Verwendung der durch die GenerateMetadata(BlobBuilder, BlobBuilder)-Methode erzeugten
pdbBuilder
-Instanz. - Serialisieren Sie die
PortablePdbBuilder
in eineBlob, und schreiben Sie dieBlob
in einen PDB-Dateistrom (nur wenn Sie eine eigenständige PDB erzeugen). - Erstellen Sie eineDebugDirectoryBuilder-Instanz und fügen Sie eineDebugDirectoryBuilder.AddCodeViewEntry(eigenständige PDB) oder DebugDirectoryBuilder.AddEmbeddedPortablePdbEntryhinzu.
- Legen Sie das optionale
debugDirectoryBuilder
-Argument fest, wenn Sie die PEBuilder-Instanz erstellen.
Das folgende Beispiel zeigt, wie Symbolinformationen ausgegeben und eine PDB-Datei generiert werden.
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;
}
Außerdem können SieCustomDebugInformationhinzufügen, indem Sie die MetadataBuilder.AddCustomDebugInformation(EntityHandle, GuidHandle, BlobHandle)-Methode von der pdbBuilder
-Instanz aufrufen, um erweiterte PDB-Informationen zur Quelleneinbettung und Quellenindizierung hinzuzufügen.
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));
}
Hinzufügen von Ressourcen mit PersistedAssemblyBuilder
Sie können MetadataBuilder.AddManifestResource(ManifestResourceAttributes, StringHandle, EntityHandle, UInt32) aufrufen, um beliebig viele Ressourcen hinzuzufügen. Die Datenströme müssen zu einem BlobBuilderverkettet werden, das Sie an das ManagedPEBuilder-Argument übergeben. Das folgende Beispiel zeigt, wie Sie Ressourcen erstellen und an die erstellte Assembly anfügen.
public static void SetResource()
{
PersistedAssemblyBuilder ab = new(new AssemblyName("MyAssembly"), typeof(object).Assembly);
ab.DefineDynamicModule("MyModule");
MetadataBuilder metadata = ab.GenerateMetadata(out BlobBuilder ilStream, out _);
using MemoryStream stream = new();
ResourceWriter myResourceWriter = new(stream);
myResourceWriter.AddResource("AddResource 1", "First added resource");
myResourceWriter.AddResource("AddResource 2", "Second added resource");
myResourceWriter.AddResource("AddResource 3", "Third added resource");
myResourceWriter.Close();
byte[] data = stream.ToArray();
BlobBuilder resourceBlob = new();
resourceBlob.WriteInt32(data.Length);
resourceBlob.WriteBytes(data);
metadata.AddManifestResource(
ManifestResourceAttributes.Public,
metadata.GetOrAddString("MyResource.resources"),
implementation: default,
offset: 0);
ManagedPEBuilder peBuilder = new(
header: PEHeaderBuilder.CreateLibraryHeader(),
metadataRootBuilder: new MetadataRootBuilder(metadata),
ilStream: ilStream,
managedResources: resourceBlob);
BlobBuilder blob = new();
peBuilder.Serialize(blob);
// Create the assembly:
using FileStream fileStream = new("MyAssemblyWithResource.dll", FileMode.Create, FileAccess.Write);
blob.WriteContentTo(fileStream);
}
Das folgende Beispiel zeigt, wie Ressourcen aus der erstellten Assembly gelesen werden.
public static void ReadResource()
{
Assembly readAssembly = Assembly.LoadFile(Path.Combine(
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
"MyAssemblyWithResource.dll"));
// Use ResourceManager.GetString() to read the resources.
ResourceManager rm = new("MyResource", readAssembly);
Console.WriteLine("Using ResourceManager.GetString():");
Console.WriteLine($"{rm.GetString("AddResource 1", CultureInfo.InvariantCulture)}");
Console.WriteLine($"{rm.GetString("AddResource 2", CultureInfo.InvariantCulture)}");
Console.WriteLine($"{rm.GetString("AddResource 3", CultureInfo.InvariantCulture)}");
// Use ResourceSet to enumerate the resources.
Console.WriteLine();
Console.WriteLine("Using ResourceSet:");
ResourceSet resourceSet = rm.GetResourceSet(CultureInfo.InvariantCulture, createIfNotExists: true, tryParents: false);
foreach (DictionaryEntry entry in resourceSet)
{
Console.WriteLine($"Key: {entry.Key}, Value: {entry.Value}");
}
// Use ResourceReader to enumerate the resources.
Console.WriteLine();
Console.WriteLine("Using ResourceReader:");
using Stream stream = readAssembly.GetManifestResourceStream("MyResource.resources")!;
using ResourceReader reader = new(stream);
foreach (DictionaryEntry entry in reader)
{
Console.WriteLine($"Key: {entry.Key}, Value: {entry.Value}");
}
}
Anmerkung
Die Metadaten-Token für alle Mitglieder werden bei der Operation Saveausgefüllt. Verwenden Sie die Token eines generierten Typs und seine Mitglieder nicht vor dem Speichern, da sie Standardwerte haben oder Ausnahmen auslösen. Es ist sicher, Token für Typen zu verwenden, die referenziert und nicht generiert werden.
Einige APIs, die für das Emittieren einer Assembly nicht wichtig sind, werden nicht implementiert. beispielsweise wird GetCustomAttributes()
nicht implementiert. Mit der Laufzeitimplementierung konnten Sie diese APIs nach dem Erstellen des Typs verwenden. Für die anhaltendeAssemblyBuilder
, werfen sie NotSupportedException
oderNotImplementedException
. Wenn Sie ein Szenario haben, das diese APIs erfordert, melden Sie ein Problem im Repository dotnet/runtime.
Eine alternative Möglichkeit zum Generieren von Assemblydateien finden Sie unter MetadataBuilder.