Condividi tramite


Assembly dinamici persistenti in .NET

Questo articolo fornisce osservazioni supplementari alla documentazione di riferimento per questa API.

L'API AssemblyBuilder.Save non è stata originariamente convertito in .NET (Core) perché l'implementazione dipendeva in larga misura dal codice nativo specifico di Windows che non è stato convertito. Novità di .NET 9, la PersistedAssemblyBuilder classe aggiunge un'implementazione completamente gestita Reflection.Emit che supporta il salvataggio. Questa implementazione non ha alcuna dipendenza dall'implementazione preesistente specifica del Reflection.Emit runtime. Ciò significa che in .NET sono disponibili due implementazioni diverse, eseguibili e persistenti. Per eseguire l'assembly persistente, salvarlo prima in un flusso di memoria o in un file, quindi caricarlo di nuovo.

Prima PersistedAssemblyBuilderdi , è possibile eseguire solo un assembly generato e non salvarlo. Poiché l'assembly era solo in memoria, era difficile eseguire il debug. I vantaggi del salvataggio di un assembly dinamico in un file sono:

  • È possibile verificare l'assembly generato con strumenti come ILVerify o decompilarlo e esaminarlo manualmente con strumenti come ILSpy.
  • L'assembly salvato può essere caricato direttamente, non è necessario compilarlo di nuovo, riducendo così il tempo di avvio dell'applicazione.

Per creare un'istanza PersistedAssemblyBuilder di , usare il PersistedAssemblyBuilder(AssemblyName, Assembly, IEnumerable<CustomAttributeBuilder>) costruttore . Il coreAssembly parametro viene usato per risolvere i tipi di runtime di base e può essere usato per risolvere il controllo delle versioni degli assembly di riferimento:

  • Se Reflection.Emit viene usato per generare un assembly che verrà eseguito solo nella stessa versione di runtime della versione di runtime in cui è in esecuzione il compilatore (in genere in-proc), l'assembly principale può essere semplicemente typeof(object).Assembly. L'esempio seguente illustra come creare e salvare un assembly in un flusso ed eseguirlo con l'assembly di runtime corrente:

    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 viene usato per generare un assembly destinato a un TFM specifico, aprire gli assembly di riferimento per il TFM specificato usando MetadataLoadContext e usare il valore della proprietà MetadataLoadContext.CoreAssembly per coreAssembly. Questo valore consente al generatore di essere eseguito in una versione di runtime .NET e di specificare come destinazione una versione di runtime .NET diversa. È consigliabile usare i tipi restituiti dall'istanza MetadataLoadContext quando si fa riferimento ai tipi di core. Ad esempio, invece di typeof(int), trovare il System.Int32 tipo in per 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
    }
    

Impostare il punto di ingresso per un eseguibile

Per impostare il punto di ingresso per un eseguibile o per impostare altre opzioni per il file di assembly, è possibile chiamare il public MetadataBuilder GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder mappedFieldData) metodo e usare i metadati popolati per generare l'assembly con le opzioni desiderate, ad esempio:

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

Generare simboli e generare PDB

I metadati dei simboli vengono popolati nel pdbBuilder parametro out quando si chiama il metodo in un'istanza GenerateMetadata(BlobBuilder, BlobBuilder)PersistedAssemblyBuilder di . Per creare un assembly con un PDB portatile:

  1. Creare ISymbolDocumentWriter istanze con il ModuleBuilder.DefineDocument(String, Guid, Guid, Guid) metodo . Durante l'emissione del codice IL del metodo, genera anche le informazioni sul simbolo corrispondenti.
  2. Creare un'istanza PortablePdbBuilder usando l'istanza pdbBuilder prodotta dal GenerateMetadata(BlobBuilder, BlobBuilder) metodo .
  3. Serializzare in PortablePdbBuilder un Blobe scrivere in Blob un flusso di file PDB (solo se si genera un PDB autonomo).
  4. Creare un'istanza DebugDirectoryBuilder di e aggiungere un DebugDirectoryBuilder.AddCodeViewEntry PDB autonomo o DebugDirectoryBuilder.AddEmbeddedPortablePdbEntry.
  5. Impostare l'argomento facoltativo debugDirectoryBuilder durante la creazione dell'istanza PEBuilder .

Nell'esempio seguente viene illustrato come generare informazioni sui simboli e generare un file 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;
}

È inoltre possibile aggiungere CustomDebugInformation chiamando il MetadataBuilder.AddCustomDebugInformation(EntityHandle, GuidHandle, BlobHandle) metodo dall'istanza pdbBuilder per aggiungere informazioni avanzate PDB di incorporamento e indicizzazione dell'origine.

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

Aggiungere risorse con PersistedAssemblyBuilder

È possibile chiamare MetadataBuilder.AddManifestResource(ManifestResourceAttributes, StringHandle, EntityHandle, UInt32) per aggiungere tutte le risorse necessarie. Flussi deve essere concatenato in un BlobBuilder oggetto passato all'argomento ManagedPEBuilder . Nell'esempio seguente viene illustrato come creare risorse e collegarlo all'assembly creato.

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

I token di metadati per tutti i membri vengono popolati nell'operazione Save . Non usare i token di un tipo generato e i relativi membri prima del salvataggio, perché avranno valori predefiniti o generano eccezioni. È sicuro usare i token per i tipi a cui viene fatto riferimento, non generato.

Alcune API che non sono importanti per l'emissione di un assembly non vengono implementate; ad esempio, GetCustomAttributes() non viene implementato. Con l'implementazione del runtime, è stato possibile usare queste API dopo aver creato il tipo. Per l'oggetto persistente AssemblyBuilder, generano NotSupportedException o NotImplementedException. Se si ha uno scenario che richiede tali API, inviare un problema nel repository dotnet/runtime.

Per un modo alternativo per generare file di assembly, vedere MetadataBuilder.