Udostępnij za pośrednictwem


Utrwalone zestawy dynamiczne na platformie .NET

Ten artykuł zawiera dodatkowe uwagi dotyczące dokumentacji referencyjnej dla tego interfejsu API.

Interfejs AssemblyBuilder.Save API nie został pierwotnie portowany do platformy .NET (Core), ponieważ implementacja była w dużym stopniu zależna od kodu natywnego specyficznego dla systemu Windows, który również nie został portowany. Nowość na platformie .NET 9 klasa PersistedAssemblyBuilder dodaje w pełni zarządzaną Reflection.Emit implementację, która obsługuje zapisywanie. Ta implementacja nie jest zależna od istniejącej implementacji specyficznej dla Reflection.Emit środowiska uruchomieniowego. Oznacza to, że teraz istnieją dwie różne implementacje na platformie .NET, możliwe do uruchomienia i utrwalone. Aby uruchomić utrwalone zestawy, najpierw zapisz go w strumieniu pamięci lub pliku, a następnie załaduj go z powrotem.

Przed PersistedAssemblyBuilderpoleceniem można było uruchomić tylko wygenerowany zestaw i nie zapisać go. Ponieważ zestaw był tylko w pamięci, trudno było debugować. Zalety zapisywania zestawu dynamicznego w pliku to:

  • Możesz zweryfikować wygenerowany zestaw za pomocą narzędzi, takich jak ILVerify, lub dekompilować go ręcznie, za pomocą narzędzi takich jak ILSpy.
  • Zapisany zestaw można załadować bezpośrednio, nie trzeba ponownie kompilować, co może zmniejszyć czas uruchamiania aplikacji.

Aby utworzyć PersistedAssemblyBuilder wystąpienie, użyj konstruktora PersistedAssemblyBuilder(AssemblyName, Assembly, IEnumerable<CustomAttributeBuilder>) . Parametr służy do rozpoznawania coreAssembly podstawowych typów środowiska uruchomieniowego i może służyć do rozpoznawania wersji zestawu referencyjnego:

  • Jeśli Reflection.Emit jest używany do generowania zestawu, który będzie wykonywany tylko w tej samej wersji środowiska uruchomieniowego co wersja środowiska uruchomieniowego, na którym jest uruchomiony kompilator (zazwyczaj w proc), podstawowy zestaw może być po prostu typeof(object).Assembly. W poniższym przykładzie pokazano, jak utworzyć i zapisać zestaw w strumieniu i uruchomić go przy użyciu bieżącego zestawu środowiska uruchomieniowego:

    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 }));
    }
    
  • Jeśli Reflection.Emit jest używany do generowania zestawu przeznaczonego dla określonego serwera TFM, otwórz zestawy referencyjne dla danego serwera TFM przy użyciu MetadataLoadContext i użyj wartości właściwości MetadataLoadContext.CoreAssembly dla elementu coreAssembly. Ta wartość umożliwia uruchamianie generatora w jednej wersji środowiska uruchomieniowego platformy .NET i określanie innej wersji środowiska uruchomieniowego platformy .NET. Podczas odwoływania się do typów podstawowych należy używać typów zwracanych przez MetadataLoadContext wystąpienie. Na przykład zamiast typeof(int)znajdź System.Int32 typ według MetadataLoadContext.CoreAssembly nazwy:

    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
    }
    

Ustawianie punktu wejścia dla pliku wykonywalnego

Aby ustawić punkt wejścia pliku wykonywalnego lub ustawić inne opcje dla pliku zestawu, możesz wywołać public MetadataBuilder GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder mappedFieldData) metodę i użyć wypełnionych metadanych, aby wygenerować zestaw z żądanymi opcjami, na przykład:

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

Emituj symbole i generuj plik PDB

Metadane symboli są wypełniane w parametrze pdbBuilder out podczas wywoływania GenerateMetadata(BlobBuilder, BlobBuilder) metody w wystąpieniu PersistedAssemblyBuilder . Aby utworzyć zestaw z przenośnym plikiem PDB:

  1. Utwórz ISymbolDocumentWriter wystąpienia za pomocą ModuleBuilder.DefineDocument(String, Guid, Guid, Guid) metody . Emitując il metody, emitują również odpowiednie informacje o symbolu.
  2. PortablePdbBuilder Utwórz wystąpienie przy użyciu pdbBuilder wystąpienia wygenerowanego przez metodę GenerateMetadata(BlobBuilder, BlobBuilder) .
  3. Serializuj PortablePdbBuilder element w obiekcie Blobi zapisz element Blob w strumieniu plików PDB (tylko w przypadku generowania autonomicznego pliku PDB).
  4. DebugDirectoryBuilder Utwórz wystąpienie i dodaj DebugDirectoryBuilder.AddCodeViewEntry element (autonomiczny plik PDB) lub DebugDirectoryBuilder.AddEmbeddedPortablePdbEntry.
  5. Ustaw opcjonalny debugDirectoryBuilder argument podczas tworzenia PEBuilder wystąpienia.

W poniższym przykładzie pokazano, jak emitować informacje o symbolach i generować plik 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;
}

Ponadto możesz dodać CustomDebugInformation metodę , wywołując metodę MetadataBuilder.AddCustomDebugInformation(EntityHandle, GuidHandle, BlobHandle) z wystąpienia w celu dodania osadzania źródłowego pdbBuilder i indeksowania źródła zaawansowanych informacji pdB.

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

Dodawanie zasobów za pomocą elementu PersistedAssemblyBuilder

Możesz wywołać metodę MetadataBuilder.AddManifestResource(ManifestResourceAttributes, StringHandle, EntityHandle, UInt32) , aby dodać dowolną liczbę zasobów zgodnie z potrzebami. Strumienie należy połączyć w taki, BlobBuilder który zostanie przekazany do argumentuManagedPEBuilder. W poniższym przykładzie pokazano, jak utworzyć zasoby i dołączyć je do utworzonego zestawu.

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

Uwaga

Tokeny metadanych dla wszystkich elementów członkowskich są wypełniane w Save operacji. Nie używaj tokenów wygenerowanego typu i jego składowych przed zapisaniem, ponieważ będą miały wartości domyślne lub zgłaszają wyjątki. Bezpieczne jest używanie tokenów dla typów, do których odwołuje się odwołanie, a nie generowanych.

Niektóre interfejsy API, które nie są ważne do emitowania zestawu, nie są implementowane; na przykład GetCustomAttributes() nie jest zaimplementowany. Implementacja środowiska uruchomieniowego umożliwiała korzystanie z tych interfejsów API po utworzeniu typu. W przypadku utrwalonego AssemblyBuilderobiektu zgłaszają wartość NotSupportedException lub NotImplementedException. Jeśli masz scenariusz, który wymaga tych interfejsów API, zgłoś problem w repozytorium dotnet/runtime.

Aby uzyskać alternatywny sposób generowania plików zestawów, zobacz MetadataBuilder.