Рабочим названием платформы .NET было |
Верификация CIL-кода. Библиотеки для создания метаинструментов
Reflection API
Библиотека рефлексии содержит классы для работы с метаданными на высоком уровне (эти классы располагаются в пространствах имен System.Reflection и System.Reflection.Emit ). Она соответствует спецификации CLS, поэтому использующий ее метаинструмент может быть написан на любом языке платформы .NET, который является потребителем CLS-библиотек.
Чтение метаданных через рефлексию
Метаинструмент, использующий библиотеку рефлексии, получает доступ к метаданным сборки через экземпляр класса Assembly, который создается при загрузке сборки в память. Затем через методы класса Assembly можно получить объекты класса Module, которые соответствуют модулям, входящим в сборку. Класс Module, в свою очередь, позволяет получить набор экземпляров класса Type для входящих в модуль типов, а уже через эти объекты можно добраться до конструкторов (класс ConstructorInfo ), методов (класс MethodInfo ) и полей (класс FieldInfo ) типов. То есть библиотека рефлексии спроектирована таким образом, что создание объектов рефлексии, соответствующих элементам метаданных, осуществляется путем вызова методов других объектов рефлексии (схема получения доступа к объектам рефлексии показана на рис. 4.5, при этом сплошные стрелки обозначают основные пути получения доступа, а пунктирные - дополнительные пути). Другими словами, конструкторы классов, входящих в библиотеку, для пользователя библиотеки недоступны. Информацию о любом элементе метаданных можно прочитать из свойств соответствующего ему объекта рефлексии.
Экземпляр класса Assembly, с создания которого, как правило, начинается работа со сборкой через рефлексию, можно получить разными способами:
- Если нас интересует текущая работающая сборка, мы можем вызвать статический метод GetExecutingAssembly класса Assembly:
Assembly assembly = Assembly.GetExecutingAssembly()
- Если мы хотим получить доступ к сборке, в которой объявлен некоторый тип данных, мы можем воспользоваться статическим методом GetAssembly:
Assembly assembly = Assembly.GetAssembly(typeof(int))
- И, наконец, если нам надо загрузить внешнюю сборку, мы используем статический метод LoadFrom:
Assembly assembly = Assembly.LoadFrom("test.exe")
Сборка .NET, как правило, состоит из одного модуля, хотя существует возможность включения в сборку сразу нескольких модулей. Получение доступа к объектам класса Module, описывающим модули, из которых состоит сборка, осуществляется одним из двух способов:
- Если мы знаем имя модуля, то мы можем использовать метод GetModule класса Assembly:
Module module = assembly.GetModule("SomeModule.exe")
- Кроме того, мы можем вызвать метод GetModules для получения массива объектов класса Module, соответствующих всем модулям в сборке:
Module[] modules = assembly.GetModules(false)
Через объект класса Module мы можем обратиться к глобальным полям и функциям модуля (глобальные поля и функции не принадлежат ни одному классу), используя методы GetField и GetFields для полей, а также GetMethod и GetMethods для функций. При этом полям будут соответствовать объекты класса FieldInfo, а методам - объекты класса MethodInfo. В следующем примере мы выводим на экран сигнатуры всех глобальных функций некоторой сборки "test.exe":
Assembly assembly = Assembly.LoadFrom("test.exe"); Module[] modules = assembly.GetModules(false); foreach (Module mod in modules) { MethodInfo[] methods = mod.GetMethods(); foreach (MethodInfo met in methods) Console.WriteLine(met); }
Особое место в библиотеке рефлексии занимает класс Type, представляющий типы. Область применения этого класса значительно шире, чем области применения остальных классов рефлексии, и этот факт отражен в том обстоятельстве, что класс Type входит в пространство имен System, а не System.Reflection.
В каждом классе существует унаследованный от класса System.Object метод GetType, возвращающий экземпляр класса Type, который описывает этот класс. В следующем примере метод GetType будет использован для динамического определения типа объекта o (так как этот объект представляет собой строку, то на печать будет выведено сообщение "Sytem.String"):
object o = new String("qwerty"); Type t = o.GetType(); Console.WriteLine(t);
В C# определен специальный оператор typeof, который возвращает объект класса Type. Например, если нам нужно получить объект Type, представляющий тип массива строк, мы можем записать:
Type t = typeof(string[]);
Кроме этого, доступ к информации о типе можно получить, зная имя этого типа:
- путем вызова статического метода GetType класса Type:
Type t = Type.GetType("System.Char");
- через объекты классов Assembly или Module, которые соответствуют сборке или модулю, содержащему нужный тип:
- Type t = module.GetType("Class1") ;
- Type t = assembly.GetType("Class1").
Мы также можем воспользоваться методами GetTypes классов Assembly и Module для получения массива типов, объявленных в сборке или модуле. В следующем примере на экран выводится список типов, содержащихся в сборке "test.exe":
Assembly assembly = Assembly.LoadFrom("test.exe"); Type[] types = assembly.GetTypes(); foreach (Type t in types) Console.WriteLine(t);
Имея объект рефлексии, соответствующий некоторому типу, мы имеем возможность получить доступ к объектам рефлексии, описывающим его поля, конструкторы, методы, свойства и вложенные типы. При этом, если мы знаем их имена, мы можем воспользоваться методами GetField, GetConstructor, GetMethod, GetProperty и GetNestedType класса Type. А если мы хотим получить массивы объектов рефлексии, описывающих члены типа, то нам нужно вызвать методы GetFields, GetConstructors, GetMethods, GetProperties и GetNestedTypes. В следующем примере на экран выводится список типов, содержащихся в сборке, вместе с сигнатурами их методов:
Assembly assembly = Assembly.LoadFrom("test.exe"); Type[] types = assembly.GetTypes(); foreach (Type t in types) { Console.WriteLine(""+t+":"); MethodInfo[] methods = t.GetMethods(); foreach (MethodInfo m in methods) Console.WriteLine(" "+m); }
Для каждого метода и конструктора мы можем получить доступ к массиву объектов рефлексии, описывающих его параметры. Для этого служит метод GetParameters, объявленный в классах MethodInfo и ConstructorInfo. Данный метод возвращает массив объектов класса ParameterInfo:
ParameterInfo[] parms = method.GetParameters();
Управление объектами
Кроме чтения метаданных, библиотека рефлексии позволяет создавать экземпляры типов, входящих в обрабатываемую сборку, вызывать методы этих типов, читать и изменять значения полей.
Рассмотрим класс TestClass:
class TestClass { private int val; public TestClass() { val = 7; } protected void print() { Console.WriteLine(val); } }
В следующем примере мы используем метод Invoke класса MethodInfo для вызова метода print объекта класса TestClass. Обратите внимание, что метод print объявлен с модификатором доступа protected.
static void CallMethodDemo() { TestClass a = new TestClass(); BindingFlags flags = (BindingFlags) (BindingFlags.NonPublic | BindingFlags.Instance); MethodInfo printer = typeof(TestClass).GetMethod("print",flags); printer.Invoke(a, null); }
Путем внесения небольших изменений в CallMethodDemo мы можем добиться того, что объект класса TestClass будет также создаваться через рефлексию (с помощью вызова его конструктора):
static void CallMethodDemo2() { Type t = typeof(TestClass); BindingFlags flags = (BindingFlags) (BindingFlags.NonPublic | BindingFlags.Instance); ConstructorInfo ctor = t.GetConstructor(new Type[0]); MethodInfo printer = t.GetMethod("print",flags); object a = ctor.Invoke(null); printer.Invoke(a, null); }
А теперь продемонстрируем, как через рефлексию можно получить значение поля объекта. Это достигается путем вызова метода GetField объекта класса Type:
static void GetFieldDemo() { TestClass a = new TestClass(); BindingFlags flags = (BindingFlags) (BindingFlags.NonPublic | BindingFlags.Instance); FieldInfo val = typeof(TestClass).GetField("val",flags); Console.WriteLine(val.GetValue(a)); }
Генерация метаданных и CIL-кода
Для генерации метаданных в библиотеке рефлексии предназначены классы пространства имен System.Reflection.Emit. Создание новой сборки начинается с создания экземпляра класса AssemblyBuilder. Далее путем вызова методов класса AssemblyBuilder создается нужное количество модулей (объектов класса ModuleBuilder ), в модулях создаются типы (объекты класса TypeBuilder ), а в типах - конструкторы, методы и поля (объекты классов ConstructorBuilder, MethodBuilder и FieldBuilder, соответственно). То есть при генерации метаданных идеология такая же, как и при чтении их из готовой сборки (на рис. 4.6 представлена схема, показывающая последовательность создания метаданных).
В отличие от Metadata Unmanaged API, библиотека рефлексии содержит средства для генерации CIL-кода. Для этого предназначен класс ILGenerator. Он позволяет после создания объекта MethodBuilder (или ConstructorBuilder ) сгенерировать для соответствующего метода (или конструктора) поток инструкций.
В следующем примере библиотека рефлексии используется для генерации сборки .NET, состоящей из одного глобального метода main, выводящего на экран сообщение "Hello, World!":
using System; using System.Threading; using System.Reflection; using System.Reflection.Emit; class HelloGenerator { static void Main(string[] args) { AppDomain appDomain = Thread.GetDomain(); AssemblyName assemblyName = new AssemblyName(); assemblyName.Name = "hello.exe"; AssemblyBuilder assembly = appDomain.DefineDynamicAssembly( assemblyName, AssemblyBuilderAccess.RunAndSave ); ModuleBuilder module = assembly.DefineDynamicModule( "hello.exe", "hello.exe" ); MethodBuilder mainMethod = module.DefineGlobalMethod( "main", MethodAttributes.Static | MethodAttributes.Public, typeof(void), null ); MethodInfo ConsoleWriteLineMethod = ((typeof(Console)).GetMethod("WriteLine", new Type[] { typeof(string) } )); ILGenerator il = mainMethod.GetILGenerator(); il.Emit(OpCodes.Ldstr,"Hello, World!"); il.Emit(OpCodes.Call,ConsoleWriteLineMethod); il.Emit(OpCodes.Ret); module.CreateGlobalFunctions(); assembly.SetEntryPoint( mainMethod,PEFileKinds.ConsoleApplication ); assembly.Save("hello.exe"); } }
Сравнение возможностей библиотек
Возможности, предоставляемые библиотекой Metadata Unmanaged API и библиотекой рефлексии, представлены в таблице 4.3.
Metadata Unmanaged API | Reflection API | |
---|---|---|
Чтение метаданных | + | + |
Генерация метаданных | + | + |
Чтение CIL-кода | - | - |
Генерация CIL-кода | - | + |
Из таблицы следует, что ни одна из библиотек, поставляемых вместе с .NET Framework, не позволяет читать CIL-код, хотя эта возможность требуется целому ряду метаинструментов (например, верификаторам и оптимизаторам кода).