Freigeben über


Persistierte dynamische Baugruppen in .NET

Dieser Artikel enthält ergänzende Hinweise zur Referenzdokumentation für diese API.

Die AssemblyBuilder.Save-API wurde ursprünglich nicht zu .NET (Core) portiert, da die Implementierung stark von windowsspezifischem systemeigenem Code abhängt, der ebenfalls nicht portiert wurde. Neu in .NET 9 fügt die PersistedAssemblyBuilder Klasse eine vollständig verwaltete Reflection.Emit Implementierung hinzu, die das Speichern unterstützt. Diese Implementierung hat keine Abhängigkeit von der bereits vorhandenen, laufzeitspezifischen Reflection.Emit Implementierung. Das heißt, es gibt nun zwei verschiedene Implementierungen in .NET, die ausgeführt und beibehalten werden können. Wenn Sie die persistierte Assembly ausführen möchten, speichern Sie sie zuerst in einem Speicherstrom oder einer Datei, dann laden Sie sie wieder.

Vor PersistedAssemblyBuilderkonnten Sie nur eine generierte Assembly ausführen und nicht speichern. Da die Assembly nur im Arbeitsspeicher war, war es schwierig zu debuggen. Vorteile des Speicherns einer dynamischen Assembly in einer Datei sind:

  • Sie können die generierte Assembly mit Tools wie ILVerify überprüfen oder dekompilieren und manuell mit Tools wie ILSpy untersuchen.
  • Die gespeicherte Assembly kann direkt geladen werden, sie muss nicht erneut kompiliert werden, wodurch die Startzeit der Anwendung verringert werden kann.

Verwenden Sie den PersistedAssemblyBuilder(AssemblyName, Assembly, IEnumerable<CustomAttributeBuilder>)-Konstruktor, um eine PersistedAssemblyBuilder Instanz zu erstellen. DercoreAssembly-Parameter wird zur Auflösung von Basis-Laufzeittypen verwendet und kann zur Auflösung von Referenz-Assembly-Versionen verwendet werden:

  • WennReflection.Emitverwendet wird, um eine Assembly zu erzeugen, die nur auf derselben Laufzeitversion ausgeführt wird wie die Laufzeitversion, auf der der Compiler läuft (typischerweise in-proc), kann die Core-Assembly einfach seintypeof(object).Assembly. Das folgende Beispiel zeigt, wie eine Baugruppe erstellt und in einem Stream gespeichert und mit der aktuellen Laufzeitbaugruppe ausgeführt wird:

    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 }));
    }
    
  • WennReflection.Emitverwendet wird, um eine Assembly zu generieren, die auf ein bestimmtes TFM abzielt, öffnen Sie die Referenz-Assemblies für das angegebene TFM mit MetadataLoadContextund verwenden Sie den Wert der Eigenschaft MetadataLoadContext.CoreAssemblyfürcoreAssembly. Mit diesem Wert kann der Generator auf einer .NET-Laufzeitversion ausgeführt und auf eine andere .NET-Laufzeitversion ausgerichtet werden. Sie sollten Typen verwenden, die von der MetadataLoadContext Instanz beim Verweisen auf Kerntypen zurückgegeben werden. Zum Beispiel, statt typeof(int), finden Sie dieSystem.Int32inMetadataLoadContext.CoreAssemblynach Namen:

    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
    }
    

Einstiegspunkt für eine ausführbare Datei festlegen

Um den Einstiegspunkt für eine ausführbare Datei festzulegen oder andere Optionen für die Assemblydatei festzulegen, können Sie die public MetadataBuilder GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder mappedFieldData)-Methode aufrufen und die aufgefüllten Metadaten verwenden, um die Assembly mit den gewünschten Optionen zu generieren, z. B.:

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

Symbole ausgeben und PDB generieren

Die Metadaten der Symbole werden in den pdbBuilderout-Parameter eingetragen, wenn Sie dieGenerateMetadata(BlobBuilder, BlobBuilder)-Methode für einePersistedAssemblyBuilder-Instanz aufrufen. So erstellen Sie eine Baugruppe mit einer portablen PDB:

  1. Erstellen SieISymbolDocumentWriter Instanzen mit derModuleBuilder.DefineDocument(String, Guid, Guid, Guid) Methode. Bei der Ausgabe der AWL der Methode werden auch die entsprechenden Symbolinformationen ausgegeben.
  2. Erstellen Sie eine PortablePdbBuilder-Instanz unter Verwendung der durch die GenerateMetadata(BlobBuilder, BlobBuilder)-Methode erzeugten pdbBuilder-Instanz.
  3. Serialisieren Sie diePortablePdbBuilderin eineBlob, und schreiben Sie dieBlob in einen PDB-Dateistrom (nur wenn Sie eine eigenständige PDB erzeugen).
  4. Erstellen Sie eineDebugDirectoryBuilder-Instanz und fügen Sie eineDebugDirectoryBuilder.AddCodeViewEntry(eigenständige PDB) oder DebugDirectoryBuilder.AddEmbeddedPortablePdbEntryhinzu.
  5. Legen Sie das optionale debugDirectoryBuilder-Argument fest, wenn Sie die PEBuilder-Instanz erstellen.

Das folgende Beispiel zeigt, wie Symbolinformationen ausgegeben und eine PDB-Datei generiert werden.

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

Außerdem können SieCustomDebugInformationhinzufügen, indem Sie die MetadataBuilder.AddCustomDebugInformation(EntityHandle, GuidHandle, BlobHandle)-Methode von der pdbBuilder-Instanz aufrufen, um erweiterte PDB-Informationen zur Quelleneinbettung und Quellenindizierung hinzuzufügen.

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

Hinzufügen von Ressourcen mit PersistedAssemblyBuilder

Sie können MetadataBuilder.AddManifestResource(ManifestResourceAttributes, StringHandle, EntityHandle, UInt32) aufrufen, um beliebig viele Ressourcen hinzuzufügen. Die Datenströme müssen zu einem BlobBuilderverkettet werden, das Sie an das ManagedPEBuilder-Argument übergeben. Das folgende Beispiel zeigt, wie Sie Ressourcen erstellen und an die erstellte Assembly anfügen.

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

Das folgende Beispiel zeigt, wie Ressourcen aus der erstellten Assembly gelesen werden.

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

Anmerkung

Die Metadaten-Token für alle Mitglieder werden bei der Operation Saveausgefüllt. Verwenden Sie die Token eines generierten Typs und seine Mitglieder nicht vor dem Speichern, da sie Standardwerte haben oder Ausnahmen auslösen. Es ist sicher, Token für Typen zu verwenden, die referenziert und nicht generiert werden.

Einige APIs, die für das Emittieren einer Assembly nicht wichtig sind, werden nicht implementiert. beispielsweise wird GetCustomAttributes() nicht implementiert. Mit der Laufzeitimplementierung konnten Sie diese APIs nach dem Erstellen des Typs verwenden. Für die anhaltendeAssemblyBuilder, werfen sie NotSupportedExceptionoderNotImplementedException. Wenn Sie ein Szenario haben, das diese APIs erfordert, melden Sie ein Problem im Repository dotnet/runtime.

Eine alternative Möglichkeit zum Generieren von Assemblydateien finden Sie unter MetadataBuilder.