.NET での永続化された動的アセンブリ
この記事では、この API のリファレンス ドキュメントに補足的な解説を提供します。
AssemblyBuilder.Save API は、実装が移植されていない Windows 固有のネイティブ コードに大きく依存していたため、最初は .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 })); }
Reflection.Emit
を使用して特定の TFM をターゲットとするアセンブリを生成する場合は、MetadataLoadContext
を使用して特定の TFM の参照アセンブリを開き、coreAssembly
の MetadataLoadContext.CoreAssembly プロパティの値を使用します。 この値により、ジェネレーターを 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(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 を使用してアセンブリを作成するには:
- ISymbolDocumentWriter インスタンスを ModuleBuilder.DefineDocument(String, Guid, Guid, Guid) メソッドで作成します。 メソッドの IL を出力するときに、対応するシンボル情報も出力します。
- GenerateMetadata(BlobBuilder, BlobBuilder) メソッドによって生成された
pdbBuilder
インスタンスを使用して、PortablePdbBuilder インスタンスを作成します。 PortablePdbBuilder
を Blobにシリアル化し、Blob
を PDB ファイル ストリームに書き込みます (スタンドアロン PDB を生成する場合のみ)。- DebugDirectoryBuilder インスタンスを作成し、DebugDirectoryBuilder.AddCodeViewEntry (スタンドアロン PDB) または DebugDirectoryBuilder.AddEmbeddedPortablePdbEntryを追加します。
- 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;
}
さらに、CustomDebugInformation を追加するには、pdbBuilder
インスタンスから MetadataBuilder.AddCustomDebugInformation(EntityHandle, GuidHandle, BlobHandle) メソッドを呼び出して、ソース埋め込みとソース インデックス作成の高度な 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) を呼び出して、必要な数のリソースを追加できます。 ストリームは、ManagedPEBuilder 引数に渡す 1 つの BlobBuilder に連結する必要があります。 次の例は、リソースを作成し、作成されたアセンブリにアタッチする方法を示しています。
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
の場合、NotSupportedException
または NotImplementedException
がスローされます。 これらの API を必要とするシナリオがある場合は、dotnet/runtime リポジトリに問題を提出してください。
アセンブリ ファイルを生成する別の方法については、MetadataBuilderを参照してください。
.NET