Опубликован: 28.06.2006 | Уровень: специалист | Доступ: платный | ВУЗ: Московский государственный технический университет им. Н.Э. Баумана
Лекция 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 было
Bogdan Drumov
Bogdan Drumov
Молдова, Республика
Azamat Nurmanbetov
Azamat Nurmanbetov
Киргизия, Bishkek