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 API AssemblyBuilder.Save 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ą implementację Reflection.Emit, która obsługuje zapisywanie. Ta implementacja nie jest zależna od istniejącej, specyficznej dla środowiska uruchomieniowego implementacji Reflection.Emit. Oznacza to, że teraz istnieją dwie różne implementacje na platformie .NET, możliwe do uruchomienia i utrwalone. Aby uruchomić utrwalony zestaw, najpierw zapisz go w strumieniu pamięci lub pliku, a następnie załaduj z powrotem.

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

  • Jeśli Reflection.Emit służy do generowania zestawu, który zostanie wykonany tylko w tej samej wersji środowiska uruchomieniowego co wersja środowiska uruchomieniowego, na którym działa 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 TFM, otwórz zestawy referencyjne dla danego TFM używając MetadataLoadContext i użyj wartości właściwości MetadataLoadContext.CoreAssembly dla 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 wystąpienie MetadataLoadContext. Na przykład zamiast typeof(int)znajdź typ System.Int32 w MetadataLoadContext.CoreAssembly według 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ć metodę public MetadataBuilder GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder mappedFieldData) i użyć wypełnionych metadanych, aby wygenerować zestaw z żądanymi opcjami, na przykład:

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

Emituj symbole i generuj plik PDB

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

  1. Utwórz wystąpienia ISymbolDocumentWriter za pomocą metody ModuleBuilder.DefineDocument(String, Guid, Guid, Guid). Emitując IL metody, emituje również odpowiadające informacje o symbolach.
  2. Utwórz wystąpienie PortablePdbBuilder przy użyciu wystąpienia pdbBuilder wygenerowanego przez metodę GenerateMetadata(BlobBuilder, BlobBuilder).
  3. Serializuj PortablePdbBuilder do Blobi zapisz Blob do strumienia pliku PDB (tylko jeśli generujesz samodzielny plik PDB).
  4. Utwórz wystąpienie DebugDirectoryBuilder i dodaj DebugDirectoryBuilder.AddCodeViewEntry (samodzielny plik PDB) lub DebugDirectoryBuilder.AddEmbeddedPortablePdbEntry.
  5. Ustaw opcjonalny argument debugDirectoryBuilder podczas tworzenia wystąpienia PEBuilder.

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, wywołując metodę MetadataBuilder.AddCustomDebugInformation(EntityHandle, GuidHandle, BlobHandle) z wystąpienia pdbBuilder w celu dodania osadzania źródłowego i zaawansowanego indeksowania źródłowego pliku 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ą PersistedAssemblyBuilder

Możesz wywołać MetadataBuilder.AddManifestResource(ManifestResourceAttributes, StringHandle, EntityHandle, UInt32), aby dodać dowolną liczbę zasobów. Strumienie muszą być łączone w jeden BlobBuilder, który jest przekazywany do argumentu ManagedPEBuilder. W poniższym przykładzie pokazano, jak utworzyć zasoby i dołączyć je do utworzonego zestawu.

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

W poniższym przykładzie pokazano, jak odczytywać zasoby z utworzonego zestawu.

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

Notatka

Tokeny metadanych dla wszystkich członków są wypełniane podczas operacji Save. 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 się odwołuje, 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 zaimplementowana. Implementacja środowiska uruchomieniowego umożliwiała korzystanie z API po utworzeniu typu. W przypadku utrwalonego AssemblyBuilderproponują 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 asemblacji, zobacz MetadataBuilder.