Assemblies dinâmicos persistentes no .NET

Este artigo fornece observações complementares à documentação de referência para essa API.

A AssemblyBuilder.Save API não foi originalmente portada para o .NET (Core) porque a implementação dependia muito do código nativo específico do Windows que também não foi portado. Novo no .NET 9, a classe adiciona uma implementação totalmente gerenciada PersistedAssemblyBuilderReflection.Emit que oferece suporte à salvamento. Essa implementação não tem dependência da implementação pré-existente, específica Reflection.Emit do tempo de execução. Ou seja, agora existem 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, em seguida, carregue-o de volta.

Antes PersistedAssemblyBuilder, você só podia executar um assembly gerado e não salvá-lo. Como o assembly era apenas na memória, era difícil depurar. As vantagens de salvar um assembly 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, sem necessidade de compilar novamente, o que pode diminuir o tempo de inicialização do aplicativo.

Para criar uma PersistedAssemblyBuilder instância, use o PersistedAssemblyBuilder(AssemblyName, Assembly, IEnumerable<CustomAttributeBuilder>) construtor. O coreAssembly parâmetro é usado para resolver tipos de tempo de execução base e pode ser usado para resolver o controle de versão do assembly de referência:

  • Se Reflection.Emit for usado para gerar um assembly que só será executado na mesma versão de tempo de execução que a versão de tempo de execução em que 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 tempo de execução 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 destinado a um TFM específico, abra os assemblies de referência para o TFM fornecido usando MetadataLoadContext e use o valor da propriedade MetadataLoadContext.CoreAssembly para coreAssembly. Esse valor permite que o gerador seja executado em uma versão de tempo de execução do .NET e direcione uma versão de tempo de execução diferente do .NET. Você deve usar tipos retornados MetadataLoadContext pela instância ao fazer referência a tipos principais. Por exemplo, em vez de typeof(int), localize o System.Int32 tipo por MetadataLoadContext.CoreAssembly 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 para um executável ou para definir outras opções para o arquivo de assembly, você pode chamar o public MetadataBuilder GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder mappedFieldData) método e usar os metadados preenchidos para gerar o assembly com as opções desejadas, por exemplo:

public static void SetEntryPoint()
{
    PersistedAssemblyBuilder ab = new PersistedAssemblyBuilder(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);
    PEHeaderBuilder peHeaderBuilder = new PEHeaderBuilder(imageCharacteristics: Characteristics.ExecutableImage);

    ManagedPEBuilder peBuilder = new ManagedPEBuilder(
                    header: peHeaderBuilder,
                    metadataRootBuilder: new MetadataRootBuilder(metadataBuilder),
                    ilStream: ilStream,
                    mappedFieldData: fieldData,
                    entryPoint: MetadataTokens.MethodDefinitionHandle(entryPoint.MetadataToken));

    BlobBuilder peBlob = new BlobBuilder();
    peBuilder.Serialize(peBlob);

    // in case saving to a file:
    using var fileStream = new FileStream("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 PersistedAssemblyBuilder instância. Para criar um assembly com um PDB portátil:

  1. Crie ISymbolDocumentWriter instâncias com o ModuleBuilder.DefineDocument(String, Guid, Guid, Guid) método. Ao emitir a IL do método, também emita a informação do símbolo correspondente.
  2. Crie uma PortablePdbBuilder instância usando a pdbBuilder instância produzida pelo GenerateMetadata(BlobBuilder, BlobBuilder) método.
  3. Serialize o PortablePdbBuilder em um Blob, e grave o Blob em um fluxo de arquivos PDB (somente se você estiver gerando um PDB autônomo).
  4. Crie uma DebugDirectoryBuilder instância e adicione um DebugDirectoryBuilder.AddCodeViewEntry (PDB autônomo) ou DebugDirectoryBuilder.AddEmbeddedPortablePdbEntry.
  5. Defina o argumento opcional debugDirectoryBuilder ao criar a PEBuilder instância.

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)pdbBuilder método da instância 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 ligar MetadataBuilder.AddManifestResource(ManifestResourceAttributes, StringHandle, EntityHandle, UInt32) para adicionar quantos recursos forem necessários. Os fluxos devem ser concatenados em um BlobBuilder que você passe para o ManagedPEBuilder argumento. O exemplo a seguir mostra como criar recursos e anexá-los ao assembly criado.

public static void SetResource()
{
    PersistedAssemblyBuilder ab = new PersistedAssemblyBuilder(new AssemblyName("MyAssembly"), typeof(object).Assembly);
    ab.DefineDynamicModule("MyModule");
    MetadataBuilder metadata = ab.GenerateMetadata(out BlobBuilder ilStream, out _);

    using MemoryStream stream = new MemoryStream();
    ResourceWriter myResourceWriter = new ResourceWriter(stream);
    myResourceWriter.AddResource("AddResource 1", "First added resource");
    myResourceWriter.AddResource("AddResource 2", "Second added resource");
    myResourceWriter.AddResource("AddResource 3", "Third added resource");
    myResourceWriter.Close();
    BlobBuilder resourceBlob = new BlobBuilder();
    resourceBlob.WriteBytes(stream.ToArray());
    metadata.AddManifestResource(ManifestResourceAttributes.Public, metadata.GetOrAddString("MyResource"), default, (uint)resourceBlob.Count);

    ManagedPEBuilder peBuilder = new ManagedPEBuilder(
                    header: new PEHeaderBuilder(imageCharacteristics: Characteristics.ExecutableImage | Characteristics.Dll),
                    metadataRootBuilder: new MetadataRootBuilder(metadata),
                    ilStream: ilStream,
                    managedResources: resourceBlob);

    BlobBuilder blob = new BlobBuilder();
    peBuilder.Serialize(blob);
    using var fileStream = new FileStream("MyAssemblyWithResource.dll", FileMode.Create, FileAccess.Write);
    blob.WriteContentTo(fileStream);
}

Observação

Os tokens de metadados para todos os membros são preenchidos Save na operação. Não use os tokens de um tipo gerado e seus membros antes de salvar, pois eles terão valores padrão ou lançarão exceções. É seguro usar tokens para tipos que são 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 de tempo de execução, você pôde usar essas APIs depois de criar o tipo. Para os persistentes AssemblyBuilder, eles jogam 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.