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 PersistedAssemblyBuilder
de , 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 simplementetypeof(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 medianteMetadataLoadContext
y use el valor de la propiedad MetadataLoadContext.CoreAssembly paracoreAssembly
. 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 laMetadataLoadContext
instancia al hacer referencia a los tipos principales. Por ejemplo, en lugar detypeof(int)
, busque elSystem.Int32
tipo enMetadataLoadContext.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:
- 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.
- Cree una PortablePdbBuilder instancia mediante la
pdbBuilder
instancia generada por el GenerateMetadata(BlobBuilder, BlobBuilder) método . - Serialice en
PortablePdbBuilder
y Blobescriba enBlob
una secuencia de archivos PDB (solo si está generando una PDB independiente). - Cree una DebugDirectoryBuilder instancia y agregue un DebugDirectoryBuilder.AddCodeViewEntry (PDB independiente) o DebugDirectoryBuilder.AddEmbeddedPortablePdbEntry.
- 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.