Сообщений 8    Оценка 1092 [+2/-0]         Оценить  
Система Orphus

Алгоритмы кодогенерации

Кодогенерация при программировании с использованием платформы Microsoft.NET

Автор: Андрей Корявченко
The RSDN Group

Источник: RSDN Magazine #4-2003
Опубликовано: 19.03.2004
Исправлено: 10.12.2016
Версия текста: 1.0
Введение
Кодогенерация «в лоб»
Кодогенерация с использованием шаблонов
Кодогенерация с использованием шаблонов XSLT
Кодогенерация с использованием ASP.NET
Кодогенерация на нескольких языках
Кодогенерация на нескольких языках с использованием шаблонов CodeDOM
Кодогенерация во время выполнения
Кодогенерация при помощи компиляции из исходного кода
Кодогенерация сразу в IL-код
Заключение

Код к статье

Введение

Не секрет, что иногда при программировании какой-то задачи приходится выполнять большой объем нудной рутинной работы, и избавиться от нее штатными средствами ООП не удается. Кроме того, ситуация усугубляется и отсутствием в языках .NET generic типов.

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

ПРИМЕЧАНИЕ

В .NET Framework кодогенерация также применяется довольно часто. Например, при помощи кодогенератора на основе xsd-файлов со схемой данных создаются типизированные наборы данных (typed datasets), при помощи кодогенератора создаются прокси-классы для веб-сервисов. Во время выполнения генерируют код для повышения производительности классы System.Xml.Serialization.XmlSerializer, System.Text.RegularExpressions.Regex, System.Web.UI.PageParser.

Статья сугубо практическая и содержит по большей части примеры построения кодогенераторов.

Кодогенерация «в лоб»

Первым, что обычно приходит на ум – это генерировать код на каком-либо языке программирования «напрямую». Предположим, необходимо по некоему описанию набора свойств сгенерировать класс. Зададим описание в виде xml-файла:

<class-descriptor namespace="Rsdn">
  <classes>
    <class name="Class1">
      <properties>
        <property name="IntField" type="System.Int32"/>
        <property name="StrField" type="System.String"/>
      </properties>
    </class>
    <class name="Class2">
      <properties>
        <property name="IntField" type="System.Int32"/>
        <property name="DateField" type="System.DateTime"/>
      </properties>
    </class>
    <class name="Class3">
      <properties>
        <property name="IntField" type="System.Int32"/>
        <property name="DoubleField" type="System.Double"/>
      </properties>
    </class>
  </classes>
</class-descriptor>

Это описание будет находиться в одной из сборок проекта в качестве ресурса.

Для представления структуры классов внутри программы создадим набор классов, с которыми будет работать алгоритм кодогенерации. Чтобы вручную не создавать объектное представление из xml, воспользуемся классом XmlSerializer.

ПРИМЕЧАНИЕ

Класс XmlSerializer представляет собой сериализатор объектов в xml. При этом, в отличие от формата, выдаваемого SoapFormatter, в данном случае формат xml поддается более тщательному контролю. В частности, в приведенном ниже примере атрибутом XmlRoot задается название корневого тега, атрибутом XmlAttribute задается сохранение свойства в значении атрибута с указанным именем, атрибутами XmlArray и XmlArrayItem способ представления массивов и списков.

      using System.Collections;
using System.IO;
using System.Reflection;
using System.Xml.Serialization;

namespace Rsdn
{
  /// <summary>/// Описание данных для генерации/// </summary>
  [XmlRoot("class-descriptor")]
  publicclass ClassDescriptor
  {
    privatestring _namespace;
    private ArrayList _classes = new ArrayList();

    /// <summary>/// Namespace/// </summary>
    [XmlAttribute("namespace")]
    publicstring Namespace
    {
      get { return _namespace; }
      set { _namespace = value; }
    }

    /// <summary>/// Коллекция классов/// </summary>
    [XmlArray("classes")]
    [XmlArrayItem("class", typeof (ClassData))]
    public IList Classes
    {
      get { return _classes; }
    }

    /// <summary>/// Загрузка дерева объектов, описывающих генерируемые классы,/// из xml/// </summary>publicstatic ClassDescriptor Load()
    {
      // загружаем xml из ресурса
      Stream resStream = typeof (ClassDescriptor).Assembly.
        GetManifestResourceStream("Rsdn.descript.xml");
      // преобразуем xml в дерево экземпляров классов
      XmlSerializer xs = new XmlSerializer(typeof (ClassDescriptor));
      return (ClassDescriptor)xs.Deserialize(resStream);
    }
  }
}

using System;
using System.Collections;
using System.Xml.Serialization;

namespace Rsdn
{
  /// <summary>/// Описание данных класса./// </summary>publicclass ClassData
  {
    privatestring _name;
    private ArrayList _properties = new ArrayList();

    /// <summary>/// Имя класса/// </summary>
    [XmlAttribute("name")]
    publicstring Name
    {
      get { return _name; }
      set { _name = value; }
    }

    /// <summary>/// Свойства класса/// </summary>
    [XmlArray("properties")]
    [XmlArrayItem("property", typeof (PropertyData))]
    public IList Properties
    {
      get { return _properties; }
    }
  }
}

using System;
using System.Xml.Serialization;

namespace Rsdn
{
  /// <summary>/// Описание данных свойства класса./// </summary>publicclass PropertyData
  {
    privatestring _name;
    privatestring _type;

    /// <summary>/// Имя свойства/// </summary>
    [XmlAttribute("name")]
    publicstring Name
    {
      get { return _name; }
      set { _name = value; }
    }

    /// <summary>/// Тип свойства/// </summary>
    [XmlAttribute("type")]
    publicstring Type
    {
      get { return _type; }
      set { _type = value; }
    }
  }
}

Теперь собственно кодогенератор. Загружаем исходные данные, потом в StringBuilder создаем исходный код на C#.

      using System;
using System.Text;
using System.Xml.Serialization;

namespace Rsdn
{
  /// <summary>/// Пример 1 к статье./// </summary>publicclass Sample1
  {
    privatestaticstring Generate(ClassDescriptor clsDesc)
    {
      StringBuilder sb = new StringBuilder();
      sb.Append("namespace " + clsDesc.Namespace + "\n");
      sb.Append("{\n");

      foreach (ClassData clsData in clsDesc.Classes)
      {
        sb.Append("\tpublic class " + clsData.Name + "\n");
        sb.Append("\t{\n");

        StringBuilder fields = new StringBuilder();
        StringBuilder props = new StringBuilder();
        foreach(PropertyData propData in clsData.Properties)
        {
          fields.Append("\t\tprivate " + propData.Type + " _" +
                                         propData.Name + ";\n");
          props.Append("\t\tpublic " + propData.Type + " " +
                                       propData.Name + "\n");
          props.Append("\t\t{\n");
          props.Append("\t\t\tget\n");
          props.Append("\t\t\t{\n");
          props.Append("\t\t\t\treturn _" +propData.Name + ";\n");
          props.Append("\t\t\t}\n");
          props.Append("\t\t\tset\n");
          props.Append("\t\t\t{\n");
          props.Append("\t\t\t\t_" + propData.Name + " = value;\n");
          props.Append("\t\t\t}\n");
          props.Append("\t\t}\n");
          props.Append("\n");
        }

        sb.Append(fields.ToString() + "\n" + props.ToString());

        sb.Append("\t}\n\n");
      }

      sb.Append("}\n");

      return sb.ToString();
    }

    staticvoid Main()
    {
      Console.WriteLine("Пример 1 к статье.");
      
      Console.WriteLine(Generate(ClassDescriptor.Load ()));
    }
  }
}

Ниже приведен сгенерированный результат:

      namespace Rsdn
{
  publicclass Class1
  {
    private System.Int32 _IntField;
    private System.String _StrField;

    public System.Int32 IntField
    {
      get
      {
        return _IntField;
      }
      set
      {
        _IntField = value;
      }
    }

    public System.String StrField
    {
      get
      {
        return _StrField;
      }
      set
      {
        _StrField = value;
      }
    }

  }

  publicclass Class2
  {
    private System.Int32 _IntField;
    private System.DateTime _DateField;

    public System.Int32 IntField
    {
      get
      {
        return _IntField;
      }
      set
      {
        _IntField = value;
      }
    }

    public System.DateTime DateField
    {
      get
      {
        return _DateField;
      }
      set
      {
        _DateField = value;
      }
    }

  }

  publicclass Class3
  {
    private System.Int32 _IntField;
    private System.Double _DoubleField;

    public System.Int32 IntField
    {
      get
      {
        return _IntField;
      }
      set
      {
        _IntField = value;
      }
    }

    public System.Double DoubleField
    {
      get
      {
        return _DoubleField;
      }
      set
      {
        _DoubleField = value;
      }
    }

  }

}

Однако, глядя на код, можно заметить, что он довольно неудобочитаемый. Как обычно, самое простое (на первый взгляд) решение оказалось далеко не оптимальным. Если присмотреться повнимательнее, то можно увидеть что большой объем кода занимается генерацией статического, то есть неизменяемого кода.

Кодогенерация с использованием шаблонов

Воспользуемся стандартной функцией Format() класса string для удаления из кода статических участков. Текст шаблонов для этой функции будет таким:

Classes.txt

      namespace {0}
{{
{1}
}}

Class.txt

      public
      class {0}
  {{
{1}

{2}
  }}

Field.txt

      private {0} _{1};

Property.txt

      public {0} {1}
    {{
      get
      {{
        return _{1};
      }}
      set
      {{
        _{1} = value;
      }}
    }}

Эти шаблоны также будут внедрены в сборку как ресурсы.

Новая функция генерации теперь будет такой:

      private
      static
      string Generate(ClassDescriptor clsDesc)
{
  string clssTemplate = ReadTemplate("Classes");
  string clsTemplate = ReadTemplate("Class");
  string fldTemplate = ReadTemplate("Field");
  string propTemplate = ReadTemplate("Property");

  StringBuilder classes = new StringBuilder();
  foreach (ClassData clsData in clsDesc.Classes)
  {
    StringBuilder flds = new StringBuilder();
    StringBuilder props = new StringBuilder();
    foreach (PropertyData propData in clsData.Properties)
    {
      flds.Append(string.Format(fldTemplate,propData.Type,propData.Name)+
                  "\n");
      props.Append(string.Format(propTemplate,propData.Type,propData.Name)+
                   "\n");
    }
    classes.Append(string.Format(clsTemplate,clsData.Name,flds,props)+
                   "\n");
  }
  returnstring.Format(clssTemplate, clsDesc.Namespace, classes);
}

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

Кодогенерация с использованием шаблонов XSLT

Одним из готовых решений для создания шаблонов является часть стандарта W3C XSL – XSL Transformation, сокращенно XSLT. Эта технология предназначена для преобразования xml-файлов в другое представление и обладает очень большой гибкостью.

ПРИМЕЧАНИЕ

XSLT представляет собой язык программирования, оптимизированный для целей задания правил преобразования. XSLT-файл состоит из набора правил, обозначаемых тегами template. В отличие от классических языков, XSLT описывает преобразование не как алгоритм действий, а как набор правил, применяемых к узлам входного xml. Каждое правило содержит логическую функцию, предикат, вычисляя которую можно определить, подходит ли оно для текущего узла. В случае XSLT для описания подобных функций применяется специальный язык, XPath. Если предикат возвращает true, выполняется содержимое правила, которое состоит из статического текста и набора тегов, напоминающих синтаксические конструкции логических языков – печать значения, цикл, условие и т. д. Кроме того, тегом apply-templates задается продолжение обработки для вложенных узлов. Обработка файла ведется, начиная с корневого узла.

СОВЕТ

Полное описание XSLT вы можете найти на сайте World Wide Web Consortium, www.w3c.org

В качестве источника данных для преобразования возьмем xml-файл из первого примера. Теперь напишем XSLT-шаблон.

<?xmlversion="1.0"encoding="UTF-8" ?>
<stylesheet version="1.0" xmlns="http://www.w3.org/1999/XSL/Transform">
  <output method="text"/>
  
  <template match="/class-descriptor">
namespace <value-of select="@namespace"/>
{
    <apply-templates select="classes"/>
}
  </template>
  
  <template match="class">
  public class <value-of select="@name"/>
  {
    <apply-templates select="properties"/>
  }
  </template>
  
  <template match="property">
    private <value-of select="@type"/> _<value-of select="@name"/>;

    public <value-of select="@type"/>&#32;<value-of select="@name"/>
    {
      get
      {
        return _<value-of select="@name"/>;
      }
      set
      {
        _<value-of select="@name"/> = value;
      }
    }
  </template>
</stylesheet>

Для первого шаблона указан предикат, выполняющийся для корневого тега входного файла. В его теле выполняется генерация конструкции namespace и продолжение обработки вложенных в classes тегов. Следующий шаблон срабатывает при обработке узла class. Он генерирует объявление класса и начинает обработку свойств. Наконец последний шаблон генерирует поле и свойство класса, завершая обработку текущего узла.

А сейчас напишем собственно функцию генерации. Для выполнения XSLT-преобразования в .NET Framework существует специальный класс, XSLTTransform. Им мы и воспользуемся:

      private
      static
      string Generate()
{
  // загружаем шаблон из ресурсов
  Stream templateData = typeof (Sample3).Assembly.GetManifestResourceStream(
    "Rsdn.Template.xslt");
  // инициализируем преобразователь
  XslTransform transform = new XslTransform();
  transform.Load(new XmlTextReader(templateData));

  // загружаем из ресурсов исходные данные
  Stream srcData=typeof(ClassDescriptor).Assembly.GetManifestResourceStream(
                           "Rsdn.descript.xml");
  XPathDocument xpd = new XPathDocument(srcData);
  // Выполняем преобразование
  XmlReader rdr = transform.Transform(xpd, null);
  rdr.Read();

  return rdr.Value;
}

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

Кодогенерация с использованием ASP.NET

В .NET Framework есть еще одно средство создания данных по шаблону. Это средство – ASP.NET. В отличие от XSLT, шаблоны ASP.NET не используют никаких дополнительных языков. Кроме того, Visual Studio .NET предоставляет средства для отладки таких шаблонов. Попробуем применить эту технологию для кодогенерации.

Для отладки шаблонов можно и нужно использовать IIS, однако для практического использования это не очень удобно, поскольку требует инсталляции продукта. Однако попытка использовать классы ASP.NET напрямую приводит к исключению, так как эти классы требуют определенной среды, а точнее – специальным образом настроенного домена, в свойствах которого будет указан ряд данных, необходимых для функционирования ASP.NET. Поэтому придется создавать подобную среду для ASP.NET самостоятельно.

Для хостинга ASP.NET необходимо создать отдельный домен и добавить в его данные определенный набор переменных. Для создания такого домена в .NET Framework существует класс System.Web.Hosting.ApplicationHost. Этот класс создает домен, а в нем объект переданного типа.

      using System;
using System.IO;
using System.Web;
using System.Web.Hosting;

// передаем объект по ссылке, чтобы можно было управлять им из другого доменаpublicclass Host : MarshalByRefObject
{
  publicvoid ProcessRequest(String page)
  {
    // Создаем HTTP-запрос к ASP.NET-странице
    HttpWorkerRequest hwr = 
      new SimpleWorkerRequest(page, null, Console.Out);
    // Выполняем его обработку
    HttpRuntime.ProcessRequest(hwr);
  }
}

publicclass Host
{
  publicstaticvoid Main()
  {
    MyExeHost host = (Host)ApplicationHost.CreateApplicationHost(
typeof(MyExeHost), "/", Directory.GetCurrentDirectory());
    host.ProcessRequest("...");
  }
}

Однако есть одна неприятность – директория для поиска сборок принудительно устанавливается в bin. Поэтому при запуске этого примера мы получим исключение «Сборка не найдена».

К сожалению, имя этой директории никак не настраивается и просто вшито в код в виде обычной строковой константы. Поэтому нам придётся отказаться от этого класса и инициализировать домен самостоятельно. На практике достаточно при помощи метода домена SetData() определить ряд переменных с "магическими" именами.

      private
      static AppDomain GetDomainForGenerator()
{
  AppDomainSetup setup = new AppDomainSetup();
  setup.ApplicationName = "ASP.NET Generator";

  AppDomain dom = AppDomain.CreateDomain("generator domain" , null, setup);

  dom.SetData(".appPath", Directory.GetCurrentDirectory() + 
    Path.DirectorySeparatorChar);
  dom.SetData(".appDomain", "*");
  dom.SetData(".appVPath", "/");
  dom.SetData(".domainId", dom.FriendlyName);
  dom.SetData(".hostingVirtualPath", "/");
  dom.SetData(".hostingInstallDir", HttpRuntime.AspInstallDirectory);

  return dom;
}

Далее создадим класс, который непосредственно будет осуществлять запрос. Запрашиваем ASP.NET, полученный результат возвращаем.

      using System;
using System.IO;
using System.Web;
using System.Web.Hosting;

namespace Rsdn
{
  /// <summary>/// Хост для ASP.NET кодогенератора./// </summary>publicclass CodeGenHost : MarshalByRefObject
  {
    publicstring Generate(string template)
    {
      StringWriter sw = new StringWriter();
      HttpWorkerRequest hwr = new SimpleWorkerRequest(template, null, sw);
      HttpRuntime.ProcessRequest(hwr);
      return sw.ToString();
    }
  }
}

Метод Generate() получает на вход имя файла странички и выдает на выход полученную строку.

Теперь создадим метод, осуществляющий генерацию. Для передачи ASP.NET-страничке исходных данных воспользуемся тем же механизмом переменных домена приложений.

      private
      static
      string Generate()
{
  AppDomain dom = GetDomainForGenerator();
  CodeGenHost host = (CodeGenHost)dom.CreateInstanceAndUnwrap(
    Assembly.GetExecutingAssembly().FullName, typeof (CodeGenHost).FullName);

  dom.SetData("ClassDescriptor", ClassDescriptor.Load());

  return host.Generate("Template.aspx");
}

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

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

<configuration>
  <system.web>
    <compilation>
      <assemblies>
        <add assembly="SourceData"/>
      </assemblies>
    </compilation>
  </system.web>
</configuration>

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

<%@Page language="c#"%>
<%@Import namespace="Rsdn"%>
<%ClassDescriptor clsDesc = (ClassDescriptor)AppDomain.CurrentDomain.GetData(
  "ClassDescriptor");%>
namespace <%=clsDesc.Namespace%>
{
<%foreach (ClassData clsData in clsDesc.Classes) {%>
  publicclass <%=clsData.Name%>
  {
  <%foreach (PropertyData propData in clsData.Properties) {%>
    private <%=propData.Type%> _<%=propData.Name%>;
    
    public <%=propData.Type%> <%=propData.Name%>
    {
      get
      {
        return _<%=propData.Name%>;
      }
      set
      {
        _<%=propData.Name%> = value;
      }
    }
    
  <%}%>
  }
<%}%>
}

Все, кодогенератор готов.

Кодогенерация на нескольких языках

Платформа Microsoft.NET поддерживает программирование на нескольких языках. Если важно не просто получить конечный результат, но получать исходный код на разныз языках программирования, также весьма желательно наличие поддержки нескольких языков. Однако приведенные ранее примеры кодогенераторов для добавления поддержки языка нуждаются либо в полном переписывании, либо, по крайней мере, в полной переделке шаблонов. Для решения этой проблемы Microsoft предлагает технологию CodeDOM.

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

ПРИМЕЧАНИЕ

Классы, представляющие разные конструкции языка, описаны в пространстве имен System.CodeDom. На каждую такую конструкцию в этом пространстве имен присутствует соответствующий класс. Например, объявление метода описывает класс CodeMemberMethod, использование поля CodeFieldReferenceExpression.

Попробуем реализовать кодогенератор из самого первого примера с использованием CodeDOM.

      private
      static
      string _mode = "VB";

privatestaticstring Generate(ClassDescriptor clsDesc)
{
  // Создаем пространство имен
  CodeNamespace ns = new CodeNamespace(clsDesc.Namespace);
  foreach (ClassData clsData in clsDesc.Classes)
  {
    // Создаем объявление класса
    CodeTypeDeclaration ctd = new CodeTypeDeclaration(clsData.Name);
    ns.Types.Add(ctd);
    foreach(PropertyData propData in clsData.Properties)
    {
      string fldName = "_" + propData.Name;
      // Создаем объявление поля
      CodeMemberField cmf = new CodeMemberField(propData.Type, fldName);
      ctd.Members.Add(cmf);

      // Создаем объявление свойства
      CodeMemberProperty cmp = new CodeMemberProperty();
      ctd.Members.Add(cmp);
      cmp.Name = propData.Name;
      // Внутри get части создаем выражение возврата из функции// и добавляем в него ссылку на поле
      CodeMethodReturnStatement cmr = new CodeMethodReturnStatement(
        new CodeFieldReferenceExpression(null, fldName));
      cmp.GetStatements.Add(cmr);
      CodeAssignStatement cas = new CodeAssignStatement(
        new CodeFieldReferenceExpression(null, fldName), 
        new CodePropertySetValueReferenceExpression());
      cmp.SetStatements.Add(cas);
    }
  }

  // Генерируем исходный код
  CodeDomProvider provider;
  if (_mode == "C#")
    provider = new CSharpCodeProvider();
  else
    provider = new VBCodeProvider();

  ICodeGenerator gen = provider.CreateGenerator();
  StringWriter sw = new StringWriter();
  // Включаем С-подобные отступы, для того чтобы код на C# выглядел нормально
  CodeGeneratorOptions opts = new CodeGeneratorOptions();
  opts.BracingStyle = "C";
  gen.GenerateCodeFromNamespace(ns, sw, opts);

  return sw.ToString();
}

Кодогенератор для Visual Basic.NET выдаст такой код:

      Namespace Rsdn
  
  PublicClass Class1
    
    Private _IntField AsIntegerPrivate _StrField AsStringPrivateProperty IntField As System.Void
      GetReturn _IntField
      EndGetSet
        _IntField = value
      EndSetEndPropertyPrivateProperty StrField As System.Void
      GetReturn _StrField
      EndGetSet
        _StrField = value
      EndSetEndPropertyEndClassPublicClass Class2
    
    Private _IntField AsIntegerPrivate _DateField AsDatePrivateProperty IntField As System.Void
      GetReturn _IntField
      EndGetSet
        _IntField = value
      EndSetEndPropertyPrivateProperty DateField As System.Void
      GetReturn _DateField
      EndGetSet
        _DateField = value
      EndSetEndPropertyEndClassPublicClass Class3
    
    Private _IntField AsIntegerPrivate _DoubleField AsDoublePrivateProperty IntField As System.Void
      GetReturn _IntField
      EndGetSet
        _IntField = value
      EndSetEndPropertyPrivateProperty DoubleField As System.Void
      GetReturn _DoubleField
      EndGetSet
        _DoubleField = value
      EndSetEndPropertyEndClassEndNamespace

Таким образом, с использованием CodeDOM вы можете написать генератор для тех языков, о которых даже не слышали. Необходимо только, чтобы для этого языка существовал CodeDomProvider. На настоящий момент в составе .NET Framework с такими провайдерами поставляются языки C#, VB, J#, JScript. В составе Visual Studio.NET 2003 поставляется также провайдер для МС++.

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

Кодогенерация на нескольких языках с использованием шаблонов CodeDOM

Понятно, что в этом случае шаблон для генерации должен быть представлен в виде CodeDOM-дерева. Вопрос состоит только в том, как его получить. Можно, конечно, разбирать исходный код на том же C#, однако парсинг таких языков – дело непростое, да и не все конструкции С# можно выразить в CodeDOM.

Поэтому в качестве языка описания CodeDOM-шаблонов используем язык с готовым парсером – XML.

К сожалению, в рамках этой статьи невозможно реализовать парсер XML-описания CodeDOM с учетом всех его возможностей, поэтому реализуем только ту часть, которая необходима для реализации примера. Для реализации воспользуемся тем же способом, которым мы загружали исходные данные, то есть XmlSerializer. Для классов, представляющих элементы шаблона определим абстрактный базовый класс:

      using System;
using System.CodeDom;

namespace Rsdn
{
  /// <summary>/// Базовый класс для объектов дерева./// </summary>publicabstractclass XmlCodeObject
  {
    /// <summary>/// Создать соответствующий объект CodeDOM/// </summary>/// <returns>объект CodeDOM</returns>publicabstract CodeObject CreateCodeObject();
  }
}

Объявим в нем виртуальный метод, который будет возвращать соответствующий элементу шаблона элемент CodeDom. Полный исходный код классов для загрузки из xml находится на прилагаемом к журналу компакт-диске.

Теперь реализуем шаблон. Поскольку и входные, и выходные данные выражены в виде XML, то воспользуемся XSLT-преобразованием.

<?xmlversion="1.0"encoding="UTF-8" ?>
<xsl:stylesheetversion="1.0"xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:outputindent="yes"method="xml"/>  
  
  <xsl:templatematch="/class-descriptor">
    <namespace> 
      <xsl:attributename="name">
        <xsl:value-ofselect="@namespace"/>
      </xsl:attribute>
      <types>
        <xsl:apply-templatesselect="classes"/>
      </types>
    </namespace>
  </xsl:template>
  
  <xsl:templatematch="class">
    <type-declaration>
      <xsl:attributename="name">
        <xsl:value-ofselect="@name"/>
      </xsl:attribute>
      <members>
        <xsl:apply-templatesselect="properties"/>
      </members>
    </type-declaration>
  </xsl:template>
  
  <xsl:templatematch="property">
    <member-field>
      <xsl:attributename="name">_<xsl:value-ofselect="@name"/></xsl:attribute>
      <xsl:attributename="type">
        <xsl:value-ofselect="@type"/>
      </xsl:attribute>
    </member-field>
    
    <member-property>
      <xsl:attributename="name">
        <xsl:value-ofselect="@name"/>
      </xsl:attribute>
      <xsl:attributename="type">
        <xsl:value-ofselect="@type"/>
      </xsl:attribute>
      <xsl:attributename="attributes">Public</xsl:attribute>
      <getter>
        <method-return>
          <field-reference>
            <xsl:attributename="name">_<xsl:value-ofselect="@name"/></xsl:attribute>
          </field-reference>
        </method-return>
      </getter>
      <setter>
        <assign>
          <field-reference>
            <xsl:attributename="name">_<xsl:value-ofselect="@name"/></xsl:attribute>
          </field-reference>
          <property-set-value/>
        </assign>
      </setter>
    </member-property>
    
  </xsl:template>
</xsl:stylesheet>

Ну, и, наконец, код генератора, представляющий по сути комбинацию предыдущего примера и примера с XSLT-шаблоном.

      private
      static
      string Generate()
{
  Stream templateData = typeof (Sample6).Assembly.GetManifestResourceStream(
    "Rsdn.Template.xslt");
  XslTransform transform = new XslTransform();
  transform.Load(new XmlTextReader(templateData));

  Stream srcData = typeof (ClassDescriptor).Assembly.GetManifestResourceStream(
    "Rsdn.descript.xml");
  XPathDocument xpd = new XPathDocument(srcData);
  StringWriter sw = new StringWriter();
  transform.Transform(xpd, null, sw);

  XmlSerializer xs = new XmlSerializer(typeof (XmlNamespace));

  XmlNamespace ns = (XmlNamespace)xs.Deserialize(new StringReader(sw.ToString()));

  // Генерируем исходный код
  CodeDomProvider provider;
  if (_mode == "C#")
    provider = new CSharpCodeProvider();
  else
    provider = new VBCodeProvider();

  ICodeGenerator gen = provider.CreateGenerator();
  StringWriter sw2 = new StringWriter();
  // Включаем С-подобные отступы, для того чтобы код на C# выглядел нормально
  CodeGeneratorOptions opts = new CodeGeneratorOptions();
  opts.BracingStyle = "C";
  gen.GenerateCodeFromNamespace((CodeNamespace)ns.CreateCodeObject(), sw2, opts);

  return sw2.ToString();
}

Кодогенерация во время выполнения

Платформа Microsoft.NET позволяет создавать код не только при разработке, но и во время выполнения программы. Реализации подобного подхода и посвящена эта глава.

Кодогенерация при помощи компиляции из исходного кода

Для обеспечения компиляции во время выполнения CodeDOM-провайдер для конкретного языка реализует специальный интерфейс System.CodeDom.Compiler.ICodeCompiler. У этого класса есть несколько методов, осуществляющих компиляцию (Таблица 1).

Метод Выполняемое действие
CompileAssemblyFromSource Компиляция из исходного кода, представленного в виде строки.
CompileAssemblyFromDom Компиляция из CodeDOM дерева.
CompileAssemblyFromFile Компиляция из файла.

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

Для настройки компиляции в эти методы передаются настройки компилятора, представленные классом System.CodeDom.Compiler.CompilerParameters. Вот основные свойства этого класса:

Свойство Значение
GenerateExecutable Генерировать запускаемый файл или библиотеку
MainClass Имя класса, содержащего метод Main()
OutputAssembly Имя результирующей сборки
IncludeDebugInformation Определяется, нужно ли включать отладочную информацию
ReferencedAssemblies Коллекция, в которую нужно добавить все сборки, на которые производятся ссылки
GenerateInMemory Генерировать в память. Опция, позволяющая не создавать при компиляции файл на диске. Очень полезная для наших целей опция.
WarningLevel Уровень выдаваемых компилятором предупреждений
TreatWarningsAsErrors Воспринимать предупреждения как ошибки. Запрещает компилятору завершать компиляцию при наличии предупреждений.
CompilerOptions Дополнительные настройки, специфичные для конкретного компилятора. Например, опция /optimize для компилятора C#
TempFiles В этой коллекции сохраняется список временных файлов, образованных при компиляции и не удаленных.

Попробуем скомпилировать сборку, полученную в примере с XSLT-шаблоном. Генерацию оставим такой, как была и добавим компиляцию сгенерированного кода.

...

privatestatic Assembly Compile(string src)
{
  CompilerParameters cp = new CompilerParameters();
  cp.GenerateInMemory = true;

  ICodeCompiler cc = new CSharpCodeProvider().CreateCompiler();
  CompilerResults crs = cc.CompileAssemblyFromSource(cp, src);
  return crs.CompiledAssembly;
}

staticvoid Main(string[] args)
{
  Console.WriteLine("Пример 7.");
  Console.WriteLine();

  Console.WriteLine("Классы сборки");
  Assembly asm = Compile(Generate());
  foreach (Type t in asm.GetTypes())
  {
    Console.WriteLine(t.FullName);
    foreach (PropertyInfo pi in t.GetProperties())
      Console.WriteLine("\t{0} {1}", pi.PropertyType.FullName, pi.Name);
  }

  Console.Read();
}

...

Результат выполнения приведён ниже.

Пример 7.

Классы сборки
Rsdn.Class1
  System.Int32 IntField
  System.String StrField
Rsdn.Class2
  System.Int32 IntField
  System.DateTime DateField
Rsdn.Class3
  System.Int32 IntField
  System.Double DoubleField

Используя подобную технику, можно реализовывать очень эффективные алгоритмы, оптимизированные под конкретные данные. Производительность при грамотной реализации кодогенерации сопоставима с полностью ручной реализацией алгоритмов для конкретных данных. Например, скорость десериализации XmlSerializer древовидной структуры практически такая же, как ручное чтение из XmlReader, и значительно (в 2-3 раза) выше скорости SoapFormatter, использующего универсальные алгоритмы и механизм отражения (reflection). Скорость работы ASP.NET, использующего кодогенерацию на сложных алгоритмах, ощутимо выше скорости ASP, использующего интерпретацию.

Однако есть у приведенного примера и один недостаток, в некоторых случаях существенный. Компиляция из исходных кодов - это очень медленный процесс. Заметить это можно, к примеру, по значительной задержке загрузки aspx-страницы при ее изменении. Но и в этом случае есть выход – .NET Framework позволяет создавать сборки сразу в IL-кодах.

СОВЕТ

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

Кодогенерация сразу в IL-код

Для создания сборок «на лету» механизм отражений предоставляет набор возможностей, выделенных в пространство имен System.Reflection.Emit. Попробуем сгенерировать классы сразу в сборку, минуя генерацию исходных кодов и компиляцию.

        using System;
using System.Reflection;
using System.Reflection.Emit;

namespace Rsdn
{
  /// <summary>/// Пример 8./// </summary>class Sample7
  {
    privatestatic Assembly GenerateClasses(ClassDescriptor clsDesc)
    {
      // Создаем динамическую сборку.// Указываем в качестве доступа только выполнение.// В реальных проектах желательно сгенерированные сборки кешировать на// диске, это позволит избежать повторных генераций.
      AssemblyName asmName = new AssemblyName();
      asmName.Name = "classes";
      AssemblyBuilder asmBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
        asmName, AssemblyBuilderAccess.Run);

      // Любая сборка состоит из модулей. Однофайловая сборка// содержит 1 модуль. Создадим модуль. Если необходимо сохранять сборку,//  то помимо имени модуля следует указать имя файла. Модули без имени//  файла считаются временными (transient) и на диске не сохраняются.
      ModuleBuilder modBuilder = asmBuilder.DefineDynamicModule("main");

      // Теперь создадим классы сборкиforeach (ClassData clsData in clsDesc.Classes)
      {
        // Определяем класс
        TypeBuilder typeBuilder = modBuilder.DefineType(clsDesc.Namespace + 
          Type.Delimiter + clsData.Name);
        
        // Добавим свойства по вкусуforeach (PropertyData propData in clsData.Properties)
        {
          // Добавляем private-полеstring fldName = "_" + propData.Name;
          Type propType = Type.GetType(propData.Type);
          FieldBuilder fldBuilder = typeBuilder.DefineField(fldName, propType, 
            FieldAttributes.Private);

          // Добавляем свойство
          PropertyBuilder propBuilder = typeBuilder.DefineProperty(propData.Name,
            PropertyAttributes.None, propType, new Type[]{});

          // Добавляем get-метод свойства
          MethodBuilder getBuilder = typeBuilder.DefineMethod("get_" + propData.Name,
            MethodAttributes.Public, propType, new Type[]{});

          // Генерируем код метода
          ILGenerator getGen = getBuilder.GetILGenerator();
          // загружаем this в стек
          getGen.Emit(OpCodes.Ldarg_0);
          // загружаем значение поля в стек
          getGen.Emit(OpCodes.Ldfld, fldBuilder);
          // Возвращаем результат
          getGen.Emit(OpCodes.Ret);

          propBuilder.SetGetMethod(getBuilder);

          // Добавляем set метод свойства
          MethodBuilder setBuilder = typeBuilder.DefineMethod("set_" + propData.Name,
            MethodAttributes.Public, propType, new Type[]{propType});

          // Генерируем код метода
          ILGenerator setGen = setBuilder.GetILGenerator();
          // загружаем this в стек
          setGen.Emit(OpCodes.Ldarg_0);
          // загружаем аргумент (value) в стек
          setGen.Emit(OpCodes.Ldarg_1);
          // записываем в поле
          setGen.Emit(OpCodes.Stfld, fldBuilder);
          // Возвращаем управление
          setGen.Emit(OpCodes.Ret);

          propBuilder.SetSetMethod(setBuilder);
        }

        // Создаем класс внутри модуля
        typeBuilder.CreateType();
      }

      return asmBuilder;
    }

    staticvoid Main(string[] args)
    {
      Console.WriteLine("Пример 8 к статье.");
      Console.WriteLine();
      
      Console.WriteLine("Классы сборки");
      Assembly asm = GenerateClasses(ClassDescriptor.Load());
      //asm = Assembly.Load("classes");foreach (Type t in asm.GetTypes())
      {
        Console.WriteLine(t.FullName);
        foreach (PropertyInfo pi in t.GetProperties())
          Console.WriteLine("\t{0} {1}", pi.PropertyType.FullName, pi.Name);
      }

      Console.Read();
    }
  }
}

Заключение

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

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


Эта статья опубликована в журнале RSDN Magazine #4-2003. Информацию о журнале можно найти здесь
    Сообщений 8    Оценка 1092 [+2/-0]         Оценить