Динамическое .Net программирование для тех, кому некогда

в 19:57, , рубрики: .net, appdomain, metaprogramming, sandbox, Программирование, метки: , , ,

Инфраструктура .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<T>. Тип возвращаемого кодом значения вы задаёте как параметр-тип метода. Это всё, что вам надо сделать в этом простейшем случае. Так просто? Задача решена?

Динамический код с параметрами

На самом деле не всё так просто. Чтобы разглядеть «подводные камни», надо заглянуть «под капот» метода 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:tempa.txt"); // SecurityException
    }

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

    const string FILE_PATH = @"c:tempa.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);
    }

Более сложный динамический код

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

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

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

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

Заключение

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

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

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

Динамическое .Net программирование для тех, кому некогда

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

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

Динамическая компиляция кода в C#
Динамическая компиляция и загрузка кода
Выполнение C# кода «на лету»
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

Автор: avmartynov

Источник

Поделиться

* - обязательные к заполнению поля