Язык программирования C#9 и платформа .NET5 - Эндрю Троелсен
Шрифт:
Интервал:
Закладка:
Выпуск типа HelloClass и строковой переменной-члена
Теперь, когда вы лучше понимаете роль метода ModuleBuilder.CreateType(), давайте посмотрим, как можно выпустить открытый тип класса HelloWorld и закрытую строковую переменную:
// Определить открытый класс по имени HelloWorld.
TypeBuilder helloWorldClass =
module.DefineType("MyAssembly.HelloWorld",
TypeAttributes.Public);
// Определить закрытую переменную-член типа String по имени theMessage.
FieldBuilder msgField = helloWorldClass.DefineField(
"theMessage",
Type.GetType("System.String"),
attributes: FieldAttributes.Private);
Обратите внимание, что метод TypeBuilder.DefineField() предоставляет доступ к объекту типа FieldBuilder. В классе TypeBuilder также определены дополнительные методы, которые обеспечивают доступ к другим типам "построителей". Например, метод DefineConstructor() возвращает объект типа ConstructorBuilder, метод DefineProperty() — объект типа PropertyBuilder и т.д.
Выпуск конструкторов
Как упоминалось ранее, для определения конструктора текущего типа можно применять метод TypeBuilder.DefineConstructor(). Однако когда дело доходит до реализации конструктора HelloClass, в тело конструктора необходимо вставить низкоуровневый код CIL, который будет отвечать за присваивание входного параметра внутренней закрытой строке. Чтобы получить объект типа ILGenerator, понадобится вызвать метод GetILGenerator() из соответствующего типа "построителя" (в данном случае ConstructorBuilder).
Помещение кода CIL в реализацию членов осуществляется с помощью метода Emit() класса ILGenerator. В самом методе Emit() часто используется тип класса Opcodes, который открывает доступ к набору кодов операций CIL через свойства только для чтения. Например, свойство Opcodes.Ret обозначает возвращение из вызова метода .Opcodes.Stfid создает присваивание значения переменной-члену, a Opcodes.Call применяется для вызова заданного метода (конструктора базового класса в данном случае). Итак, логика для реализации конструктора будет выглядеть следующим образом:
// Создать специальный конструктор, принимающий
// единственный аргумент типа string.
Type[] constructorArgs = new Type[1];
constructorArgs[0] = typeof(string);
ConstructorBuilder constructor =
helloWorldClass.DefineConstructor(
MethodAttributes.Public,
CallingConventions.Standard,
constructorArgs);
// Выпустить необходимый код CIL для конструктора.
ILGenerator constructorIl = constructor.GetILGenerator();
constructorIl.Emit(OpCodes.Ldarg_0);
Type objectClass = typeof(object);
ConstructorInfo superConstructor =
objectClass.GetConstructor(new Type[0]);
constructorIl.Emit(OpCodes.Call, superConstructor);
// Загрузить в стек указатель this объекта.
constructorIl.Emit(OpCodes.Ldarg_0);
constructorIl.Emit(OpCodes.Ldarg_1);
// Загрузить входной аргумент в виртуальный стек и сохранить его в msgField
constructorIl.Emit(OpCodes.Stfld, msgField);
constructorIl.Emit(OpCodes.Ret);
Как вам теперь уже известно, в результате определения специального конструктора для типа стандартный конструктор молча удаляется. Чтобы снова определить конструктор без аргументов, нужно просто вызвать метод DefineDefaultConstructor() типа TypeBuilder:
// Создать стандартный конструктор.
helloWorldClass.DefineDefaultConstructor(
MethodAttributes.Public);
Выпуск метода SayHello()
В заключение давайте исследуем процесс выпуска метода SayHello(). Первая задача связана с получением объекта типа MethodBuilder из переменной helloWorldClass. После этого можно определить сам метод и получить внутренний объект типа ILGenerator для вставки необходимых инструкций CIL:
// Создать метод SayHello.
MethodBuilder sayHiMethod = helloWorldClass.DefineMethod(
"SayHello", MethodAttributes.Public, null, null);
methodIl = sayHiMethod.GetILGenerator();
// Вывести строку на консоль.
methodIl.EmitWriteLine("Hello from the HelloWorld class!");
methodIl.Emit(OpCodes.Ret);
Здесь был определен открытый метод (т.к. указано значение MethodAttributes.Public), который не имеет параметров и ничего не возвращает (на что указывают значения null в вызове DefineMethod()). Также обратите внимание на вызов EmitWriteLine(). Посредством данного вспомогательного метода класса ILGenerator можно записать строку в стандартный поток вывода, приложив минимальные усилия.
Использование динамически сгенерированной сборки
Теперь, когда у вас есть логика для создания сборки, осталось лишь выполнить сгенерированный код. Логика в вызывающем коде обращается к методу CreateMyAsm(), получая ссылку на созданный объект AssemblyBuilder.
Далее вы поупражняетесь с поздним связыванием (см. главу 17) для создания экземпляра класса HelloWorld и взаимодействия с его членами. Модифицируйте операторы верхнего уровня, как показано ниже:
using System;
using System.Reflection;
using System.Reflection.Emit;
Console.WriteLine("***** The Amazing Dynamic Assembly Builder App *****");
// Создать объект AssemblyBuilder с использованием вспомогательной функции.
AssemblyBuilder builder = CreateMyAsm();
// Получить тип HelloWorld.
Type hello = builder.GetType("MyAssembly.HelloWorld");
// Создать экземпляр HelloWorld и вызвать корректный конструктор.
Console.Write("-> Enter message to pass HelloWorld class: ");
string msg = Console.ReadLine();
object[] ctorArgs = new object[1];
ctorArgs[0] = msg;
object obj = Activator.CreateInstance(hello, ctorArgs);
// Вызвать метод SayHelloO и отобразить возвращенную строку.
Console.WriteLine("-> Calling SayHello() via late binding.");
MethodInfo mi = hello.GetMethod("SayHello");
mi.Invoke(obj, null);
// Вызвать метод GetMsg().
mi = hello.GetMethod("GetMsg");
Console.WriteLine(mi.Invoke(obj, null));
Фактически только что была построена сборка .NET Core, которая способна создавать и запускать другие сборки .NET Core во время выполнения. На этом исследование языка CIL и роли динамических сборок завершено. Настоящая глава должна была помочь углубить знания системы типов .NET Core, синтаксиса и семантики языка CIL, а также способа обработки кода компилятором C# в процессе его компиляции.
Резюме
В главе был представлен обзор синтаксиса и семантики языка CIL. В отличие от управляемых языков более высокого уровня, таких как С#, в CIL не просто определяется набор ключевых слов, а предоставляются директивы (используемые для определения конструкции сборки и ее типов), атрибуты (дополнительно уточняющие данные директивы) и коды операций (применяемые для реализации членов типов).
Вы ознакомились с несколькими инструментами, связанными с программированием на CIL, и узнали, как изменять содержимое сборки .NET Core за счет добавления новых инструкций CIL, используя возвратное проектирование. Кроме того, вы изучили способы установления текущей (и ссылаемой) сборки, пространств имен, типов и членов. Был рассмотрен простой пример построения библиотеки кода и исполняемого файла .NET Core с применением CIL и соответствующих инструментов командной строки.
Наконец, вы получили начальное представление о процессе создания динамической сборки. Используя пространство имен System.Reflection.Emit, сборку .NET Core можно определять в памяти во время выполнения. Вы видели, что работа с этим пространством имен требует знания семантики кода CIL. Хотя