Trvalá dynamická sestavení v .NET
Tento článek obsahuje doplňující poznámky k referenční dokumentaci pro toto rozhraní API.
Rozhraní API AssemblyBuilder.Save se původně nepře portovalo do .NET (Core), protože implementace výrazně závisela na nativním kódu specifickém pro Windows, který nebyl také portován. V rozhraní .NET 9 přidává třída PersistedAssemblyBuilder plně spravovanou implementaci Reflection.Emit
, která podporuje ukládání. Tato implementace nemá žádnou závislost na existující implementaci Reflection.Emit
specifické pro modul runtime. To znamená, že v .NET existují dvě různé implementace, spustitelné a trvalé. Pokud chcete spustit trvalé sestavení, nejprve ho uložte do datového proudu paměti nebo souboru a pak ho načtěte zpět.
Před PersistedAssemblyBuilder
byste mohli spustit pouze vygenerované sestavení a neukládat ho. Vzhledem k tomu, že sestavení bylo pouze uloženo v paměti, bylo obtížné ho odladit. Výhody ukládání dynamického sestavení do souboru jsou:
- Vygenerované sestavení můžete ověřit pomocí nástrojů, jako je ILVerify, nebo dekompilovat, a ručně ho prozkoumat pomocí nástrojů, jako je ILSpy.
- Uložené sestavení lze načíst přímo, není nutné znovu kompilovat, což může zkrátit dobu spuštění aplikace.
K vytvoření PersistedAssemblyBuilder
instance použijte konstruktor PersistedAssemblyBuilder(AssemblyName, Assembly, IEnumerable<CustomAttributeBuilder>). Parametr coreAssembly
se používá k řešení základních typů za běhu a lze ho použít k řešení verzování referenčních sestavení.
Pokud
Reflection.Emit
slouží ke generování sestavení, které se spustí pouze ve stejné verzi modulu runtime jako verze modulu runtime, na které kompilátor běží (obvykle v proc), základní sestavení může být jednodušetypeof(object).Assembly
. Následující příklad ukazuje, jak vytvořit a uložit sestavení do datového proudu a spustit ho s aktuálním sestavením runtime: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 })); }
Pokud se
Reflection.Emit
používá k vygenerování sestavení, které cílí na konkrétní TFM, otevřete referenční sestavení pro danou sadu TFM pomocíMetadataLoadContext
a použijte hodnotu MetadataLoadContext.CoreAssembly vlastnost procoreAssembly
. Tato hodnota umožňuje generátoru spustit na jedné verzi modulu runtime .NET a cílit na jinou verzi modulu runtime .NET. Při odkazování na základní typy byste měli použít typy vrácené instancíMetadataLoadContext
. Například místotypeof(int)
vyhledejte typSystem.Int32
vMetadataLoadContext.CoreAssembly
podle názvu.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 }
Nastavení vstupního bodu pro spustitelný soubor
Chcete-li nastavit vstupní bod spustitelného souboru nebo nastavit další možnosti pro soubor sestavení, můžete volat metodu public MetadataBuilder GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder mappedFieldData)
a pomocí vyplněných metadat vygenerovat sestavení s požadovanými možnostmi, například:
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);
}
Generování symbolů a generování PDB
Metadata symbolů se vyplní do výstupního parametru pdbBuilder
, když zavoláte metodu GenerateMetadata(BlobBuilder, BlobBuilder) u instance PersistedAssemblyBuilder
. Vytvoření sestavení s přenosným souborem PDB:
- Pomocí metody ModuleBuilder.DefineDocument(String, Guid, Guid, Guid) vytvořte instance ISymbolDocumentWriter. Při generování IL metody rovněž generujte příslušné symbolické informace.
- Vytvořte instanci PortablePdbBuilder pomocí instance
pdbBuilder
vytvořené metodou GenerateMetadata(BlobBuilder, BlobBuilder). - Serializujte
PortablePdbBuilder
do Bloba zapišteBlob
do datového proudu souboru PDB (pouze pokud generujete samostatný PDB). - Vytvořte instanci DebugDirectoryBuilder a přidejte DebugDirectoryBuilder.AddCodeViewEntry (samostatný soubor PDB) nebo DebugDirectoryBuilder.AddEmbeddedPortablePdbEntry.
- Při vytváření instance PEBuilder nastavte volitelný argument
debugDirectoryBuilder
.
Následující příklad ukazuje, jak vygenerovat informace o symbolu a vygenerovat soubor 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;
}
Dále můžete přidat CustomDebugInformation voláním metody MetadataBuilder.AddCustomDebugInformation(EntityHandle, GuidHandle, BlobHandle) z instance pdbBuilder
pro přidání zdrojového vkládání a pokročilé PDB informace o indexování zdroje.
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));
}
Přidání prostředků pomocí PersistedAssemblyBuilder
Můžete volat MetadataBuilder.AddManifestResource(ManifestResourceAttributes, StringHandle, EntityHandle, UInt32) a přidat tolik prostředků, kolik potřebujete. Datové proudy musí být zřetězeny do jednoho BlobBuilder, který pak předáte jako argument ManagedPEBuilder. Následující příklad ukazuje, jak vytvořit prostředky a připojit ho k vytvořenému sestavení.
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);
}
Následující příklad ukazuje, jak načíst zdroje z vytvořené sestavy.
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}");
}
}
Poznámka
Tokeny metadat pro všechny členy jsou naplněny při operaci Save. Nepoužívejte tokeny vygenerovaného typu a jeho členů před uložením, protože budou mít výchozí hodnoty nebo vyvolají výjimky. Je bezpečné používat tokeny pro typy, na které se odkazuje, které nejsou generovány.
Některá rozhraní API, která nejsou důležitá pro generování sestavení, nejsou implementována. například GetCustomAttributes()
není implementováno. Při implementaci modulu runtime jste po vytvoření typu mohli tato rozhraní API použít. U trvalých AssemblyBuilder
vyhodí NotSupportedException
nebo NotImplementedException
. Pokud máte scénář, který vyžaduje tato rozhraní API, vytvořte problém v úložišti dotnet/runtime.
Pro alternativní způsob, jak generovat soubory sestavení, se podívejte na MetadataBuilder.