Compartilhar via


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 simplesmente typeof(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 usando MetadataLoadContext e use o valor da propriedade MetadataLoadContext.CoreAssembly para coreAssembly. 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 de MetadataLoadContext ao referenciar tipos de núcleo. Por exemplo, em vez de typeof(int), localize o System.Int32 tipo em MetadataLoadContext.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:

  1. 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.
  2. Crie uma instância de PortablePdbBuilder usando a instância de pdbBuilder produzida pelo método GenerateMetadata(BlobBuilder, BlobBuilder).
  3. Serialize o PortablePdbBuilder em um Blobe escreva o Blob em um fluxo de arquivos PDB (somente se você estiver gerando um PDB autônomo).
  4. Crie uma instância DebugDirectoryBuilder e adicione um DebugDirectoryBuilder.AddCodeViewEntry (PDB autônomo) ou DebugDirectoryBuilder.AddEmbeddedPortablePdbEntry.
  5. 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.