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 portata su .NET (Core) perché l'implementazione dipendeva in larga misura dal codice nativo specifico per Windows che non è stato portato. Novità di .NET 9, la classe PersistedAssemblyBuilder aggiunge un'implementazione di Reflection.Emit completamente gestita che supporta il salvataggio. Questa implementazione non ha alcuna dipendenza dall'implementazione di Reflection.Emit preesistente specifica del 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 nuovamente.

Prima di PersistedAssemblyBuilder, è possibile eseguire solo un assembly generato e non salvarlo. Poiché l'assembly era presente unicamente in memoria, era difficile da 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 di PersistedAssemblyBuilder, usare il costruttore PersistedAssemblyBuilder(AssemblyName, Assembly, IEnumerable<CustomAttributeBuilder>). Il parametro coreAssembly viene usato per risolvere i tipi di esecuzione di base e può essere usato per risolvere il versionamento 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 viene usato per generare un assembly destinato a un TFM specifico, aprire gli assembly di riferimento per il TFM specificato usando e usare il valore della proprietà MetadataLoadContext.CoreAssembly per . 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 di MetadataLoadContext quando si fa riferimento ai tipi di core. Ad esempio, invece di typeof(int), trova il tipo System.Int32 in MetadataLoadContext.CoreAssembly per 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 metodo public MetadataBuilder GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder mappedFieldData) e usare i metadati popolati per generare l'assembly con le opzioni desiderate, ad esempio:

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

Emettere simboli e generare PDB

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

  1. Creare istanze di ISymbolDocumentWriter con il metodo ModuleBuilder.DefineDocument(String, Guid, Guid, Guid). Durante l'emissione del codice IL del metodo, emetti anche le informazioni sui simboli corrispondenti.
  2. Creare un'istanza di PortablePdbBuilder usando l'istanza di pdbBuilder prodotta dal metodo GenerateMetadata(BlobBuilder, BlobBuilder).
  3. Serializzare il PortablePdbBuilder in un Blobe scrivere il Blob in un flusso di file PDB (solo se si genera un PDB autonomo).
  4. Crea un'istanza di DebugDirectoryBuilder e aggiungi un DebugDirectoryBuilder.AddCodeViewEntry (PDB indipendente) o DebugDirectoryBuilder.AddEmbeddedPortablePdbEntry.
  5. Impostare l'argomento facoltativo debugDirectoryBuilder durante la creazione dell'istanza di 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 metodo MetadataBuilder.AddCustomDebugInformation(EntityHandle, GuidHandle, BlobHandle) dall'istanza di pdbBuilder per aggiungere informazioni PDB avanzate sull'incorporamento e sull'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. I flussi devono essere concatenati in un unico BlobBuilder che si passa all'argomento ManagedPEBuilder. Nell'esempio seguente viene illustrato come creare risorse e collegarlo all'assembly creato.

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

Nell'esempio seguente viene illustrato come leggere le risorse dall'assembly creato.

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

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

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'AssemblyBuilderpersistente, generano NotSupportedException o NotImplementedException. Se hai uno scenario che richiede tali API, segnala un problema nel repository dotnet/runtime .

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