.NET의 지속형 동적 어셈블리
이 문서는 이 API에 대한 참조 설명서를 보충하는 추가 설명을 제공합니다.
구현은 이식되지 않은 Windows 관련 네이티브 코드에 크게 의존하기 때문에 AssemblyBuilder.Save API는 원래 .NET(Core)로 이식되지 않았습니다. .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에 대한 레퍼런스 어셈블리를 열고,coreAssembly
에 대해 MetadataLoadContext.CoreAssembly 속성의 값을 사용합니다. 이 값을 사용하면 생성기가 하나의 .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를 사용하여 어셈블리를 만들려면 다음을 수행합니다.
- ModuleBuilder.DefineDocument(String, Guid, Guid, Guid) 메서드를 사용하여 ISymbolDocumentWriter 인스턴스를 만듭니다. 메서드의 IL을 내보내는 동안 해당 기호 정보도 내보냅니다.
-
GenerateMetadata(BlobBuilder, BlobBuilder) 메서드에서 생성된
pdbBuilder
인스턴스를 사용하여 PortablePdbBuilder 인스턴스를 만듭니다. -
PortablePdbBuilder
을(를) Blob로 직렬화하고, (독립 실행형 PDB를 생성하는 경우에만)Blob
를 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;
}
먼저 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(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