Bevarade dynamiska sammansättningar i .NET
Den här artikeln innehåller ytterligare kommentarer till referensdokumentationen för det här API:et.
Det AssemblyBuilder.Save API:et portades inte ursprungligen till .NET (Core) eftersom implementeringen var starkt beroende av Windows-specifik intern kod som inte heller portades. Den PersistedAssemblyBuilder-klassen är ny i .NET 9 och lägger till en fullständigt hanterad Reflection.Emit
implementering som stöder sparande. Den här implementeringen är inte beroende av den befintliga, körningsspecifika Reflection.Emit
implementeringen. Det betyder att det nu finns två olika implementeringar i .NET, som kan köras och sparas. Om du vill köra den beständiga sammansättningen sparar du den först i en minnesström eller en fil och läser sedan in den igen.
Innan PersistedAssemblyBuilder
kunde du bara köra en genererad sammansättning och inte spara den. Eftersom sammansättningen endast var minnesintern var det svårt att felsöka. Fördelarna med att spara en dynamisk sammansättning i en fil är:
- Du kan verifiera den genererade sammansättningen med verktyg som ILVerify eller dekompilera och manuellt undersöka den med verktyg som ILSpy.
- Den sparade sammansättningen kan läsas in direkt, du behöver inte kompilera igen, vilket kan minska programmets starttid.
Om du vill skapa en PersistedAssemblyBuilder
instans använder du PersistedAssemblyBuilder(AssemblyName, Assembly, IEnumerable<CustomAttributeBuilder>) konstruktorn. Parametern coreAssembly
används för att lösa bas-körningstyper och kan användas för att lösa versionshantering av referenssamlingar.
Om
Reflection.Emit
används för att generera ett assembly som endast kommer att köras på samma körningsversion som den körningsversion som kompilatorn körs på (vanligtvis på plats), kan kärnassemblyt enkelt varatypeof(object).Assembly
. I följande exempel visas hur du skapar och sparar en assembly i en dataström och kör den med den nuvarande runtime-assemblyn.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 })); }
Om
Reflection.Emit
används för att generera en sammansättning som riktar sig mot en specifik TFM öppnar du referenssammansättningarna för den angivna TFM:en med hjälp avMetadataLoadContext
och använder värdet för egenskapen MetadataLoadContext.CoreAssembly förcoreAssembly
. Med det här värdet kan generatorn köras på en .NET-körningsversion och rikta in sig på en annan .NET-körningsversion. Du bör använda typer som returneras av denMetadataLoadContext
instansen när du refererar till kärntyper. Till exempel, istället förtypeof(int)
, sök uppSystem.Int32
-typen iMetadataLoadContext.CoreAssembly
med namnet: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 }
Ange startpunkt för en körbar fil
Om du vill ange startpunkten för en körbar fil eller ange andra alternativ för sammansättningsfilen kan du anropa metoden public MetadataBuilder GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder mappedFieldData)
och använda ifyllda metadata för att generera sammansättningen med önskade alternativ, till exempel:
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);
}
Avge symboler och generera PDB
Symbolmetadata fylls i i parametern pdbBuilder
out när du anropar metoden GenerateMetadata(BlobBuilder, BlobBuilder) på en PersistedAssemblyBuilder
-instans. Så här skapar du en sammansättning med ett portabelt PDB:
- Skapa ISymbolDocumentWriter instanser med metoden ModuleBuilder.DefineDocument(String, Guid, Guid, Guid). När du sänder ut metodens IL genererar du även motsvarande symbolinformation.
- Skapa en PortablePdbBuilder-instans med hjälp av den
pdbBuilder
instans som skapas av metoden GenerateMetadata(BlobBuilder, BlobBuilder). - Serialisera
PortablePdbBuilder
till en Bloboch skrivBlob
till en PDB-filström (endast om du genererar en fristående PDB). - Skapa en DebugDirectoryBuilder-instans och lägg till en DebugDirectoryBuilder.AddCodeViewEntry (fristående PDB) eller DebugDirectoryBuilder.AddEmbeddedPortablePdbEntry.
- Ange det valfria argumentet
debugDirectoryBuilder
när du skapar PEBuilder-instansen.
I följande exempel visas hur du genererar symbolinformation och genererar en PDB-fil.
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;
}
Dessutom kan du lägga till CustomDebugInformation genom att anropa metoden MetadataBuilder.AddCustomDebugInformation(EntityHandle, GuidHandle, BlobHandle) från pdbBuilder
-instansen för att lägga till källinbäddning och källindexering av avancerad PDB-information.
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));
}
Lägga till resurser med PersistedAssemblyBuilder
Du kan anropa MetadataBuilder.AddManifestResource(ManifestResourceAttributes, StringHandle, EntityHandle, UInt32) för att lägga till så många resurser som behövs. Strömmarna måste sammanfogas till en BlobBuilder som du skickar in i argumentet ManagedPEBuilder. I följande exempel visas hur du skapar resurser och kopplar dem till den sammansättning som skapas.
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);
}
I följande exempel visas hur du läser resurser från den skapade sammansättningen.
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}");
}
}
Notera
Metadatatoken för alla medlemmar fylls i under operationen Save. Använd inte token för en genererad typ och dess medlemmar innan du sparar dem, eftersom de har standardvärden eller utlöser undantag. Det är säkert att använda token för typer som refereras, inte genereras.
Vissa API:er som inte är viktiga för att generera en sammansättning implementeras inte. GetCustomAttributes()
implementeras till exempel inte. Med körningsimplementeringen kunde du använda dessa API:er efter att du hade skapat typen. För den beständiga AssemblyBuilder
kastar de NotSupportedException
eller NotImplementedException
. Om du har ett scenario som kräver dessa API:er kan du ange ett problem i dotnet/runtime-lagringsplatsen.
Ett alternativt sätt att generera sammansättningsfiler finns i MetadataBuilder.