Assemblies dinâmicos persistentes no .NET
Este artigo fornece comentários complementares à documentação de referência para esta API.
A API de AssemblyBuilder.Save não foi originalmente portada para .NET (Core) porque a implementação dependia muito do código nativo específico do Windows que também não foi portado. Nova no .NET 9, a classe PersistedAssemblyBuilder adiciona uma implementação de Reflection.Emit
totalmente gerenciada que dá suporte ao salvamento. Essa implementação não tem dependência da implementação de Reflection.Emit
pré-existente e específica do runtime. Ou seja, agora há duas implementações diferentes no .NET, executáveis e persistentes. Para executar o assembly persistente, primeiro salve-o em um fluxo de memória ou em um arquivo e carregue-o novamente.
Antes de PersistedAssemblyBuilder
, você só podia executar um assembly gerado e não podia salvá-lo. Como o assembly estava apenas na memória, foi difícil de depurar. As vantagens de salvar um conjunto dinâmico em um arquivo são:
- Você pode verificar o assembly gerado com ferramentas como ILVerify ou descompilá-lo e examiná-lo manualmente com ferramentas como ILSpy.
- O assembly salvo pode ser carregado diretamente, não é necessário compilar novamente, o que pode diminuir o tempo de inicialização do aplicativo.
Para criar uma instância de PersistedAssemblyBuilder
, use o construtor PersistedAssemblyBuilder(AssemblyName, Assembly, IEnumerable<CustomAttributeBuilder>). O parâmetro coreAssembly
é usado para resolver tipos básicos de tempo de execução e pode ser usado para resolver o versionamento de assemblies de referência.
Se
Reflection.Emit
for usado para gerar um assembly que será executado somente na mesma versão de runtime na qual o compilador está sendo executado (normalmente in-proc), o assembly principal pode ser simplesmentetypeof(object).Assembly
. O exemplo a seguir demonstra como criar e salvar um assembly em um fluxo e executá-lo com o assembly de runtime atual: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
for usado para gerar um assembly direcionado a um TFM específico, abra os assemblies de referência para o TFM específico usandoMetadataLoadContext
e use o valor da propriedade MetadataLoadContext.CoreAssembly paracoreAssembly
. Esse valor possibilita que o gerador rode em uma versão de runtime do .NET e tenha como alvo uma versão de runtime do .NET diferente. Você deve usar tipos retornados pela instância deMetadataLoadContext
ao referenciar tipos de núcleo. Por exemplo, em vez detypeof(int)
, localize oSystem.Int32
tipo emMetadataLoadContext.CoreAssembly
por 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 }
Definir ponto de entrada para um executável
Para definir o ponto de entrada de um executável ou definir outras opções para o arquivo de assembly, você pode chamar o método public MetadataBuilder GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder mappedFieldData)
e usar os metadados preenchidos para gerar o assembly com as opções desejadas, por exemplo:
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);
}
Emitir símbolos e gerar PDB
Os metadados de símbolos são preenchidos no pdbBuilder
parâmetro out quando você chama o GenerateMetadata(BlobBuilder, BlobBuilder) método em uma instância de PersistedAssemblyBuilder
. Para criar um assembly com um PDB portátil:
- Crie ISymbolDocumentWriter instâncias com o método ModuleBuilder.DefineDocument(String, Guid, Guid, Guid). Ao emitir o IL do método, emita também as informações de símbolos correspondentes.
- Crie uma instância de PortablePdbBuilder usando a instância de
pdbBuilder
produzida pelo método GenerateMetadata(BlobBuilder, BlobBuilder). - Serialize o
PortablePdbBuilder
em um Blobe escreva oBlob
em um fluxo de arquivos PDB (somente se você estiver gerando um PDB autônomo). - Crie uma instância DebugDirectoryBuilder e adicione um DebugDirectoryBuilder.AddCodeViewEntry (PDB autônomo) ou DebugDirectoryBuilder.AddEmbeddedPortablePdbEntry.
- Defina o argumento
debugDirectoryBuilder
opcional ao criar a instância de PEBuilder.
O exemplo a seguir mostra como emitir informações de símbolo e gerar um arquivo 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;
}
Além disso, você pode adicionar CustomDebugInformation chamando o MetadataBuilder.AddCustomDebugInformation(EntityHandle, GuidHandle, BlobHandle) método da instância pdbBuilder
para adicionar informações avançadas de PDB de incorporação e indexação de origem.
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));
}
Adicionar recursos com PersistedAssemblyBuilder
Você pode chamar MetadataBuilder.AddManifestResource(ManifestResourceAttributes, StringHandle, EntityHandle, UInt32) para adicionar quantos recursos forem necessários. Os fluxos devem ser concatenados em uma BlobBuilder que você passa para o argumento ManagedPEBuilder. O exemplo a seguir mostra como criar recursos e anexá-los ao assembly criado.
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);
}
O exemplo a seguir mostra como ler recursos do assembly que foi criado.
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}");
}
}
Nota
Os tokens de metadados para todos os membros são preenchidos na operação Save. Não use os tokens de um tipo gerado e seus membros antes de salvar, pois eles terão valores padrão ou poderão lançar exceções. É seguro usar tokens para tipos referenciados, não gerados.
Algumas APIs que não são importantes para emitir um assembly não são implementadas; por exemplo, GetCustomAttributes()
não é implementado. Com a implementação do runtime, você conseguiu usar essas APIs depois de criar o tipo. Para os AssemblyBuilder
persistentes, eles lançam NotSupportedException
ou NotImplementedException
. Se você tiver um cenário que exija essas APIs, registre um problema no repositório dotnet/runtime.
Para obter uma maneira alternativa de gerar arquivos de assembly, consulte MetadataBuilder.