Лекция 9:

Верификация CIL-кода. Библиотеки для создания метаинструментов

< Лекция 8 || Лекция 9: 1234 || Лекция 10 >

Reflection API

Библиотека рефлексии содержит классы для работы с метаданными на высоком уровне (эти классы располагаются в пространствах имен System.Reflection и System.Reflection.Emit ). Она соответствует спецификации CLS, поэтому использующий ее метаинструмент может быть написан на любом языке платформы .NET, который является потребителем CLS-библиотек.

Чтение метаданных через рефлексию

Метаинструмент, использующий библиотеку рефлексии, получает доступ к метаданным сборки через экземпляр класса Assembly, который создается при загрузке сборки в память. Затем через методы класса Assembly можно получить объекты класса Module, которые соответствуют модулям, входящим в сборку. Класс Module, в свою очередь, позволяет получить набор экземпляров класса Type для входящих в модуль типов, а уже через эти объекты можно добраться до конструкторов (класс ConstructorInfo ), методов (класс MethodInfo ) и полей (класс FieldInfo ) типов. То есть библиотека рефлексии спроектирована таким образом, что создание объектов рефлексии, соответствующих элементам метаданных, осуществляется путем вызова методов других объектов рефлексии (схема получения доступа к объектам рефлексии показана на рис. 4.5, при этом сплошные стрелки обозначают основные пути получения доступа, а пунктирные - дополнительные пути). Другими словами, конструкторы классов, входящих в библиотеку, для пользователя библиотеки недоступны. Информацию о любом элементе метаданных можно прочитать из свойств соответствующего ему объекта рефлексии.

Получение доступа к объектам рефлексии

Рис. 4.5. Получение доступа к объектам рефлексии

Экземпляр класса Assembly, с создания которого, как правило, начинается работа со сборкой через рефлексию, можно получить разными способами:

  1. Если нас интересует текущая работающая сборка, мы можем вызвать статический метод GetExecutingAssembly класса Assembly:
    Assembly assembly = Assembly.GetExecutingAssembly()
  2. Если мы хотим получить доступ к сборке, в которой объявлен некоторый тип данных, мы можем воспользоваться статическим методом GetAssembly:
    Assembly assembly = Assembly.GetAssembly(typeof(int))
  3. И, наконец, если нам надо загрузить внешнюю сборку, мы используем статический метод LoadFrom:
    Assembly assembly = Assembly.LoadFrom("test.exe")

Сборка .NET, как правило, состоит из одного модуля, хотя существует возможность включения в сборку сразу нескольких модулей. Получение доступа к объектам класса Module, описывающим модули, из которых состоит сборка, осуществляется одним из двух способов:

  1. Если мы знаем имя модуля, то мы можем использовать метод GetModule класса Assembly:
    Module module = assembly.GetModule("SomeModule.exe")
  2. Кроме того, мы можем вызвать метод 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[]);

Кроме этого, доступ к информации о типе можно получить, зная имя этого типа:

  1. путем вызова статического метода GetType класса Type:
    Type t = Type.GetType("System.Char");
  2. через объекты классов 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 представлена схема, показывающая последовательность создания метаданных).

Последовательность создания метаданных

Рис. 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.

Таблица 4.3. Сравнение возможностей библиотек
Metadata Unmanaged API Reflection API
Чтение метаданных + +
Генерация метаданных + +
Чтение CIL-кода - -
Генерация CIL-кода - +

Из таблицы следует, что ни одна из библиотек, поставляемых вместе с .NET Framework, не позволяет читать CIL-код, хотя эта возможность требуется целому ряду метаинструментов (например, верификаторам и оптимизаторам кода).

< Лекция 8 || Лекция 9: 1234 || Лекция 10 >
Анастасия Булинкова
Анастасия Булинкова
Рабочим названием платформы .NET было
Администратор Администратор
Администратор Администратор
Россия, Москва, МГУ, 1986
Анатолий Федоров
Анатолий Федоров
Россия, Москва, Московский государственный университет им. М. В. Ломоносова, 1989