共用方式為


.NET 中持久化的動態組件

本文提供此 API 參考文件的補充備註。

AssemblyBuilder.Save API 最初沒有被移植到 .NET(Core),因為其實作嚴重依賴於尚未移植的 Windows 特定原生程式碼。 .NET 9 的新功能:PersistedAssemblyBuilder 類別會新增完全管理的 Reflection.Emit 實作,以支持儲存。 此實作與預先存在的運行時間特定 Reflection.Emit 實作沒有相依性。 也就是說,現在 .NET 中有兩個不同的實作,可執行和保存。 若要執行持久化的元件,請先將它儲存到記憶體資料流或檔案中,然後重新載入。

PersistedAssemblyBuilder之前,您只能執行生成的程式集,而無法儲存它。 由於組件僅存在於記憶體中,因此很難進行除錯。 將動態元件儲存至檔案的優點如下:

  • 您可以使用像 ILVerify 這類的工具來驗證所產生的組件,或者使用像 ILSpy 這類的工具來反編譯並手動檢查它。
  • 您可以直接載入已儲存的元件,而不需要再次編譯,這可能會減少應用程式啟動時間。

若要建立 PersistedAssemblyBuilder 實例,請使用 PersistedAssemblyBuilder(AssemblyName, Assembly, IEnumerable<CustomAttributeBuilder>) 建構函式。 coreAssembly 參數可用來解析基底運行時間類型,並可用於解析參考元件版本設定:

  • 如果使用 Reflection.Emit 產生的元件將僅在與編譯器運行所在的相同執行階段版本上運行(通常在同一進程中),則核心元件可以簡單地為 typeof(object).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 }));
    }
    
  • 如果使用 Reflection.Emit 產生以特定 TFM 為目標的元件,請透過 MetadataLoadContext 開啟指定 TFM 的參考元件,然後將 MetadataLoadContext.CoreAssembly 屬性的值用於 coreAssembly。 這個值可讓產生器在一個 .NET 運行時間版本上執行,並以不同的 .NET 運行時間版本為目標。 參考核心類型時,您應該使用 MetadataLoadContext 實例所傳回的類型。 例如,應該依名稱在 MetadataLoadContext.CoreAssembly 中尋找 System.Int32 類型,而不是 typeof(int)

    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
    }
    

設定可執行文件的進入點

若要設定可執行文件的進入點,或設定元件檔的其他選項,您可以呼叫 public MetadataBuilder GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder mappedFieldData) 方法,並使用填入的元數據產生具有所需選項的元件,例如:

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

發出符號並產生 PDB

當您在 PersistedAssemblyBuilder 實例上呼叫 GenerateMetadata(BlobBuilder, BlobBuilder) 方法時,符號中繼資料會被填入 pdbBuilder out 參數。 若要建立具有可攜式 PDB 的組件:

  1. 使用 ModuleBuilder.DefineDocument(String, Guid, Guid, Guid) 方法建立 ISymbolDocumentWriter 實例。 發出方法的 IL 時,同時發出對應的符號資訊。
  2. 使用 GenerateMetadata(BlobBuilder, BlobBuilder) 方法所產生的 pdbBuilder 實例,建立 PortablePdbBuilder 實例。
  3. PortablePdbBuilder 串行化為 Blob,並將 Blob 寫入 PDB 檔案數據流中(只有在您產生獨立 PDB 時)。
  4. 建立 DebugDirectoryBuilder 實體,並新增 DebugDirectoryBuilder.AddCodeViewEntry (獨立 PDB) 或 DebugDirectoryBuilder.AddEmbeddedPortablePdbEntry
  5. 建立 PEBuilder 實例時,請設定選擇性 debugDirectoryBuilder 自變數。

下列範例示範如何發出符號信息併產生 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;
}

此外,您可以從 pdbBuilder 實例中呼叫 MetadataBuilder.AddCustomDebugInformation(EntityHandle, GuidHandle, BlobHandle) 方法來新增 CustomDebugInformation,以新增來源嵌入和來源索引的進階 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));
}

使用 PersistedAssemblyBuilder 新增資源

您可以呼叫 MetadataBuilder.AddManifestResource(ManifestResourceAttributes, StringHandle, EntityHandle, UInt32),視需要新增盡可能多的資源。 串流必須串接成一個 BlobBuilder,然後您將該 BlobBuilder 傳入 ManagedPEBuilder 參數。 下列範例示範如何建立資源,並將它附加至所建立的元件。

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

下列範例示範如何從建立的元件讀取資源。

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

注意

所有成員的元數據令牌都會在 Save 作業中被填充。 儲存之前,請勿使用所產生型別及其成員的令牌,因為它們會有預設初始值或拋出例外。 使用被參考而非被產生的類型之令牌是安全的。

對於生成組件不重要的一些 API 不會被實作;例如,GetCustomAttributes() 並未實作。 使用運行時間實作,您可以在建立類型之後使用這些 API。 對於持續的 AssemblyBuilder,他們會丟出 NotSupportedExceptionNotImplementedException。 如果您有需要這些 API 的案例,請在 dotnet/runtime 存放庫中提出問題,

如需產生組件檔案的替代方式,請參閱 MetadataBuilder