Сохранённые динамические сборки в .NET
В этой статье приводятся дополнительные замечания к справочной документации по этому API.
API AssemblyBuilder.Save изначально не был перенесен в .NET (Core), так как реализация сильно зависит от собственного кода Windows, который также не был перенесен. Новый в .NET 9 класс PersistedAssemblyBuilder добавляет полностью управляемую Reflection.Emit
реализацию, которая поддерживает сохранение. Эта реализация не зависит от уже существующей, специфичной для среды выполнения реализации Reflection.Emit
. То есть теперь в .NET существуют две разные реализации, доступные для запуска и сохраняемые. Чтобы запустить сохраненную сборку, сначала сохраните ее в поток памяти или файл, а затем загрузите его обратно.
Перед PersistedAssemblyBuilder
можно запустить только созданную сборку и не сохранить ее. Так как сборка была только в памяти, было трудно выполнить отладку. Преимущества сохранения динамической сборки в файл:
- Вы можете проверить созданную сборку с помощью таких средств, как ILVerify, или декомпилировать и вручную проверить ее с помощью таких средств, как ILSpy.
- Сохраненная сборка может быть загружена напрямую, не нужно скомпилировать повторно, что может снизить время запуска приложения.
Чтобы создать экземпляр PersistedAssemblyBuilder
, используйте конструктор PersistedAssemblyBuilder(AssemblyName, Assembly, IEnumerable<CustomAttributeBuilder>). Параметр coreAssembly
используется для разрешения базовых типов среды выполнения и может использоваться для разрешения ссылочных версий сборки:
Если
Reflection.Emit
используется для создания сборки, которая будет выполняться только в той же версии среды выполнения, что и версия среды выполнения компилятора (обычно в proc), базовая сборка может быть просто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(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
Метаданные символов заполняются параметром pdbBuilder
out при вызове метода GenerateMetadata(BlobBuilder, BlobBuilder) в экземпляре PersistedAssemblyBuilder
. Чтобы создать сборку с переносимым PDB, выполните приведенные действия.
- Создайте экземпляры ISymbolDocumentWriter с помощью метода ModuleBuilder.DefineDocument(String, Guid, Guid, Guid). При генерации IL метода также выводится соответствующая информация о символах.
- Создайте экземпляр PortablePdbBuilder с помощью экземпляра
pdbBuilder
, созданного методом GenerateMetadata(BlobBuilder, BlobBuilder). - Сериализируйте
PortablePdbBuilder
в Blobи напишитеBlob
в поток PDB-файлов (только если создается автономная PDB). - Создайте экземпляр DebugDirectoryBuilder и добавьте DebugDirectoryBuilder.AddCodeViewEntry (автономный PDB) или DebugDirectoryBuilder.AddEmbeddedPortablePdbEntry.
- Задайте необязательный аргумент
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;
}
Кроме того, можно добавить CustomDebugInformation, вызвав метод MetadataBuilder.AddCustomDebugInformation(EntityHandle, GuidHandle, BlobHandle) из экземпляра pdbBuilder
, чтобы внедрить исходные данные и индексировать их, добавив расширенные сведения о 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, который передается в аргумент 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.