.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 的組件:
- 使用 ModuleBuilder.DefineDocument(String, Guid, Guid, Guid) 方法建立 ISymbolDocumentWriter 實例。 發出方法的 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;
}
此外,您可以從 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
,他們會丟出 NotSupportedException
或 NotImplementedException
。 如果您有需要這些 API 的案例,請在 dotnet/runtime 存放庫中提出問題,。
如需產生組件檔案的替代方式,請參閱 MetadataBuilder。