Pull to refresh

Runtime-генерирование .Net-кода для тех, кому некогда

Reading time8 min
Views35K
Инфраструктура .Net содержит встроенные средства генерирования кода (On-the-Fly Code Generation). Это позволяет .Net-программе в момент своего исполнения самостоятельно (без участия программиста) скомпилировать текст, написанный на каком-либо языке программирования и исполнить получившийся код. Логично было бы ожидать, что для осуществления этих действий в стандартной .Net-библиотеке предусмотрен простейший метод соответствующего класса. Но к сожалению это не так. Microsoft, проделав огромный путь по встраиванию в среду .Net средств генерирования кода, не сделала самый последний шаг навстречу простейшим потребностям программистов. Значит, придётся сделать этот шаг самостоятельно.

Самое простое средство предлагаемое Microsoft для решения описанной задачи — это класс CSharpCodeProvider, который входит в стандартную библиотеку .Net. Использование этого класса — не очень сложная задача, но тем не менее было бы полезно иметь инструмент, превращающий использование сгенерированного на лету кода в задачу тривиальную.


В результате получился небольшой набор классов, центральным из которых является Tech.DynamicCoding.CodeGenerator. Начнем его описание с простейшего примера использования. (Тексты библиотеки и примеров есть в архиве).

Генерирование простейшего кода


Предположим, вам нужно вычислить значение числового выражения, заданного в текстовом виде. Например, вот такого «12345678 * 9 + 9». В этом случае вам достаточно написать следующее:

var result = CodeGenerator.ExecuteCode<int>("return 12345678 * 9 + 9;");

Cначала вы формируете фрагмент C#-кода и передаёте его как параметр вызова метода CodeGenerator.ExecuteCode. Тип возвращаемого кодом значения вы задаёте как параметр-тип метода. Это всё, что вам надо сделать в этом простейшем случае. Так просто? Задача решена?

Генерирование кода с параметрами


На самом деле не всё так просто. Чтобы разглядеть "подводные камни", надо заглянуть "под капот" метода ExecuteCode
. Дело в том, что этот метод формирует исходный код временной сборки, компилирует её, загружает в память текущего процесса, после чего исполняет. Проблема состоит в том, что если вам понадобится вычислить аналогичное выражение с другими числовыми значениями, то вся эта последовательность действий будет проделана заново, хотя код получится идентичный. Дело не только в том, что на это будут потрачено время, но ещё и в том, что в память будет загружена вторая, практически равная первой сборка.

Чтобы преодолеть эту проблему, мы будем использовать в генерируемом коде параметры. В результате выполнения следующего кода в память будет загружена только одна сборка.

    var result = CodeGenerator.ExecuteCode<double>("return a * b + c;", 
        CodeParameter.Create("a", 9876543.21), 
        CodeParameter.Create("b", 9),
        CodeParameter.Create("c", -0.01));
    
    var result2 = CodeGenerator.ExecuteCode<double>("return a * b + c;",
        CodeParameter.Create("a", 12345678.9),
        CodeParameter.Create("b", 8),
        CodeParameter.Create("c", 0.9));

В этом варианте, мы задали формулу отдельно, а значения параметров отдельно. Метод ExecuteCode проверяет, нет ли среди ранее скомпилированных им сборок, подходящей для выполнения текущего вызова. Если исходный C#-код, возвращаемый им тип, а также типы и имена параметров совпадают, то можно использовать приготовленную при первом вызове ExecuteCode сборку повторно.

Повторное использование сгенерированного кода


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

    var code = CodeGenerator.CreateCode<DateTime>(
        "return StartDate.AddDays(Duration);",
        new CodeParameter("StartDate", typeof(DateTime)),
        new CodeParameter("Duration",  typeof(int)));

    var result1 = code.Execute(DateTime.Parse("2013-01-01"), 256);
    var result2 = code.Execute(DateTime.Parse("2013-10-13"), 131);

Этапы приготовления кода и его исполнения здесь разнесены во времени. На первом этапе задается исходный текст фрагмента C#-кода, тип возвращаемого значения, имена и типы параметров. На втором этапе готовый к исполнению код вызывается несколько раз с разными значениями параметров. Использование кода скомпилированного на лету становится в этом случае более эффективным.

Но к сожалению проблемы на этом не заканчиваются. Осталось два очень неприятных момента.

Первый момент. Возможна ситуация, когда вы не знаете точное количество возможных генерируемых фрагментов кода, которые вашей программе предстоит выполнить во время её работы. Проблема в том, что каждый генерируемый фрагмент - это новая сборка в памяти. Это значит, что приложение будет занимать в памяти всё больше и больше места. Поэтому надо научиться выгружать из памяти динамически созданные сборки, чтобы возвращать память.

Вторая проблема - это безопасность. Генерируемый код по своим возможностям совершенно не отличается от кода, который пишет и компилирует программист. При этом источник генерируемого кода может быть не очень надёжен (например, источником исходного кода может быть конечный пользователь). Значит надо иметь возможность регулировать права этого кода, чтобы ошибочный или злонамеренный генерируемый код не разрушил систему.

Вызов сгенерированного кода в песочнице


Для решения обеих проблем, можно использовать дополнительный домен приложения (AppDomain) со строго ограниченными правами исполнения кода - так называемую "песочницу" (sandbox). В следующем фрагменте кода, сгенерированный код создается в такого рода песочнице и исполняется в ней. Затем, песочница завершает свою работу и выгружает из памяти все сборки, работавшие в ней.

    using (var sandbox = new Sandbox())
    {
        var code = CodeGenerator.CreateCode<int>(sandbox, 
            "return (int)(DateTime.Now - StartDate).TotalDays;",
            new CodeParameter("StartDate", typeof(DateTime)));

        var result = code.Execute(DateTime.Parse("1962-09-17"));
    }

Здесь мы используем новый класс Sandbox, олицетворяющий дополнительный AppDomain. В одной песочнице можно многократно исполнять несколько сгенерированных фрагментов кода. Время жизни песочницы регулируется программистом приложения.

Управляем правами сгенерированного кода в песочнице


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

    using (var sandbox = new Sandbox())
    {
        var code = CodeGenerator.CreateCode<int>(sandbox, 
            @"System.IO.File.Delete(filePath);",
            new CodeParameter("filePath", typeof(string)));

        code.Execute(@"c:\temp\a.txt"); // SecurityException
    }

Если это действительно необходимо, то вы можете добавить прав коду в песочнице. Следующий код делает это.

    const string FILE_PATH = @"c:\temp\a.txt";
    using (var sandbox = new Sandbox(
        new FileIOPermission(FileIOPermissionAccess.AllAccess, FILE_PATH)))
    {
        var code = CodeGenerator.CreateCode<int>(sandbox, 
            @"System.IO.File.Delete(filePath);",
            new CodeParameter("filePath", typeof(string)));

        code.Execute(FILE_PATH);
    }


Ограничения на параметры и возвращаемое значение кода, работающего в песочнице


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

Более существенно то, что приходится учитывать ограничения на типы параметров и возвращаемых значений генерируемого кода. Использовать в данном случае можно только сериализуемые типы и типы, производные от MarshalByRefObject. Это ограничение накладывает технология .Net Remoting, используемая для междоменого взаимодействия.

Следующий код демонстрирует возможность управлять из песочницы объектом переданным по ссылке. В качестве объекта будем использовать простейшую модель кондиционера:

    public class AirConditioner : MarshalByRefObject
    {
        public bool Working { get; set; }
    }

    [Serializable]
    public struct Climate
    {
        public double Temperature { get; set; }
    }

Предоставим пользователю возможность самому написать алгоритм, по которому определяется необходимость автоматического включения/выключения кондиционера, в зависимости от температуры в помещении.

    const string controlAlgorithm = @"
        IF Climate.Temperature > 26 THEN Unit.Working = TRUE
        IF Climate.Temperature < 22 THEN Unit.Working = FALSE";

    var unit    = new AirConditioner();
    var сlimate = new Climate { Temperature = 28 };

    using (var sandbox = new Sandbox())
    {
        var controlCode = CodeGenerator.CreateCode<int>(sandbox, 
                VB.Compiler, 
                controlAlgorithm, null, null,
                CodeParameter.Create("Unit", unit),
                CodeParameter.Create("Climate", сlimate));

        while (!Console.KeyAvailable)
        {
            Console.WriteLine("t={0}°C", сlimate.Temperature);

            controlCode.Execute(unit, сlimate);
            Thread.Sleep(300);
            сlimate.Temperature += unit.Working ? -1 : 1;
        } 
    }

Заметьте, что кондиционер бы не заработал, если бы мы не унаследовали его от MarshalByRefObject, а структуру данных Climate не снабдили бы атрибутом [Serializable].

Более сложный сгенерированный код


В заключение можно упомянуть ещё несколько дополнительных возможностей представленной библиотеки.

1. Если в компилируемом во время исполнения С#-коде вам надо использовать не только классы из пространства имён System, то вам придётся указывать полные, длинные имена. При обычном C# программировании эта проблема решается с помощью using'ов. Предусмотрена аналогичная возможность и в нашем случае. В библиотеке есть перегруженные методы, которые принимают на вход список пространств имён, классы из которых вы сможете использовать без указания полных имён. Это позволит сделать ваш динамический код более кратким.

2. Возможно вам понадобится использовать в сгенерированном коде классы не только из библиотек System.dll и mscorlib.dll, но и из других. Чтобы такой динамический код компилировался, придётся указать полный список необходимых библиотек. В библиотеке есть перегруженные методы, которые принимают такой список дополнительных библиотек.

Сборки, в которых определены типы параметров и возвращаемого значения сгенерированного кода, добавляются в список дополнительных библиотек автоматически. Вам не надо об этом заботиться.

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

Заключение


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

Замечу, что потребность в генерируемом во время исполнения коде возникает довольно часто. Эта техника применяется, например, для создания гибких бизнес-приложений, легко адаптируемых к часто изменяющимся бизнес требованиям. Динамический код, написанный бизнес-аналитиком или администратором системы может служить эффективной альтернативой разработке громоздких систем подключаемых модулей (plugin modules).

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



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

Близкие по теме публикации

Динамическая компиляция кода в C#
Динамическая компиляция и загрузка кода
Выполнение C# кода "на лету"
Алгоритмы кодогенерации
CS-Script - The C# Script Engine
Security and On-the-Fly Code Generation
How to: Run Partially Trusted Code in a Sandbox
Metaprogramming in .NET. EBook
Debugging a generated .NET assembly from within the application that generated it
Tags:
Hubs:
+44
Comments29

Articles

Change theme settings