.NET での永続化された動的アセンブリ

この記事では、この API のリファレンス ドキュメントへの補足的な解説を提供します。

実装は、移植されていない Windows 固有のネイティブ コードに大きく依存していたため、API は AssemblyBuilder.Save 最初は .NET (Core) に移植されませんでした。 .NET 9 の新機能であるこのクラスは、保存を PersistedAssemblyBuilder サポートするフル マネージド Reflection.Emit の実装を追加します。 この実装は、既存のランタイム固有 Reflection.Emit の実装には依存しません。 つまり、.NET には、実行可能で永続化された 2 つの異なる実装があります。 永続化されたアセンブリを実行するには、まずメモリ ストリームまたはファイルに保存してから、再度読み込みます。

以前 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 }));
    }
    
  • 特定の TFM を対象とするアセンブリを生成するために使用する場合Reflection.Emitは、MetadataLoadContext.CoreAssembly プロパティcoreAssembly値を使用してMetadataLoadContext、指定された TFM の参照アセンブリを開きます。 この値により、ジェネレーターを 1 つの .NET ランタイム バージョンで実行し、別の .NET ランタイム バージョンをターゲットにすることができます。 コア型を参照するときは、インスタンスによって返される型を MetadataLoadContext 使用する必要があります。 たとえば、代わりにtypeof(int)、名前でMetadataLoadContext.CoreAssembly型をSystem.Int32検索します。

    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 を生成する

インスタンスでメソッドを呼び出GenerateMetadata(BlobBuilder, BlobBuilder)すと、シンボル メタデータが out パラメーターにPersistedAssemblyBuilder入力されますpdbBuilder。 ポータブル PDB を使用してアセンブリを作成するには:

  1. メソッドを使用してインスタンスをModuleBuilder.DefineDocument(String, Guid, Guid, Guid)作成ISymbolDocumentWriterします。 メソッドの IL を出力するときに、対応するシンボル情報も出力します。
  2. PortablePdbBuilderメソッドによって生成されたインスタンスをpdbBuilder使用してインスタンスをGenerateMetadata(BlobBuilder, BlobBuilder)作成します。
  3. PortablePdbBuilderBlobシリアル化し、PDB ファイル ストリームに書き込Blobみます (スタンドアロン PDB を生成する場合のみ)。
  4. インスタンスを DebugDirectoryBuilder 作成し、 DebugDirectoryBuilder.AddCodeViewEntry (スタンドアロン PDB) または DebugDirectoryBuilder.AddEmbeddedPortablePdbEntry.
  5. インスタンスの作成時に省略可能 debugDirectoryBuilder な引数を PEBuilder 設定します。

次の例は、シンボル情報を出力し、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);
}

Note

操作では、すべてのメンバーのメタデータ トークンが設定されます Save 。 既定値または例外をスローするため、保存する前に、生成された型とそのメンバーのトークンを使用しないでください。 生成されずに参照される型にトークンを使用しても安全です。

アセンブリの出力に重要ではない API の一部は実装されていません。たとえば、 GetCustomAttributes() 実装されていません。 ランタイム実装では、型の作成後にこれらの API を使用できました。 永続化された AssemblyBuilder場合は、スロー NotSupportedException または NotImplementedException. これらの API を必要とするシナリオがある場合は、dotnet/runtime リポジトリに問題を 提出してください

アセンブリ ファイルを生成する別の方法については、次を参照してください MetadataBuilder