Freigeben über


Beibehaltene dynamische Assemblys 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 permanente Assembly ausführen möchten, speichern Sie sie zuerst in einem Speicherdatenstrom oder einer Datei, und laden Sie sie dann wieder.

Vorher PersistedAssemblyBuilderkonnten Sie nur eine generierte Assembly ausführen und sie 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. Der coreAssembly Parameter wird verwendet, um Basislaufzeittypen aufzulösen und kann zum Auflösen der Referenzassemblyversionsverwaltung verwendet werden:

  • Wenn Reflection.Emit zum Generieren einer Assembly verwendet wird, die nur auf derselben Laufzeitversion wie die Laufzeitversion ausgeführt wird, auf der der Compiler ausgeführt wird (in der Regel in proc), kann die Kernassembly einfach typeof(object).Assemblysein. Im folgenden Beispiel wird veranschaulicht, wie Sie eine Assembly in einem Stream erstellen und speichern und mit der aktuellen Laufzeitassembly ausführen:

    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 }));
    }
    
  • Wenn Reflection.Emit zum Generieren einer Assembly verwendet wird, die auf eine bestimmte TFM ausgerichtet ist, öffnen Sie die Referenzassemblys für das angegebene TFM, MetadataLoadContext und verwenden Sie den Wert der MetadataLoadContext.CoreAssembly -Eigenschaft für coreAssembly. 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. Suchen Sie z. B. anstelle von typeof(int)" den System.Int32 Typ MetadataLoadContext.CoreAssembly nach Dem 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
    }
    

Festlegen des Einstiegspunkts für eine ausführbare Datei

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

Ausgeben von Symbolen und Generieren von PDB

Die Symbolmetadaten werden in den pdbBuilder Ausgabeparameter aufgefüllt, wenn Sie die GenerateMetadata(BlobBuilder, BlobBuilder) Methode für eine PersistedAssemblyBuilder Instanz aufrufen. So erstellen Sie eine Assembly mit einem tragbaren PDB:

  1. Erstellen Sie ISymbolDocumentWriter Instanzen mit der ModuleBuilder.DefineDocument(String, Guid, Guid, Guid) Methode. Geben Sie beim Ausgeben der Il-Methode auch die entsprechenden Symbolinformationen aus.
  2. Erstellen Sie eine PortablePdbBuilder Instanz mithilfe der instanz, die pdbBuilder von der GenerateMetadata(BlobBuilder, BlobBuilder) Methode erstellt wird.
  3. Serialisieren Sie die PortablePdbBuilder Datei in ein Blob, und schreiben Sie den Blob In einen PDB-Dateidatenstrom (nur, wenn Sie einen eigenständigen PDB generieren).
  4. Erstellen Sie eine DebugDirectoryBuilder Instanz, und fügen Sie eine DebugDirectoryBuilder.AddCodeViewEntry (eigenständige PDB) oder DebugDirectoryBuilder.AddEmbeddedPortablePdbEntry.
  5. Legen Sie das optionale debugDirectoryBuilder Argument beim Erstellen der PEBuilder Instanz fest.

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

Darüber hinaus können Sie die CustomDebugInformation Methode hinzufügen, indem Sie die MetadataBuilder.AddCustomDebugInformation(EntityHandle, GuidHandle, BlobHandle) Methode aus der pdbBuilder Instanz aufrufen, um erweiterte PDB-Informationen zur Quelleinbettung und Quellindizierung 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 aufrufen MetadataBuilder.AddManifestResource(ManifestResourceAttributes, StringHandle, EntityHandle, UInt32) , um beliebig viele Ressourcen hinzuzufügen. Datenströme müssen mit einem BlobBuilder verkettet werden, den 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 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);
}

Hinweis

Die Metadatentoken für alle Member werden für den Save Vorgang aufgefüllt. Verwenden Sie die Token eines generierten Typs und seiner Member vor dem Speichern nicht, da sie Standardwerte aufweisen oder Ausnahmen auslösen. Es ist sicher, Token für Typen zu verwenden, auf die verwiesen wird, nicht generiert.

Einige APIs, die für das Emittieren einer Assembly nicht wichtig sind, werden nicht implementiert. Beispiel: GetCustomAttributes() ist nicht implementiert. Mit der Laufzeitimplementierung konnten Sie diese APIs nach dem Erstellen des Typs verwenden. Für die beibehaltenen AssemblyBuilder, sie werfen NotSupportedException oder NotImplementedException. Wenn Sie über ein Szenario verfügen, das diese APIs erfordert, geben Sie ein Problem im Dotnet/Runtime-Repository an.

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