Системы. Рефлексия ion. Класс Emit.AssemblyBuilder

В этой статье приводятся дополнительные замечания к справочной документации по этому API.

Динамическая сборка — это сборка, созданная с помощью API-интерфейсов Рефлексия ion Emit. Динамическая сборка может ссылаться на типы, определенные в другой динамической или статической сборке. Вы можете использовать AssemblyBuilder для создания динамических сборок в памяти и выполнения кода во время выполнения того же приложения. В .NET 9 мы добавили новый PersistedAssemblyBuilder с полностью управляемой реализацией отражения, которая позволяет сохранить сборку в файл. В платформа .NET Framework можно выполнить оба действия: запустить динамическую сборку и сохранить ее в файле. Динамическая сборка, созданная для сохранения, называется сохраняемой сборкой, а обычная сборка только для памяти называется временной или запущенной. В платформа .NET Framework динамическая сборка может состоять из одного или нескольких динамических модулей. В .NET Core и .NET 5+ динамическая сборка может состоять только из одного динамического модуля.

Способ создания экземпляра отличается для каждой AssemblyBuilder реализации, но дальнейшие шаги по определению модуля, типа, метода или перечисления, а также для записи IL довольно похожи.

Запускаемые динамические сборки в .NET

Чтобы получить объект runnable AssemblyBuilder , используйте AssemblyBuilder.DefineDynamicAssembly этот метод. Динамические сборки можно создать с помощью одного из следующих режимов доступа:

  • AssemblyBuilderAccess.Run

    Динамическая сборка, представленная приложением, AssemblyBuilder может использоваться для выполнения созданного кода.

  • AssemblyBuilderAccess.RunAndCollect

    Динамическая сборка, представленная модулятором AssemblyBuilder , может использоваться для выполнения созданного кода и автоматически удаляется сборщиком мусора.

Режим доступа необходимо указать, указав соответствующее AssemblyBuilderAccess значение в вызове AssemblyBuilder.DefineDynamicAssembly метода при определении динамической сборки и не может быть изменено позже. Среда выполнения использует режим доступа динамической сборки для оптимизации внутреннего представления сборки.

В следующем примере показано, как создать и запустить сборку:

public void CreateAndRunAssembly(string assemblyPath)
{
    AssemblyBuilder ab = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("MyAssembly"), AssemblyBuilderAccess.Run);
    ModuleBuilder mob = ab.DefineDynamicModule("MyModule");
    TypeBuilder tb = mob.DefineType("MyType", TypeAttributes.Public | TypeAttributes.Class);
    MethodBuilder mb = tb.DefineMethod("SumMethod", MethodAttributes.Public | MethodAttributes.Static,
                                                                   typeof(int), new Type[] {typeof(int), typeof(int)});
    ILGenerator il = mb.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldarg_1);
    il.Emit(OpCodes.Add);
    il.Emit(OpCodes.Ret);

    Type type = tb.CreateType();

    MethodInfo method = type.GetMethod("SumMethod");
    Console.WriteLine(method.Invoke(null, new object[] { 5, 10 }));
}

Сохраняемые динамические сборки в .NET

AssemblyBuilder.Save API изначально не был перенесен в .NET (Core), так как реализация сильно зависит от собственного кода Windows, который также не был перенесен. В .NET 9 мы добавили полностью управляемую Reflection.Emit реализацию, которая поддерживает сохранение. Эта реализация не зависит от существующей реализации для конкретной Reflection.Emit среды выполнения. То есть теперь в .NET существуют две разные реализации, доступные для запуска и сохраняемые. Чтобы запустить сохраненную сборку, сначала сохраните ее в поток памяти или файл, а затем загрузите его обратно. Прежде чем PersistedAssemblyBuilderзапускать только созданную сборку и не сохранять ее, было трудно выполнить отладку этих сборок в памяти. Преимущества сохранения динамической сборки в файл:

  • Вы можете проверить созданную сборку с помощью таких средств, как ILVerify, или декомпилировать и вручную проверить ее с помощью таких средств, как ILSpy.
  • Сохраненная сборка может быть загружена напрямую, не нужно скомпилировать повторно, что может снизить время запуска приложения.

Чтобы создать PersistedAssemblyBuilder экземпляр, используйте public PersistedAssemblyBuilder(AssemblyName name, Assembly coreAssembly, IEnumerable<CustomAttributeBuilder>? assemblyAttributes = null) конструктор. Параметр coreAssembly используется для разрешения базовых типов среды выполнения и может использоваться для разрешения ссылочных версий сборки:

  • Если Reflection.Emit используется для создания сборки, предназначенной для конкретного TFM, откройте эталонные сборки для заданного TFM с помощью MetadataLoadContext и используйте значение свойства MetadataLoadContext.CoreAssembly для coreAssembly. Это значение позволяет генератору работать в одной версии среды выполнения .NET и использовать другую версию среды выполнения .NET.

  • Если Reflection.Emit используется для создания сборки, которая будет выполняться только в той же версии среды выполнения, что и версия среды выполнения, в которую выполняется компилятор (обычно в proc), базовая сборка может быть typeof(object).Assembly. В этом случае ссылочные сборки не нужны.

В следующем примере показано, как создать и сохранить сборку в потоке и запустить ее:

public void CreateSaveAndRunAssembly(string assemblyPath)
{
    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 }));
}

Чтобы задать точку входа для исполняемого файла или задать другие параметры для файла сборки, можно вызвать public MetadataBuilder GenerateMetadata(out BlobBuilder ilStream, out BlobBuilder mappedFieldData) метод и использовать заполненные метаданные для создания сборки с нужными параметрами, например:

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); 

Примечание.

Маркеры метаданных для всех членов заполняются операцией Save . Не используйте маркеры созданного типа и его членов перед сохранением, так как они будут иметь значения по умолчанию или исключение. Безопасно использовать маркеры для типов, на которые ссылаются, а не создаются.

Некоторые API, которые не важны для создания сборки, не реализуются; Например, GetCustomAttributes() не реализуется. Реализация среды выполнения позволяет использовать эти API после создания типа. Для сохраненных AssemblyBuilder, они бросают NotSupportedException или NotImplementedException. Если у вас есть сценарий, требующий этих API, отправьте проблему в репозитории dotnet/runtime.

Альтернативный способ создания файлов сборок см. в статье MetadataBuilder.

Сохраняемые динамические сборки в платформа .NET Framework

В платформа .NET Framework динамические сборки и модули можно сохранить в файлах. Для поддержки этой функции AssemblyBuilderAccess перечисление объявляет два дополнительных поля: Save и RunAndSave.

Динамические модули в сохраняемой динамической сборке сохраняются при сохранении динамической сборки с помощью Save метода. Чтобы создать исполняемый файл, SetEntryPoint необходимо вызвать метод, чтобы определить метод, который является точкой входа в сборку. Сборки сохраняются как библиотеки DLL по умолчанию, если SetEntryPoint метод не запрашивает создание консольного приложения или приложения под управлением Windows.

В следующем примере показано, как создать, сохранить и запустить сборку с помощью платформа .NET Framework.

public void CreateRunAndSaveAssembly(string assemblyPath)
{
    AssemblyBuilder ab = Thread.GetDomain().DefineDynamicAssembly(new AssemblyName("MyAssembly"), AssemblyBuilderAccess.RunAndSave);
    ModuleBuilder mob = ab.DefineDynamicModule("MyAssembly.dll");
    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);

    Type type = tb.CreateType();

    MethodInfo method = type.GetMethod("SumMethod");
    Console.WriteLine(method.Invoke(null, new object[] { 5, 10 }));
    ab.Save("MyAssembly.dll");
}

Некоторые методы базового Assembly класса, такие как GetModules и GetLoadedModules, не будут работать правильно при вызове из AssemblyBuilder объектов. Вы можете загрузить определенную динамическую сборку и вызвать методы в загруженной сборке. Например, чтобы убедиться, что модули ресурсов включены в возвращаемый Assembly список модулей, вызовите GetModules загруженный объект. Если динамическая сборка содержит несколько динамических модулей, имя файла манифеста сборки должно совпадать с именем модуля, указанным в качестве первого аргумента DefineDynamicModule метода.

Подписывание динамической KeyPair сборки не действует, пока сборка не будет сохранена на диске. Поэтому строгие имена не будут работать с временными динамическими сборками.

Динамические сборки могут ссылаться на типы, определенные в другой сборке. Временная динамическая сборка может безопасно ссылаться на типы, определенные в другой временной динамической сборке, сохраняемой динамической сборке или статической сборке. Однако среда CLR не позволяет сохраняемому динамическому модулю ссылаться на тип, определенный в временном динамическом модуле. Это связано с тем, что при загрузке сохраняемого динамического модуля после сохранения на диск среда выполнения не может разрешать ссылки на типы, определенные в временном динамическом модуле.

Ограничения на выдачу доменов удаленных приложений

Для некоторых сценариев требуется создать динамическую сборку и выполнить ее в домене удаленного приложения. Рефлексия инициации не позволяет динамической сборке создаваться непосредственно в домен удаленного приложения. Решение состоит в том, чтобы выпустить динамическую сборку в текущем домене приложения, сохранить динамическую сборку на диск, а затем загрузить динамическую сборку в домен удаленного приложения. Домены удаленного взаимодействия и приложений поддерживаются только в платформа .NET Framework.