.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 的程序集,请使用给定 TFM MetadataLoadContext 的引用程序集并使用 MetadataLoadContext.CoreAssembly 属性的值coreAssembly 此值允许生成器在一个 .NET 运行时版本上运行,并面向不同的 .NET 运行时版本。 引用核心类型时,应使用实例返回 MetadataLoadContext 的类型。 例如,而不是 typeof(int)按名称查找 System.Int32 类型 MetadataLoadContext.CoreAssembly

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

发出符号并生成 PDB

在实例上PersistedAssemblyBuilder调用GenerateMetadata(BlobBuilder, BlobBuilder)方法时,符号元数据将填充到 pdbBuilder out 参数中。 使用可移植 PDB 创建程序集:

  1. 使用ModuleBuilder.DefineDocument(String, Guid, Guid, Guid)该方法创建ISymbolDocumentWriter实例。 发出方法的 IL 时,还会发出相应的符号信息。
  2. PortablePdbBuilder使用pdbBuilder方法生成的GenerateMetadata(BlobBuilder, BlobBuilder)实例创建实例。
  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)方法添加源嵌入和源索引高级 PDB 信息来添加CustomDebugInformation

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 传入自变量的 ManagedPEBuilder 流。 以下示例演示如何创建资源并将其附加到创建的程序集。

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

注意

所有成员的元数据令牌都在操作上 Save 填充。 保存前不要使用生成的类型的标记及其成员,因为它们将具有默认值或引发异常。 对于引用的类型(未生成)使用令牌是安全的。

未实现某些不重要的发出程序集的 API;例如, GetCustomAttributes() 未实现。 使用运行时实现,可以在创建类型后使用这些 API。 对于持久化 AssemblyBuilder,他们抛出 NotSupportedExceptionNotImplementedException。 如果有需要这些 API 的方案,请将问题提交到 dotnet/runtime 存储库中。

有关生成程序集文件的替代方法,请参阅 MetadataBuilder