Динамическая компиляция и загрузка кода
Страница 2. Динамическая компиляция исходного кода на C#


 

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

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

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

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

Рассмотрим динамическуюкомпиляцию исходного кода на простом примере. Напишем класс, позволяющийзапускать «макросы на 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 = newCSharpCodeProvider();

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

CompilerParameters parameters = newCompilerParameters(assemblies);

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

string path =Assembly.GetExecutingAssembly().Location;

parameters.ReferencedAssemblies.Add(path);

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

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

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

if( results.Errors.HasErrors )

{

foreach( CompilerError error inresults.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).

 
« Предыдущая статья   Следующая статья »