Исходники.Ру - Программирование
Исходники
Статьи
Книги и учебники
Скрипты
Новости RSS
Магазин программиста

Главная » Статьи по программированию » .NET - Framework »

Обсудить на форуме Обсудить на форуме

Динамическая компиляция и загрузка кода

Прежде чем говорить о динамической компиляции и загрузке кода, нужно ответить на вопрос: зачем нужно динамически выполнять код? Можно привести массу примеров использования, но все, в конечном итоге сводится к одной цели – возможности расширения и изменения функциональности приложения без его перекомпиляции. А теперь посмотрим, чем может помочь .NET Framework в решении этой задачи.

Динамическая загрузка кода

Нередко встречается ситуация когда, в зависимости от тех или иных факторов, нужно загрузить некоторую сборку (assembly) для последующего выполнения содержащегося в ней кода. Пример из жизни – WS-Security ([1]), в котором для проверки пароля пользователя используется динамически загружаемая сборка, идентифицируемая с помощью элемента passwordProvider в файле web.config. В WS-Security используется самый распространенный подход для динамического подключения сборок, который заключается в следующем:

1.     Создается базовый класс или интерфейс для классов в динамически загружаемых сборках, что позволяет впоследствии вызывать нужные методы напрямую, без использования рефлексии.

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

3.     В процессе работы, на основе конфигурационной информации динамически подгружается сборка и создается экземпляр нужного класса, к которому можно обращаться с помощью интерфейсных (или базовых) методов. Если это необходимо, можно вызывать методы при помощи рефлексии.

 

Создать экземпляр класса, находящегося в динамически загружаемой сборке можно несколькими способами. Самый простой путь – вызвать у класса Activator ([2]) метод CreateInstance (или CreateInstanceFrom). При этом будет неявно загружена сборка, содержащая искомый класс (если эта сборка не была загружена ранее). Можно выбрать более сложный путь – загрузить сборку (вызвав один из соответствующих статических методов, например, Assembly.Load), получить нужный тип (вызвав метод Assembly.GetType, с названием класса в качестве параметра), получить у типа информацию о конструкторе (вызвав метод Type.GetConstructor) и, наконец, создать экземпляр класса с помощью метода ConstructorInfo.Invoke. Можно также воспользоваться методами CreateInstance классов Assembly или AppDomain (они вызывают метод Activator.CreateInstance). О том, как загрузить сборку, и какая информация для этого необходима, подробно описано в MSDN ([3],[4]).

Динамическая загрузка доменов приложений (application domains) может понадобиться по двум причинам – для обеспечения большей безопасности (изолированности загруженного кода) и для возможности выгрузки динамически загруженных сборок (в .NET Framework нет возможности выгрузить сборку – только домен приложения). Однако за изолированность доменов приходится платить тем, что вызовы между границами доменов происходят с помощью Remoting. Подробнее о доменах приложений вообще и, в частности, об их изолированности можно прочитать в MSDN ([5]).

В .NET Framework 2.0 появились новые методы, Assembly.ReflectionOnlyLoad и Assembly.ReflectionOnlyLoadFrom, позволяющие загрузить сборку только для получения информации о ней. Вызов этого метода позволяет считывать информацию о сборке, не загружая ее для выполнения. Также добавлен аналогичный метод для типа - Type.ReflectionOnlyGetType. Загруженные таким образом сборки по-прежнему нельзя выгрузить отдельно. Но, помимо более высокой скорости загрузки, эти методы позволяют обойти некоторые запреты, имеющие смысл при выполнении кода, но мешающие получению информации о сборке. К примеру, можно просмотреть сборку, скомпилированную под другой процессор, обойти CAS Policy, не выполнять конструктор модуля. Снятие подобных ограничений будет особенно полезно при написании приложений предназначенных только для исследования сборок, таких как .NET Reflector([15]). Очевидно, что для подобных приложений невозможность получить информацию о 64-битной сборке на машине с 32-битным процессором была бы, по меньшей мере, неприятной.

Динамическая компиляция исходного кода на C#

Наиболее удобной в использовании технологией динамической компиляции является компиляция исходного кода. Как следует из названия, эта технология позволяет динамически компилировать исходный код. Не правда ли, замечательная возможность – писать макросы для своего приложения на C#?

Но есть и плохая новость – создание сборки происходит не в оперативной памяти, поскольку используется компилятор командной строки (csc.exe). И, хотя у класса CompilerParameters есть привлекательное на вид свойство GenerateInMemory, присвоив ему значение “true”, вы лишь получите динамическую сборку в виде временного файла. С другой стороны, мыслить нужно позитивно, поэтому можно сказать следующее – .NET Framework предоставляет очень удобную инфраструктуру для динамической компиляции исходного кода (скрывая вызов компилятора командной строки).

Еще одна проблема, которая может возникнуть при частой динамической компиляции исходного кода в рамках одного домена приложения – нехватка памяти. Не забывайте также, что компилируя дважды одинаковый код, вы получите две сборки. Как уже упоминалось выше, невозможно выгрузить отдельную сборку – можно лишь выгрузить целиком домен приложения. В статьях “Dynamically executing code in .Net” и “AppDomains and Dynamic Loading” ([6], [7]) описано, помимо прочего, решение этой проблемы путем создания новых доменов приложений. Конечно, не обязательно всегда использовать несколько доменов приложения, иногда вполне допустимо использовать один домен приложения.

Рассмотрим динамическую компиляцию исходного кода на простом примере. Напишем класс, позволяющий запускать «макросы на C#», выдающие информацию пользователю в текстовом виде.

Для уменьшения объема исходного кода и профилактики туннельного синдрома запястий опишем пространства имен:

 

using System;

using System.Reflection;

using System.CodeDom.Compiler;

using Microsoft.CSharp;

 

namespace SimpleMacro {

 

Чтобы абстрагироваться от способа вывода сообщений (консоль, журнал событий и т.д.) опишем делегат, единственная функция которого – передавать сообщение:

 

public delegate void SpeakOut(string message);

 

Чтобы не использовать для запуска макроса рефлексию, опишем интерфейс:

 

public interface IMacro

{

      void Run(SpeakOut speakOut);

}

 

Теперь, напишем класс, осуществляющий динамическую компиляцию и запуск макроса. Этот класс будет содержать поле типа IMacro и делегат SpeakOut:

 

public class Macro {

      readonly SpeakOut speakOut;

      readonly IMacro macro;

 

Основной код, компилирующий макрос, поместим в конструктор класса, параметрами которого являются – исходный код, название класса, реализующего интерфейс IMacro, список подключаемых сборок и делегат SpeakOut:

public Macro(string code, string className, string [] assemblies, SpeakOut speakOut)

{

      if( code == null || assemblies == null || speakOut == null )

           throw new ArgumentNullException();

      // запоминаем делегат, для вызова в Run

      this.speakOut = speakOut;

      // создаем экземпляр компилятора

      CSharpCodeProvider codeCompiler = new CSharpCodeProvider();

      // добавляем ссылки на сборки

      CompilerParameters parameters = new CompilerParameters(assemblies);

      // добавляем ссылку на нашу сборку SimpleMacro

      string path = Assembly.GetExecutingAssembly().Location;

      parameters.ReferencedAssemblies.Add(path);

      // компилируем

      CompilerResults results = codeCompiler.CompileAssemblyFromSource(parameters, code);

      // есть ли ошибки?

      if( results.Errors.HasErrors )

      {

           foreach( CompilerError error in results.Errors )

           {

                 speakOut(string.Format("Line:{0:d},Error:{1}\n", error.Line, error.ErrorText));

           }

           throw new ArgumentException("Ошибки при компиляции.");

      }

      // создаем класс

      object objMacro = results.CompiledAssembly.CreateInstance(className);

      if( objMacro == null )

      {

           throw new ArgumentException("Ошибка при создании класса " + className);

      }

      // запоминаем класс как интерфейс

      macro = objMacro as IMacro;

      if( macro == null )

      {

           throw new ArgumentException("Не реализован интерфейс IMacro.");

      }

}

 

Разумеется, нужно написать метод Run, который будет запускать макрос:

 

public void Run()

{

      macro.Run(speakOut);

}

 

Теперь осталось написать клиентский код для этого класса. Чтобы не изобретать велосипед для тестового примера, я написал макрос, выводящий на консоль “Hello, world!”:

 

const string NAMESPACE_NAME = "Test";

const string CLASS_NAME = "TestMacro";

// для нашего случая даже System.dll не нужна

string [] assemblies = new string[0];

// формируем исходный код

string codeString = @"using System;

using SimpleMacro;

namespace "+NAMESPACE_NAME+@"

{

      public class "+CLASS_NAME+@" : IMacro

      {

           public void Run(SpeakOut speakOut)

           {

                 speakOut("+ "\"Hello, world!\"" +@");

           }

      }

}";

// компилируем макрос

Macro macro = new Macro(codeString,

      NAMESPACE_NAME+"."+CLASS_NAME,

      assemblies,

      new SpeakOut(Console.WriteLine) );

// запускаем макрос

macro.Run();

 

Как видно из примера, динамическая компиляция исходного кода не сводится к вызову одного метода с парой параметров. С другой стороны, имеет смысл один раз написать класс, отвечающий за динамическую компиляцию (универсальный или для частного случая) и повторно использовать его в дальнейшем.

Приведенный пример, несмотря на свою простоту, демонстрирует весьма большие возможности, которые открывает для нас .NET Framework. Как их использовать? Вот всего лишь один из вариантов.

Пусть в базе данных хранится список тестов и их исходный код. На основе некоторого запроса к БД клиентское приложение получает определенный набор тестов, которые затем динамически компилируются и запускаются. При желании, исходный код тестов можно модифицировать «на лету»  с помощью того же запроса к БД (или в клиентском приложении).

В .NET Framework 2.0 некоторые методы считаются устаревшими. Поэтому в примере используются методы класса CSharpCodeProvider (а не ICodeCompiler как в .NET Framework 1.1).

Динамическая компиляция средствами Reflection Emit

Пространство имен Reflection.Emit предоставляет набор классов для динамической генерации кода на промежуточном языке (MSIL). Неоспоримым преимуществом этой технологии перед динамической компиляцией исходного кода написанного на языках высокого уровня является скорость компиляции. Это, безусловно, важный фактор, хотя не каждый программист захочет изучать ради этого MSIL.

Интерес представляет также то, что можно использовать конструкции, недопустимые в языках высокого уровня. Например, можно при определении метода задать доступ к нему только для производных классов в той же сборке (MethodAttributes.FamANDAssem), в то время как C# не позволяет задать такой модификатор доступа (“protected internal” соответствует MethodAttributes.FamORAssem).

Как правило, для динамической компиляции используется следующая последовательность действий:

1.     Создается динамическая сборка (экземпляр класса AssemblyBuilder) с помощью метода AppDomain.DefineDynamicAssembly.

2.     Создается модуль (экземпляр класса ModuleBuilder) с помощью метода AssemblyBuilder.DefineDynamicModule.

3.     Создаются элементы модуля (TypeBuilder, EnumBuilder,..) с помощью методов класса ModuleBuilder.

4.     Если нужно, сборка сохраняется на диск, с помощью метода AssemblyBuilder.Save.

5.     Создаются нужные экземпляры классов (получить соответствующий тип можно с помощью TypeBuilder.CreateType) и вызываются  их методы посредством Reflection (если они не были унаследованы от известных вызывающему коду базовых классов и не реализуют известные интерфейсы).

Получить более подробную информацию об использовании Reflection Emit  можно в MSDN ([8], [9]).

В .NET Framework 2.0 появилось много новых вариантов использования Reflection Emit. Перечислю лишь некоторые из них (подробнее в [10], [11]):

·        Генерация шаблонов ([12]).

·        Использование MethodBody для получения содержимого метода ([11],[13])

·        Генерация и выполнение методов без создания динамической сборки ([14]).

 

Остановимся подробнее на генерации динамических методов без создания сборки. Она позволяет создавать метод, глобальный для модуля и, помимо этого, не использовать проверку видимости (JIT compilervisibility checks). Рассмотрим небольшой пример, вычисляющий значение логарифма (разумеется, пример не преследует цель найти самый легкий путь для вычисления логарифма). Нам потребуются следующие пространства имен:

 

using System;

using System.Reflection;

using System.Reflection.Emit;

 

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

 

private delegate double DynamicCalcDelegate(double x, double y);

 

Теперь мы можем написать метод (например, Main), который будет создавать динамический метод и вызывать его:

 

// задаем сигнатуру метода

DynamicMethod dynamicCalc = new DynamicMethod("DynamicCalc",

      typeof(double),

      new Type[] { typeof(double), typeof(double) },

      typeof(Program));

// получаем описание стандартного метода для вычисления логарифма

MethodInfo log = typeof(Math).GetMethod("Log",

      new Type[] { typeof(double), typeof(double) });

// генерируем реализацию метода

ILGenerator il = dynamicCalc.GetILGenerator();

il.Emit(OpCodes.Ldarg_0);

il.Emit(OpCodes.Ldarg_1);

il.EmitCall(OpCodes.Call, log, null);

il.Emit(OpCodes.Ret);

// получаем делегат и вызываем его

DynamicCalcDelegate dc = (DynamicCalcDelegate)dynamicCalc.CreateDelegate( typeof (DynamicCalcDelegate) );

Console.WriteLine(dc(100.0, 10.0));

 

Естественно, наш код, генерирующий реализацию метода очень прост. Простота объясняется небольшим количеством кода и совпадением сигнатур методов Math.Log и DynamicCalc. С другой стороны, пример наглядно демонстрирует, что написание динамических методов сравнительно простая задача, если не принимать во внимание генерацию кода с помощью ILGenerator.


Может пригодится:


Автор: OlegAxenow
Прочитано: 10085
Рейтинг:
Оценить: 1 2 3 4 5

Комментарии: (0)

Добавить комментарий
Ваше имя*:
Ваш email:
URL Вашего сайта:
Ваш комментарий*:
Код безопастности*:

Рассылка новостей
Рейтинги
© 2007, Программирование Исходники.Ру