Delen via


Persistente dynamische assembly's in .NET

In dit artikel vindt u aanvullende opmerkingen in de referentiedocumentatie voor deze API.

De AssemblyBuilder.Save-API is oorspronkelijk niet overgezet naar .NET (Core), omdat de implementatie sterk afhankelijk is van systeemeigen Windows-code die ook niet is overgezet. Nieuw in .NET 9 voegt de PersistedAssemblyBuilder-klasse een volledig beheerde Reflection.Emit-implementatie toe die ondersteuning biedt voor opslaan. Deze implementatie is niet afhankelijk van de bestaande runtimespecifieke Reflection.Emit-implementatie. Dat wil gezegd, er zijn nu twee verschillende implementaties in .NET, die kunnen worden uitgevoerd en persistent zijn. Als u de persistente assembly wilt uitvoeren, moet u deze eerst opslaan in een geheugenstroom of een bestand en vervolgens weer laden.

Voordat PersistedAssemblyBuilder, kunt u alleen een gegenereerde assembly uitvoeren en deze niet opslaan. Omdat de assembly alleen in het geheugen was, was het moeilijk om fouten op te sporen. Voordelen van het opslaan van een dynamische assembly in een bestand zijn:

  • U kunt de gegenereerde assembly verifiëren met hulpprogramma's zoals ILVerify of decompileren en deze handmatig onderzoeken met hulpprogramma's zoals ILSpy.
  • De opgeslagen assembly kan rechtstreeks worden geladen, hoeft niet opnieuw te worden gecompileerd, waardoor de opstarttijd van de toepassing kan worden verlaagd.

Als u een PersistedAssemblyBuilder-exemplaar wilt maken, gebruikt u de PersistedAssemblyBuilder(AssemblyName, Assembly, IEnumerable<CustomAttributeBuilder>) constructor. De parameter coreAssembly wordt gebruikt om basistypen op runtime-niveau vast te stellen en kan worden gebruikt voor het oplossen van referentie-assemblyversies.

  • Als Reflection.Emit wordt gebruikt om een assembly te genereren die alleen wordt uitgevoerd op dezelfde runtimeversie als de runtimeversie waarop de compiler wordt uitgevoerd (meestal in-proc), kan de kernassembly gewoon worden typeof(object).Assembly. In het volgende voorbeeld ziet u hoe u een assembly maakt en opslaat in een stream en deze uitvoert met de huidige runtime-assembly:

    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 }));
    }
    
  • Als Reflection.Emit wordt gebruikt om een assembly te genereren die gericht is op een specifieke TFM, opent u de referentieassembly's voor de opgegeven TFM met behulp van MetadataLoadContext en gebruikt u de waarde van de eigenschap MetadataLoadContext.CoreAssembly voor coreAssembly. Met deze waarde kan de generator worden uitgevoerd op één .NET Runtime-versie en gericht zijn op een andere .NET Runtime-versie. U moet typen gebruiken die door het MetadataLoadContext exemplaar worden geretourneerd bij het verwijzen naar kerntypen. Zoek bijvoorbeeld in plaats van typeof(int)het System.Int32 type in MetadataLoadContext.CoreAssembly op naam:

    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
    }
    

Invoerpunt instellen voor een uitvoerbaar bestand

Als u het invoerpunt voor een uitvoerbaar bestand wilt instellen of andere opties voor het assemblybestand wilt instellen, kunt u de methode public MetadataBuilder GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder mappedFieldData) aanroepen en de ingevulde metagegevens gebruiken om de assembly te genereren met de gewenste opties, bijvoorbeeld:

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

Symbolen verzenden en PDB genereren

De metagegevens van symbolen worden ingevuld in de parameter pdbBuilder out wanneer u de GenerateMetadata(BlobBuilder, BlobBuilder) methode aanroept op een PersistedAssemblyBuilder exemplaar. Een assembly maken met een draagbare PDB:

  1. Maak ISymbolDocumentWriter exemplaren door middel van de ModuleBuilder.DefineDocument(String, Guid, Guid, Guid)-methode. Wanneer u de IL van de methode verzendt, verzendt u ook de bijbehorende symboolgegevens.
  2. Maak een PortablePdbBuilder exemplaar met behulp van het pdbBuilder exemplaar dat is geproduceerd door de GenerateMetadata(BlobBuilder, BlobBuilder) methode.
  3. Serialiseer de PortablePdbBuilder in een Bloben schrijf de Blob naar een PDB-bestandsstroom (alleen als u een zelfstandige PDB genereert).
  4. Maak een DebugDirectoryBuilder-exemplaar en voeg een DebugDirectoryBuilder.AddCodeViewEntry (zelfstandige PDB) of DebugDirectoryBuilder.AddEmbeddedPortablePdbEntrytoe.
  5. Stel het optionele debugDirectoryBuilder-argument in bij het maken van het PEBuilder exemplaar.

In het volgende voorbeeld ziet u hoe u symboolgegevens verzendt en een PDB-bestand genereert.

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

Verder kunt u CustomDebugInformation toevoegen door de MetadataBuilder.AddCustomDebugInformation(EntityHandle, GuidHandle, BlobHandle) methode aan te roepen vanuit het pdbBuilder exemplaar om geavanceerde PDB-gegevens voor het insluiten van bronnen en bronindexering toe te voegen.

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

Resources toevoegen met PersistentedAssemblyBuilder

U kunt MetadataBuilder.AddManifestResource(ManifestResourceAttributes, StringHandle, EntityHandle, UInt32) aanroepen om zoveel resources toe te voegen als nodig is. Streams moeten worden samengevoegd tot één BlobBuilder dat u aan het argument ManagedPEBuilder doorgeeft. In het volgende voorbeeld ziet u hoe u resources maakt en deze koppelt aan de assembly die is gemaakt.

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

In het volgende voorbeeld ziet u hoe u resources leest van de gemaakte assembly.

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

Notitie

De metadatatokens voor alle leden worden ingevuld tijdens de Save-bewerking. Gebruik de tokens van een gegenereerd type en de bijbehorende leden niet voordat u opslaat, omdat ze standaardwaarden hebben of uitzonderingen genereren. Het is veilig om tokens te gebruiken voor typen waarnaar wordt verwezen, niet gegenereerd.

Sommige API's die niet belangrijk zijn voor het verzenden van een assembly, worden niet geïmplementeerd; GetCustomAttributes() is bijvoorbeeld niet geïmplementeerd. Met de runtime-implementatie kunt u deze API's gebruiken nadat u het type hebt gemaakt. Voor de persistente AssemblyBuildergooien ze NotSupportedException of NotImplementedException. Als u een scenario hebt waarvoor deze API's zijn vereist, moet u een probleem indienen in de dotnet/runtime-opslagplaats.

Zie MetadataBuildervoor een alternatieve manier om assemblybestanden te genereren.