Javascript Dsl Для C++ Или Генератор Кода — Это Просто!

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

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

И тут я вспомнил похожую историю из своего далекого прошлого, которой делюсь.



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

Проблема Это было давно и неправда, настолько давно, что это можно смело отнести к мемуарам.

Нам поручили помочь отделу аутсорсинга.

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

Необходимо было создать интерфейс бизнес-уровня к данным, хранящимся на 20 планшетах.

В С++.

Для Windows и Linux. 20 пластин в Postgres или Oracle. «Интерфейс бизнес-уровня» — это эвфемизм для очень глупого набора операций над сущностями из этих таблиц, а именно выбора по ключу или значению других полей, создания, изменения или выбора связанных сущностей.

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

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

На этом фоне у моих коллег все было не так уж и плохо - они просто перебрали кучу C++(C) или библиотек, упрощающих доступ к базе данных, и убедились, что ни одна из них, вопреки утверждениям, не работает нормально одновременно как Oracle и Postgres. Идея Убежденность в том, что эту дурацкую работу должен делать кто угодно, только не я, постепенно подтолкнула меня к мысли, что я должен винить в этом кого-то.

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

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

Отладчик показал мне код, сгенерированный на странице JSP (страницы сервера Java).

Например, с этой страницы:

  
  
  
   
  
  
 rhino codegen.js template:
В результате получается примерно следующий код:
  
  
  
  
  
 rhino codegen.js template:
То есть алгоритм генерации кода из JSP, который, в свою очередь, генерирует HTML, очень прост, примерно: всё внутри <% %> становится кодом, а все, что находится снаружи, оборачивается out.print(" "), ну и немного синтаксического сахара.

В нашем случае нам просто нужно заменить HTML на C++, а Java на что-то другое и после простейших преобразований мы получим код, который сгенерирует для нас нужный код C++.

Я выбрал «что-то еще» по принципу наименьшего сопротивления — хост Windows-скриптов уже был на нашей сборочной машине, поэтому мы используем java-скрипт (или ECMAScript, как бы он ни назывался).

Опытный образец В этой статье мы рассмотрим решение, максимально «близкое к тексту».

К сожалению, рассмотреть оригинальное решение не получится — слишком много важных деталей уже стерты из памяти.

Будем считать, что у нас есть только база данных Postgres, поэтому, чтобы не покидать уютную среду Linux, мы также не будем использовать хост сценариев Windows; мы будем использовать некоторую простую стандартную схему с данными о сотруднике, отделе и т. д. В качестве самостоятельной реализации JS возьмем первую, которая пришла на ум — Rhino (почему-то v8 была второй).

Итак, устанавливаем Rhino, создаем файл codegen.js, пишем print(2*2); rhino codegen.js: 4, вуаля! codegen.js

  
 rhino codegen.js cpp.template:
— самая тупая и прямолинейная реализация описанного чуть выше алгоритма — 50 строк — весь генератор кода.

Тестирование: файл шаблона:

  
 rhino codegen.js sql.template:


<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>");

В <% %> у нас есть любой js-код, <%= expr %> заменяется результатом вычисления выраж.

В принципе, это всё, что нам нужно, достаточно, чтобы сгенерировать вообще что угодно.

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

Сам 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);



<% var className = 'MyClass'; var fields = ['Name', 'Description', 'AnotherOne', 'LastOne']; %> class <%= className %> { private: <% for(var i = 0; i < fields.length; i++) { %> int <%= fields[i] %>; <% } %> };



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

Содержимое переменной модели, по сути, является так называемым DSL (предметно-ориентированный язык).

В нашем случае это язык описания сущностей предметной области.

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



<% 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 %>; <% } %> }; <% } %>

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

Чтобы генерировать разные исходники из одной модели, немного разбросаем код: модель

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

будет содержать только наш DSL, cpp.шаблон

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.шаблон

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' } ];

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



<% 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; } <% } %> }; <% } %>



<% 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 %>) ); <% } %>

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

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; } };



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-движок, и на компиляторе — генерировать плюс-код гораздо проще, чем машинный код, и пусть плюс-компилятор несет всю нагрузку.

Теги: #JavaScript #C++ #dsl #генерация кода #генератор кода #генеративное программирование #orm #JavaScript #C++

Вместе с данным постом часто просматривают: