Sdílet prostřednictvím


Trvalá dynamická sestavení v .NET

Tento článek obsahuje doplňující poznámky k referenční dokumentaci pro toto rozhraní API.

Rozhraní API AssemblyBuilder.Save se původně nepře portovalo do .NET (Core), protože implementace výrazně závisela na nativním kódu specifickém pro Windows, který nebyl také portován. V rozhraní .NET 9 přidává třída PersistedAssemblyBuilder plně spravovanou implementaci Reflection.Emit, která podporuje ukládání. Tato implementace nemá žádnou závislost na existující implementaci Reflection.Emit specifické pro modul runtime. To znamená, že v .NET existují dvě různé implementace, spustitelné a trvalé. Pokud chcete spustit trvalé sestavení, nejprve ho uložte do datového proudu paměti nebo souboru a pak ho načtěte zpět.

Před PersistedAssemblyBuilderbyste mohli spustit pouze vygenerované sestavení a neukládat ho. Vzhledem k tomu, že sestavení bylo pouze uloženo v paměti, bylo obtížné ho odladit. Výhody ukládání dynamického sestavení do souboru jsou:

  • Vygenerované sestavení můžete ověřit pomocí nástrojů, jako je ILVerify, nebo dekompilovat, a ručně ho prozkoumat pomocí nástrojů, jako je ILSpy.
  • Uložené sestavení lze načíst přímo, není nutné znovu kompilovat, což může zkrátit dobu spuštění aplikace.

K vytvoření PersistedAssemblyBuilder instance použijte konstruktor PersistedAssemblyBuilder(AssemblyName, Assembly, IEnumerable<CustomAttributeBuilder>). Parametr coreAssembly se používá k řešení základních typů za běhu a lze ho použít k řešení verzování referenčních sestavení.

  • Pokud Reflection.Emit slouží ke generování sestavení, které se spustí pouze ve stejné verzi modulu runtime jako verze modulu runtime, na které kompilátor běží (obvykle v proc), základní sestavení může být jednoduše typeof(object).Assembly. Následující příklad ukazuje, jak vytvořit a uložit sestavení do datového proudu a spustit ho s aktuálním sestavením runtime:

    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 }));
    }
    
  • Pokud se Reflection.Emit používá k vygenerování sestavení, které cílí na konkrétní TFM, otevřete referenční sestavení pro danou sadu TFM pomocí MetadataLoadContext a použijte hodnotu MetadataLoadContext.CoreAssembly vlastnost pro coreAssembly. Tato hodnota umožňuje generátoru spustit na jedné verzi modulu runtime .NET a cílit na jinou verzi modulu runtime .NET. Při odkazování na základní typy byste měli použít typy vrácené instancí MetadataLoadContext. Například místo typeof(int)vyhledejte typ System.Int32 v MetadataLoadContext.CoreAssembly podle názvu.

    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
    }
    

Nastavení vstupního bodu pro spustitelný soubor

Chcete-li nastavit vstupní bod spustitelného souboru nebo nastavit další možnosti pro soubor sestavení, můžete volat metodu public MetadataBuilder GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder mappedFieldData) a pomocí vyplněných metadat vygenerovat sestavení s požadovanými možnostmi, například:

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

Generování symbolů a generování PDB

Metadata symbolů se vyplní do výstupního parametru pdbBuilder, když zavoláte metodu GenerateMetadata(BlobBuilder, BlobBuilder) u instance PersistedAssemblyBuilder. Vytvoření sestavení s přenosným souborem PDB:

  1. Pomocí metody ModuleBuilder.DefineDocument(String, Guid, Guid, Guid) vytvořte instance ISymbolDocumentWriter. Při generování IL metody rovněž generujte příslušné symbolické informace.
  2. Vytvořte instanci PortablePdbBuilder pomocí instance pdbBuilder vytvořené metodou GenerateMetadata(BlobBuilder, BlobBuilder).
  3. Serializujte PortablePdbBuilder do Bloba zapište Blob do datového proudu souboru PDB (pouze pokud generujete samostatný PDB).
  4. Vytvořte instanci DebugDirectoryBuilder a přidejte DebugDirectoryBuilder.AddCodeViewEntry (samostatný soubor PDB) nebo DebugDirectoryBuilder.AddEmbeddedPortablePdbEntry.
  5. Při vytváření instance PEBuilder nastavte volitelný argument debugDirectoryBuilder.

Následující příklad ukazuje, jak vygenerovat informace o symbolu a vygenerovat soubor 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;
}

Dále můžete přidat CustomDebugInformation voláním metody MetadataBuilder.AddCustomDebugInformation(EntityHandle, GuidHandle, BlobHandle) z instance pdbBuilder pro přidání zdrojového vkládání a pokročilé PDB informace o indexování zdroje.

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

Přidání prostředků pomocí PersistedAssemblyBuilder

Můžete volat MetadataBuilder.AddManifestResource(ManifestResourceAttributes, StringHandle, EntityHandle, UInt32) a přidat tolik prostředků, kolik potřebujete. Datové proudy musí být zřetězeny do jednoho BlobBuilder, který pak předáte jako argument ManagedPEBuilder. Následující příklad ukazuje, jak vytvořit prostředky a připojit ho k vytvořenému sestavení.

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

Následující příklad ukazuje, jak načíst zdroje z vytvořené sestavy.

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

Poznámka

Tokeny metadat pro všechny členy jsou naplněny při operaci Save. Nepoužívejte tokeny vygenerovaného typu a jeho členů před uložením, protože budou mít výchozí hodnoty nebo vyvolají výjimky. Je bezpečné používat tokeny pro typy, na které se odkazuje, které nejsou generovány.

Některá rozhraní API, která nejsou důležitá pro generování sestavení, nejsou implementována. například GetCustomAttributes() není implementováno. Při implementaci modulu runtime jste po vytvoření typu mohli tato rozhraní API použít. U trvalých AssemblyBuildervyhodí NotSupportedException nebo NotImplementedException. Pokud máte scénář, který vyžaduje tato rozhraní API, vytvořte problém v úložišti dotnet/runtime.

Pro alternativní způsob, jak generovat soubory sestavení, se podívejte na MetadataBuilder.