DSL на JavaScript для C++ или кодгенератор — это просто!

    С добрым понедельником, хабровчане!

    Ковырялся давеча с одним универсальным, и потому до неприличного мощным, интерфейсом доступа к данным на Python-е. Неприличная мощь выражается в виде множества параметров на все случаи жизни, зачастую крайне экстравагантные и нужные только в 5% случаев. В итоге приходится дублировать всю пачку параметров и деталей даже в прямолинейных запросах, что вызывает пессимизм и желание заняться чем-то другим. И тут вспомнилась мне аналогичная история из моего далекого прошлого, которой и делюсь.


    Проблема


    Было это давно и не правда, настолько давно, что можно смело относить в категорию мемуаров. Отрядили нас в помощь отделу занимающемуся аутсорсингом. Проект был уже в стадии активного кодирования и, что-то обсуждать и менять было уже поздно (а может и изначально невозможно). Необходимо было сделать интерфейс бизнес уровня к данным, хранящимся в 20 табличках. На С++. Под винду и линукс. 20 табличек в Постгресе или Оракле. «Интерфейс бизнес уровня» это такой эвфемизм к тупейшему набору операций над сущностями из этих таблиц, а именно — выбрать по ключу или значению других полей, создать, изменить или выбрать связные сущности.
    К тому моменту я уже начинал догадываться, что наша работа иногда бывает не очень интересной, но чтобы настолько! Эта задача по своей унылости побила все рекорды. В одночасье я почувствовал себя в шкуре тех людей которым их работа не интересна, и меня это, почему-то, совсем не воодушевляло. На этом фоне у моих коллег все было не так плохо — они, всего лишь, перебрали кучу С++(С) либ упрощающих доступ к БД и убедились в том, что ни одна из них, вопреки заявлениям, нормально не работает одновременно с Ораклом и Постгресом.

    Идея


    Убежденность в том, что это тупую работу должен делать кто угодно, но только не я, потихоньку подталкивала меня к мысли, что надо на кого-нить это свалить. Из моего окружения смириться с такой участью согласился только компьютер, так что, в итоге, ему выпала честь сгенерировать необходимый код. Напрягало во всей этой затее только то, что на тот момент о предмете у меня было весьма смутное представление как о чем-то очень сложном и нетривиальном. Из инкубационного периода идею с генерацией вывело случайное воспоминание о дебуге одного вэб приложения с JSP, бинами и прочим ужасом летящим на крыльях ночи. Дебугер показал мне код который генерится из JSP (java server pages) странички. К примеру из такой вот странички:
    <ul>
    <% for (int i = 0; i < 10; i++) { %>
      <li> <%= i %>
    <% } %>
    </ul>
    

    получается примерно следующий код:
    out.println("<ul>");
    for (int i = 0; i < 10; i++) {
      out.print("<li>");
      out.print(i);
      out.println();
    }
    out.println("</ul>");
    

    То есть, алгоритм генерации из JSP кода который, в свою очередь, генерит HTML очень прост, приблизительно: все, что внутри <% %> становится кодом, а все, что снаружи заворачивается в out.print("<текст>"), ну и немного синтаксического сахара. В нашем случае нужно лишь заменить HTML на С++, а жаву на что-то еще и, после простейших преобразований, мы получаем код, который нам сгенерит желаемый C++ код. «Что-то еще» я выбрал по принципу наименьшего сопротивления — windows scripting host уже был на нашей билд машине, соответственно, используем java script (или ECMAScript, как его там).

    Прототип


    В данной статье мы рассмотрим максимально «близкое к тексту» решение. Оригинальное решение, к сожалению, рассмотреть не получится — слишком много важных деталей уже стерлось из памяти. Будем считать, что БД у нас только Постгрес, чтобы не покидать уютный линух энвайрмент windows scripting host тоже не будем использовать, в качестве данных будем использовать какую-нить простенькую типовую схемку с Employee, Department etc.
    В качестве standalone реализации JS возьмем первый пришедший в голову — rhino (почему-то v8 был вторым). Итак, ставим rhino, создаем файл codegen.js, пишем туда print(2*2); rhino codegen.js: 4, voila!
    codegen.js
    if (arguments.length < 1)
    {
      print("Usage: codegen.js <template>");
      quit();
    }
     
    function produce_text(text)
    {
      return "__codegen_output += '" + text.replace("'", "\\'", 'g').replace('\n', '\\n', 'g').replace('\r', '\\r', 'g').replace('\t', '\\t', 'g') + "';\n";
    }
     
    function produce_code(code)
    {
      if (code[0] == '=')
      {
        return '__codegen_output += ' + code.substr(1) + ';\n';
      }
      return code + '\n';
    }
     
    remainder = readFile(arguments[0]);
    var code = 'var __codegen_output = ""; ';
    while (remainder.length)
    {
      var begin = remainder.indexOf('<%');
      if (begin >= 0)
      {
        code += produce_text(remainder.substr(0, begin));
        remainder = remainder.substr(begin + 2);
        var end = remainder.indexOf('%>');
        if (end >= 0)
        {
          code += produce_code(remainder.substr(0, end));
          remainder = remainder.substr(end + 2);
        }
        else
        {
          code += produce_code(remainder);
          remainder = '';
        }
      }
      else
      {
        code += produce_text(remainder);
        remainder = '';
      }
    }
    code += 'print(__codegen_output);'
     
    eval(code);
    

    — тупейшая, прямолинейная реализация описанного чуть выше алгоритма — 50 строк — весь кодогенератор. Тестируем:
    файл template:
    <%
    var className = 'MyClass';
    var fields = ['Name', 'Description', 'AnotherOne', 'LastOne'];
    %>
    
    class <%= className %>
    {
      private:
      <% for(var i = 0; i < fields.length; i++) { %>
        int <%= fields[i] %>;
      <% } %>
    };
    


    rhino codegen.js template:

    class MyClass
    {
      private:
    
        int Name;
    
        int Description;
    
        int AnotherOne;
    
        int LastOne;
    
    };
    

    В <% %> у нас любой js код, <%= expr %> заменяется результатом вычисления expr. В принципе — это все, что нам надо, того достаточно, чтобы сгенерить вообще все, что угодно. К сожалению, стоит отметить, что это простота имеет и обратную сторону — код в шаблоне очень плотный и без подходящей подсветки синтаксиса читать его сложно. Не последнюю скрипку в этом играет сам JS — лаконичностью и выразительностью он не отличается.
    Теперь время попробовать сгенерить что-то более полезное.
    файл template:
    <%
    var model = [
      {
        name: 'Employee',
        fields: {
          Id: { type: 'int' },
          Name: { type: 'string' }
        }
      }
    ];
    
    var cppTypeMap = {
      'int': 'int',
      'string': 'std::string'
    };
    %>
    
    <% for (var i = 0; i < model.length; i++)
       {
         var entity = model[i];%>
         struct <%= entity.name %>
    {
      <% for (var field in entity.fields)
         { %>
      <%= cppTypeMap[entity.fields[field].type] %> <%= field %>;
      <% } %>
    };
    <% } %>
    


    rhino codegen.js template:

    struct Employee
    {
    
      int Id;
    
      std::string Name;
    
    };
    

    Содержимое переменной model это, по сути, так называемый DSL (domain specific language). В нашем случае это язык описания сущностей предметной области. Текущий его вариант слишком примитивен чтобы быть хоть как-то полезным, так что дополним его всем необходимым.
    var model = [
      {
        name: 'Department',
        fields: {
          Id: { type: 'int' },
          Name: { type: 'string'}
        },
        primaryKey: 'Id'
      },
      {
        name: 'Employee',
        fields: {
          Id: { type: 'int' },
          Name: { type: 'string' },
          DepartmentId: { type: 'int', references: 'Department' }
        },
        primaryKey: 'Id'
      }
    ];
    


    Решение


    Теперь мы в состоянии сгенерить код получения сущностей из БД по ключу, по связным сущностям и т.д. Также уже можно сгенерить sql скрипт для создания базы данных. Чтобы из одной модели генерить разные исходники немного поразбросаем код:
    model
    var model = [
      {
        name: 'Department',
        fields: {
          Id: { type: 'int' },
          Name: { type: 'string'}
        },
        primaryKey: 'Id'
      },
      {
        name: 'Employee',
        fields: {
          Id: { type: 'int' },
          Name: { type: 'string' },
          DepartmentId: { type: 'int', references: 'Department' }
        },
        primaryKey: 'Id'
      }
    ];
    

    будет содержать только наш DSL,
    cpp.template
    <%
     
    load('model');
     
    var cppTypeMap = {
      'int': 'int',
      'string': 'std::string'
    };
     
     
    function fieldType(entity, field)
    {
      return cppTypeMap[entity.fields[field].type];
    }
     
    %>
     
    <% for (var i = 0; i < model.length; i++)
       {
          var entity = model[i];%>
    struct <%= entity.name %>
    {
      <% for (var field in entity.fields)
         { %>
      <%= fieldType(entity, field) %> <%= field %>;
      <% } %>
      <% var fieldList = [];
         for (var field in entity.fields)
           fieldList.push(field);
      %>
      static <%= entity.name %> ByKey(<%= fieldType(entity, entity.primaryKey) %> key, pqxx::work& tr)
      {
        if (!tr.prepared("<%= entity.name %>ByKey").exists())
        {
          tr.conn().prepare("<%= entity.name %>ByKey",
             "select <%= fieldList.join() %> from <%= entity.name %> where <%= entity.primaryKey %> = $1");
        }
        pqxx::result rows = tr.prepared("<%= entity.name %>ByKey")(key).exec(query);
        <%= entity.name %> result;
        <% for (var j = 0; j < fieldList.length; j++)
           { %>
        result.<%= fieldList[j] %> = rows[0][<%= j %>].as<<%= fieldType(entity, fieldList[j]) %>>();
        <% } %>
        return result;
      }
     
      <% for (var field in entity.fields)
           if (entity.fields[field].references)
           { 
             var ref = entity.fields[field].references; %>
      <%= ref %> Get<%= ref %>()
      {
        return <%= ref %>::ByKey(<%= field %>);
      }
     
      static std::vector<<%= entity.name %>> By<%= ref %>(<%= fieldType(entity, field) %> key)
      {
        if (!tr.prepared("<%= entity.name %>By<%= ref %>").exists())
        {
          tr.conn().prepare("<%= entity.name %>By<%= ref %>",
             "select <%= fieldList.join() %> from <%= entity.name %> where <%= field %> = $1");
        }
        pqxx::result rows = tr.prepared("<%= entity.name %>By<%= ref %>")(key).exec(query);
        std::vector<<%= entity.name %>> result;
        for (pqxx::result::size_type i = 0; i < rows.size(); i++)
        {
          <%= entity.name %> row;
        <% for (var j = 0; j < fieldList.length; j++)
           { %>
          row.<%= fieldList[j] %> = rows[i][<%= j %>].as<<%= fieldType(entity, fieldList[j]) %>>();
        <% } %>
          result.push_back(row);
        }
        return result;
      }
      <%   } %>
    };
    <% } %>
    

    — шаблон для генерации плюсового кода и
    sql.template
    <%
     
    load('model');
     
    var sqlTypeMap = {
      'int': 'integer',
      'string': 'text'
    };
     
     
    function fieldType(entity, field)
    {
      return sqlTypeMap[entity.fields[field].type];
    }
     
    %>
     
    <% for (var i = 0; i < model.length; i++)
       {
          var entity = model[i];%>
    CREATE TABLE <%= entity.name %> (
      <% for (var field in entity.fields)
         {
           var ref = entity.fields[field].references; %>
      <%= field %> <%= fieldType(entity, field) %><% if (ref) { %> REFERENCES <%= ref %><% } %>,
      <% } %>
      PRIMARY KEY (<%= entity.primaryKey %>)
    );
    <% } %>
    

    — шаблон для генерации скрипта создания БД.

    rhino codegen.js cpp.template:

    struct Department
    {
      
      int Id;
      
      std::string Name;
      
      
      static Department ByKey(int key, pqxx::work& tr)
      {
        if (!tr.prepared("DepartmentByKey").exists())
        {
          tr.conn().prepare("DepartmentByKey",
             "select Id,Name from Department where Id = $1");
        }
        pqxx::result rows = tr.prepared("DepartmentByKey")(key).exec(query);
        Department result;
        
        result.Id = rows[0][0].as<int>();
        
        result.Name = rows[0][1].as<std::string>();
        
        return result;
      }
    
      
    };
    
    struct Employee
    {
      
      int Id;
      
      std::string Name;
      
      int DepartmentId;
      
      
      static Employee ByKey(int key, pqxx::work& tr)
      {
        if (!tr.prepared("EmployeeByKey").exists())
        {
          tr.conn().prepare("EmployeeByKey",
             "select Id,Name,DepartmentId from Employee where Id = $1");
        }
        pqxx::result rows = tr.prepared("EmployeeByKey")(key).exec(query);
        Employee result;
        
        result.Id = rows[0][0].as<int>();
        
        result.Name = rows[0][1].as<std::string>();
        
        result.DepartmentId = rows[0][2].as<int>();
        
        return result;
      }
    
      
      Department GetDepartment()
      {
        return Department::ByKey(DepartmentId);
      }
    
      static std::vector<Employee> ByDepartment(int key)
      {
        if (!tr.prepared("EmployeeByDepartment").exists())
        {
          tr.conn().prepare("EmployeeByDepartment",
             "select Id,Name,DepartmentId from Employee where DepartmentId = $1");
        }
        pqxx::result rows = tr.prepared("EmployeeByDepartment")(key).exec(query);
        std::vector<Employee> result;
        for (pqxx::result::size_type i = 0; i < rows.size(); i++)
        {
          Employee row;
        
          row.Id = rows[i][0].as<int>();
        
          row.Name = rows[i][1].as<std::string>();
        
          row.DepartmentId = rows[i][2].as<int>();
        
          result.push_back(row);
        }
        return result;
      }
      
    };
    

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

    rhino codegen.js sql.template:

    CREATE TABLE Department (
      
      Id integer,
      
      Name text,
      
      PRIMARY KEY (Id)
    );
    
    CREATE TABLE Employee (
      
      Id integer,
      
      Name text,
      
      DepartmentId integer REFERENCES Department,
      
      PRIMARY KEY (Id)
    );
    

    Для многих, я думаю, очевидно, что ничего нового я не придумал. Множество ORM'ов умеет генерить аналогичный код. Основная цель статьи была продемонстрировать, что создать свой язык, пусть всего лишь DSL, не просто, а очень просто. Это совсем не так страшно, как кажется, потому как на многих этапах можно очень неплохо сэкономить. Например, в данном случае мы сэкономили на парсере — большую часть работы делает JS движок, и на компиляторе — генерить плюсовый код значительно проще, чем машинный, а тяжести пусть таскает плюсовый компилятор.

    Acronis

    92,00

    Компания

    Поделиться публикацией

    Похожие публикации

    Комментарии 3
      +2
      В уже забытом 2001 делал что-то похожее под delphi, для генерации платёжных поручений и репортов в excel, только вместо бд был консольный клиент diasoft к субд pervacivesql (доступ к субд не предоставили), который пришлось облачить в gui… Тоже использовал cscript и js для генерации, правда я тогда знать не знал что такое dsl и использовал просто макроопределения, который формировались в delphi парсингом вывода консольного клиента. :)
        +2
        На тот момент я тоже всех этих «страшных» слов не знал :)
        –1
        «дебугер», оригинально…

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое