Ensamblados dinámicos persistentes en .NET

En este artículo se proporcionan comentarios adicionales a la documentación de referencia de esta API.

La AssemblyBuilder.Save API no se ha migrado originalmente a .NET (Core) porque la implementación depende en gran medida del código nativo específico de Windows que tampoco se ha migrado. Novedad de .NET 9, la PersistedAssemblyBuilder clase agrega una implementación totalmente administrada Reflection.Emit que admite el ahorro. Esta implementación no depende de la implementación existente específica Reflection.Emit del entorno de ejecución. Es decir, ahora hay dos implementaciones diferentes en .NET, ejecutables y persistentes. Para ejecutar el ensamblado persistente, guárdelo primero en un flujo de memoria o en un archivo y vuelva a cargarlo.

Antes PersistedAssemblyBuilderde , solo podía ejecutar un ensamblado generado y no guardarlo. Dado que el ensamblado solo estaba en memoria, era difícil depurar. Las ventajas de guardar un ensamblado dinámico en un archivo son:

  • Puede comprobar el ensamblado generado con herramientas como ILVerify o descompilar y examinarlo manualmente con herramientas como ILSpy.
  • El ensamblado guardado se puede cargar directamente, no es necesario volver a compilar, lo que puede reducir el tiempo de inicio de la aplicación.

Para crear una PersistedAssemblyBuilder instancia, use el PersistedAssemblyBuilder(AssemblyName, Assembly, IEnumerable<CustomAttributeBuilder>) constructor . El coreAssembly parámetro se usa para resolver los tipos de tiempo de ejecución base y se puede usar para resolver el control de versiones de ensamblado de referencia:

  • Si Reflection.Emit se usa para generar un ensamblado que solo se ejecutará en la misma versión en tiempo de ejecución que la versión en tiempo de ejecución en la que se ejecuta el compilador (normalmente en proceso), el ensamblado principal puede ser simplemente typeof(object).Assembly. En el ejemplo siguiente se muestra cómo crear y guardar un ensamblado en una secuencia y ejecutarlo con el ensamblado en tiempo de ejecución actual:

    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 }));
    }
    
  • Si Reflection.Emit se usa para generar un ensamblado destinado a un TFM específico, abra los ensamblados de referencia para el TFM especificado mediante MetadataLoadContext y use el valor de la propiedad MetadataLoadContext.CoreAssembly para coreAssembly. Este valor permite que el generador se ejecute en una versión en tiempo de ejecución de .NET y tener como destino una versión en tiempo de ejecución de .NET diferente. Debe usar los tipos devueltos por la MetadataLoadContext instancia al hacer referencia a los tipos principales. Por ejemplo, en lugar de typeof(int), busque el System.Int32 tipo en MetadataLoadContext.CoreAssembly por nombre:

    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
    }
    

Establecimiento del punto de entrada para un archivo ejecutable

Para establecer el punto de entrada de un archivo ejecutable o para establecer otras opciones para el archivo de ensamblado, puede llamar al public MetadataBuilder GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder mappedFieldData) método y usar los metadatos rellenados para generar el ensamblado con las opciones deseadas, por ejemplo:

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);
}

Emisión de símbolos y generación de PDB

Los metadatos de símbolos se rellenan en el pdbBuilder parámetro out cuando se llama al GenerateMetadata(BlobBuilder, BlobBuilder) método en una PersistedAssemblyBuilder instancia de . Para crear un ensamblado con una PDB portátil:

  1. Cree ISymbolDocumentWriter instancias con el ModuleBuilder.DefineDocument(String, Guid, Guid, Guid) método . Al emitir el IL del método, también emite la información del símbolo correspondiente.
  2. Cree una PortablePdbBuilder instancia mediante la pdbBuilder instancia generada por el GenerateMetadata(BlobBuilder, BlobBuilder) método .
  3. Serialice en PortablePdbBuilder y Blobescriba en Blob una secuencia de archivos PDB (solo si está generando una PDB independiente).
  4. Cree una DebugDirectoryBuilder instancia y agregue un DebugDirectoryBuilder.AddCodeViewEntry (PDB independiente) o DebugDirectoryBuilder.AddEmbeddedPortablePdbEntry.
  5. Establezca el argumento opcional debugDirectoryBuilder al crear la PEBuilder instancia.

En el ejemplo siguiente se muestra cómo emitir información de símbolos y generar un archivo 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;
}

Además, puede agregar CustomDebugInformation llamando al MetadataBuilder.AddCustomDebugInformation(EntityHandle, GuidHandle, BlobHandle) método desde la pdbBuilder instancia para agregar la inserción de origen y la información avanzada de PDB de indexación de origen.

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));
}

Adición de recursos con PersistedAssemblyBuilder

Puede llamar MetadataBuilder.AddManifestResource(ManifestResourceAttributes, StringHandle, EntityHandle, UInt32) a para agregar tantos recursos como sea necesario. Secuencias debe concatenarse en uno BlobBuilder que pase al ManagedPEBuilder argumento . En el ejemplo siguiente se muestra cómo crear recursos y adjuntarlos al ensamblado que se crea.

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);
}

Nota:

Los tokens de metadatos de todos los miembros se rellenan en la Save operación. No use los tokens de un tipo generado y sus miembros antes de guardarlos, ya que tendrán valores predeterminados o producirán excepciones. Es seguro usar tokens para tipos a los que se hace referencia, no se generan.

Algunas API que no son importantes para emitir un ensamblado no se implementan; por ejemplo, GetCustomAttributes() no se implementa. Con la implementación en tiempo de ejecución, pudo usar esas API después de crear el tipo. Para el objeto persistente AssemblyBuilder, inician NotSupportedException o NotImplementedException. Si tiene un escenario que requiere esas API, abra un problema en el repositorio dotnet/runtime.

Para obtener una manera alternativa de generar archivos de ensamblado, consulte MetadataBuilder.