Compartir a través de


Ensamblados dinámicos persistentes en .NET

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

La API de AssemblyBuilder.Save 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 clase PersistedAssemblyBuilder agrega una implementación de Reflection.Emit totalmente administrada que admite el ahorro. Esta implementación no tiene ninguna dependencia de la implementación de Reflection.Emit específica del entorno de ejecución existente. 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 de PersistedAssemblyBuilder, 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 instancia de PersistedAssemblyBuilder, use el constructor PersistedAssemblyBuilder(AssemblyName, Assembly, IEnumerable<CustomAttributeBuilder>). El parámetro coreAssembly 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 un flujo y ejecutarlo con el ensamblado de 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 se usa Reflection.Emit para generar un ensamblado orientado 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 instancia de MetadataLoadContext al hacer referencia a los tipos principales. Por ejemplo, en lugar de typeof(int), busque el tipo System.Int32 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
    }
    

Configura el punto de entrada para un ejecutable

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

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

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

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

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

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 método MetadataBuilder.AddCustomDebugInformation(EntityHandle, GuidHandle, BlobHandle) desde la instancia de pdbBuilder para agregar información avanzada de PDB de inserción de origen e 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 a MetadataBuilder.AddManifestResource(ManifestResourceAttributes, StringHandle, EntityHandle, UInt32) para agregar tantos recursos como sea necesario. Las secuencias deben concatenarse en un BlobBuilder que se pasa al argumento ManagedPEBuilder. En el ejemplo siguiente se muestra cómo crear recursos y adjuntarlos al ensamblado que se crea.

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

En el ejemplo siguiente se muestra cómo leer recursos del ensamblado creado.

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

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

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

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