Кодогенератор Waffle++ для C++

от автора

Список пасхалок: https://clck.ru/33J7ck
Список пасхалок: https://clck.ru/33J7ck

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

Я делаю расширяемый кодогенератор для C++, в котором можно реализовать много полезного. Примеры модулей: перевод значений enum в строку и обратно, перевод структуры в JSON и обратно, декларативный веб-сервер, система слотов и сигналов, свой динамический полиморфизм, генератор кода для тестов…

В этом обзоре будет showcase, сравнение с другими кодогенераторами, описание работы модулей, как сделать свой модуль, и как подключить кодогенератор в свои проекты.

Почему Waffle++?

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

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

Enum в строку и обратно

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

Waffle++ читает особо отформатированные комментарии, чтобы сгенерировать код. Читаются комментарии формата Doxygen — это формат тулзы, которая используется для генерации HTML-страниц документации. Waffle++ читает свои «команды» по аналогии с существующими «командами» Doxygen.

Для перевода enum в строку и обратно нужно подключить статический header, где объявлены функции:

#pragma once  #include <span> #include <string_view>  namespace Waffle {  template<typename EnumType> EnumType FromString(std::string_view value);  template<typename EnumType> EnumType FromStringOrDefault(std::string_view value, EnumType defaultResult);  template<typename EnumType> std::string_view ToString(EnumType value);  template<typename EnumType> std::span<const EnumType> GetAllEnumValues();  } // namespace Waffle

Если есть обычный enum class Color, то без кодогенератора использование данных функций с этим типом упадет во время линковки, потому что компилятор не найдет определение шаблонной функции с нужным шаблонным параметром. Нужно пометить enum командой serializable, чтобы Waffle++ «увидел» его:

// @serializable enum class Color {     Red,     Green,     Blue,     Cyan,     Magenta,     Yellow,     Black };

Пусть этот enum находится в файле foo.h, тогда Waffle++ сгенерирует foo.enum_serializer.cpp с определениями шаблонных функций, и этот файл просто нужно будет указать в вашей системе сборки.

По умолчанию названия переводятся один к одному, то есть вызов Waffle::ToString(Color::Red) вернет строку "Red".

Но через команду stringvalue можно указать любую другую строку, или даже несколько строк («каноничной» будет считаться первая в списке):

/*  * @brief Represents the color of a book  * @author Izaron  * @serializable  */ enum class BookColor {     kRed, ///< @stringvalue red rot rouge     kGreen, ///< @stringvalue green grün vert     kBlue, ///< @stringvalue blue blau bleu };

В примере выше команды, которые читает Doxygen (brief, author) перемешаны с командами Waffle++ (serializable, stringvalue ).

ВызовToString(BookColor::kRed) вернет "red". Вызовы FromString<BookColor>(XXX) вернут BookColor::kRed для XXX равному "red", "rot" или "rouge".

Можно посмотреть исходные enum: misc_enum_places.h, custom_names.h.

Кодогенерация: misc_enum_places.enum_serializer.cpp, custom_names.enum_serializer.cpp.

Тест с примерами: test.cpp.

Структура в JSON и обратно

Для этого модуля тоже нужно подключить статический header с двумя функциями:

#pragma once  #include <nlohmann/json.hpp>  namespace Waffle {  template<typename T> nlohmann::json ToJson(const T& value);  template<typename T> T FromJson(const nlohmann::json& value);  } // namespace Waffle

nlohmann/json это header-only библиотека для работы с Json в C++, чтобы не делать свои велосипеды с представлением Json.

Структуры, для которых нужны определения этих функций, надо помечать jsonable. По умолчанию в Json-представлении имена полей будут такими же, как у структуры, но этим также можно управлять через stringvalue. Файл books_library.h:

#include <optional> #include <string> #include <vector>  namespace model {  struct Book {     std::string Name; // @stringvalue name     std::string Author; // @stringvalue author     int Year; // @stringvalue year };  struct LatLon {     double Lat; // @stringvalue lat     double Lon; // @stringvalue lon };  // @jsonable struct Library {     std::vector<Book> Books; // @stringvalue books     std::optional<std::string> Description; // @stringvalue description     LatLon Address; // @stringvalue address };  } // namespace model

std::vector и подобные контейнеры в Json-представлении преобразуются в array. std::optional преобразуется в null, если он пустой. Метка jsonable транзитивно передается на другие структуры, если есть возможность (в примере выше Book и LatLon неявно помечены jsonable).

Кодогенерация такая: books_library.json_dump.cpp.

В тесте можно посмотреть, как Json и объекты переводятся друг в друга: test.cpp.

Генератор дата-классов

Идея data-классов позаимствована из Java-библиотеки Lombok. В Java очень популярна кодогенерация с кучей идей. В данном случае для класса генерируются геттеры, сеттеры и другие методы.

В Waffle++ для этого есть команда dataclass. В файле mountain.h:

#include <string> #include <optional>  namespace model {  // @dataclass LatLon struct LatLonStub {     double latitude;     double longitude; };  // @dataclass Mountain struct MountainStub {     std::optional<std::string> name;     std::string country; // @getteronly     LatLonStub position;     double peak; };  } // namespace model

Здесь заводятся «мусорные» (неиспользуемые) структуры, а в параметре команды указывается название сгенерированного класса. Для некоторых полей можно определить, что там должны быть доступны только геттеры (то есть эти поля будет нельзя изменить).

Кодогенерация (внимание, не .cpp-файл, а .h-файл!): mountains.data_class.h.

Как видно из кодогенерации, для «больших» типов данных есть два сеттера — по const-ссылке и rvalue-ссылке:

    void SetName(std::optional<std::string>&& name) {         name_ = std::move(name);     }     void SetName(const std::optional<std::string>& name) {         name_ = name;     }     const std::optional<std::string>& GetName() const {         return name_;     }

Для «маленьких» используется обычное копирование:

    void SetPeak(double peak) {         peak_ = peak;     }     double GetPeak() const {         return peak_;     }

Тест: test.cpp.

Мок-классы для GoogleMock

GoogleMock это библиотека для тестирования с использованием «моков» — объектов, которые имитируют поведение внешних сервисов. Лучше почитать документацию, там много информации.

Проблемы возникают во время рефакторинга класса или просто добавления метода. В силу особенностей C++, GoogleMock требует скопипастить определение каждого виртуального метода, иначе код теста не скомпилируется. Таким образом, тратится лишнее время на правку моков в тестах.

В файле turtle.h скопирован класс из документации GoogleMock:

namespace model {  // @gmock class Turtle { public:     virtual ~Turtle() = default;     virtual void PenUp() = 0;     virtual void PenDown() = 0;     virtual void Forward(int distance) = 0;     virtual void Turn(int degrees) = 0;     virtual void GoTo(int x, int y) = 0;     virtual int GetX() const = 0;     virtual int GetY() const = 0; };  } // namespace model

Waffle++ сгенерирует файл turtle.gmock.h с мок-классом, который обычно пишут вручную:

// Generated by the Waffle++ code generator. DO NOT EDIT! // source: turtle.h  #include <gmock/gmock.h>  #include "turtle.h"  namespace Waffle {  class MockTurtle : public model::Turtle { public:     MOCK_METHOD(void, PenUp, (), (override));     MOCK_METHOD(void, PenDown, (), (override));     MOCK_METHOD(void, Forward, (int distance), (override));     MOCK_METHOD(void, Turn, (int degrees), (override));     MOCK_METHOD(void, GoTo, (int x, int y), (override));     MOCK_METHOD(int, GetX, (), (const, override));     MOCK_METHOD(int, GetY, (), (const, override)); };  } // namespace Waffle

Waffle++ устроен так, что при изменении исходного файла (то есть turtle.h) зависимый файл (то есть turtle.gmock.h) перегенерируется на этапе компиляции, не нужно будет даже запускать какие-то дополнительные команды. Позже будет описание работы с системой сборки, которые позволяют такие фокусы.

Декларативный веб-сервер

Сейчас начинаются тяжелые примеры. В движке Java Spring, который является de facto стандартом индустрии, есть кодогенерация вплоть до веб-сервера с минимумом кода. В «аннотациях» описываются обработчики HTTP-запросов, выглядит все максимально человекочитаемо:

Пример для Java, Spring Framework
@RestController class EmployeeController {    private final EmployeeRepository repository;    EmployeeController(EmployeeRepository repository) {     this.repository = repository;   }    @GetMapping("/employees")   List<Employee> all() {     return repository.findAll();   }    @PostMapping("/employees")   Employee newEmployee(@RequestBody Employee newEmployee) {     return repository.save(newEmployee);   }    @GetMapping("/employees/{id}")   Employee one(@PathVariable Long id) {     return repository.findById(id)       .orElseThrow(() -> new EmployeeNotFoundException(id));   }    @DeleteMapping("/employees/{id}")   void deleteEmployee(@PathVariable Long id) {     repository.deleteById(id);   } }

При GET-запросе на http://myserver.com/employees/123 вызовется метод one(123), и в ответе на запрос вернут json-представление объекта Employee.

В нашем примере этот модуль нужно совместить с модулем json_dump, который описывался ранее:

Пример для C++, Waffle++

Файл employee.h:

#include <memory> #include <optional> #include <vector>  namespace model {  // @jsonable struct Employee {     size_t Id; // @stringvalue id     std::string Name; // @stringvalue name     double Salary; // @stringvalue salary };  class IEmployeeRepository { public:     virtual void Add(Employee employee) = 0;     virtual std::optional<Employee> FindById(size_t id) = 0;     virtual std::vector<Employee> FindBySalaryRange(double lowerBound, double upperBound) = 0;     virtual std::vector<Employee> FindAll() = 0;     virtual void DeleteById(size_t id) = 0; };  // @restcontroller class EmployeeController { public:     EmployeeController(std::shared_ptr<IEmployeeRepository> repository)         : repository_{std::move(repository)}     {}      /*      * @brief Add a new employee      * @postmapping /employees      * @requestbody employee      */     void Add(Employee employee) {         repository_->Add(std::move(employee));     }      /*      * @brief Get the employee with given ID      * @getmapping /employees/{id}      * @pathvariable id      */     std::optional<Employee> FindById(size_t id) {         return repository_->FindById(id);     }      /*      * @brief Get all employers with salary in given range      * @getmapping /employees/find?lowerBound={lowerBound}&upperBound={upperBound}      * @pathvariable lowerBound      * @pathvariable upperBound      */     std::vector<Employee> FindBySalaryRange(double lowerBound, double upperBound) {         return repository_->FindBySalaryRange(lowerBound, upperBound);     }      /*      * @brief Get all employees      * @getmapping /employees      */     std::vector<Employee> FindAll() {         return repository_->FindAll();     }      /*      * @brief Delete the employee with given ID      * @deletemapping /employees/{id}      * @pathvariable id      */     void DeleteById(size_t id) {         repository_->DeleteById(id);     }  private:     std::shared_ptr<IEmployeeRepository> repository_; };  } // namespace model

В этом модуле есть команды restcontroller (класс-обработчик, для которого сгенерировать код) getmapping/postmapping/deletemapping (соответствующие методы HTTP-запроса), pathvariable (в переменную подставляется кусок пути), requestbody (в переменную подставляется body HTTP-запроса).

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

#pragma once  #include <memory> #include <string>  namespace Waffle {  struct HttpRequest {     std::string Method;     std::string Path;     std::string Body; };  struct HttpResponse {     int StatusCode;     std::string Body; };  template<typename Handler> [[nodiscard]] HttpResponse ProcessRequest(Handler& handler, const HttpRequest& httpRequest);  } // namespace Waffle

Кодогенератор сгенерирует два файла: employee.json_dump.cpp и employee.rest_controller.cpp.

В большом тесте проверяется корректность обработчика HTTP-запросов: test.cpp.

Свой динамический полиморфизм

Я несколько раз пробовал сделать полиморфизм в C++ «в стиле Go».

Интерфейсы в Go работают так, что они позволяют писать один и тот же код для разных классов, которые вообще никак не связаны в родственном плане (т.е. они не «наследуются друг от друга» и т.д., в Go этого нет). В C++ есть шаблоны, но это просто заготовки кода, а не что-то нормальное.

Несколько моих подходов закончились чем-то вроде kelbon/AnyAny — ограничения C++ не позволяют отойти от реализации, похожей на эту библиотеку.

С кодогенерацией это стало намного гибче! Пример structs.h с интерфейсами:

#include <string>  namespace model {  // @polymorphic struct Robot {     void Forward(double distance);     void Turn(double degrees);     void GoTo(double x, double y);     double GetX() const;     double GetY() const; };  // @polymorphic struct Stringer {     std::string String() const; };  } // namespace model

Кодогенератор сделает файл structs.poly_ptr.h с несколькими классами (для T = Robot и T = Stringer):

  • poly_obj<T> — объект-враппер, который содержит что-то, что имеет такие же методы, как интерфейс. Это достигается за счет type erasure, как в std::function. Объект-враппер аллоцирует память в куче и управляет жизнью содержимого объекта.

  • poly_ref<T> — ссылка на что-то, что имеет такие же методы, как интерфейс. Быстр почти как void*, не управляет жизнью содержимого объекта.

  • poly_ptr<T> — то же самое, что poly_ref<T>, но может быть пустым (не указывать на объект).

  • const_poly_ref<T> и const_poly_ptr<T> — то же самое, что два класса выше, но доступны только константные методы интерфейса.

В тестах проверяется поведение класса, который удовлетворяет обоим интерфейсам:

class TestRobot { public:     void Forward(double distance) { /* ... */ }     void Turn(double degrees) { /* ... */ }     void GoTo(double x, double y) { /* ... */ }     double GetX() const { /* ... */ };     double GetY() const { /* ... */ };     std::string String() const { /* ... */ }      /* ... */ };

Тесты: poly_obj_test.cpp, poly_ref_test.cpp, poly_ptr_test.cpp.

Система сигналов и слотов

Сигналы и слоты используются для коммуникации между объектами. Наверное, самая популярная реализация этого паттерна реализована в Qt (документация с примерами).

У Qt в документации есть хорошее объяснение концепции, я приведу краткий пересказ:

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

Пользователь подключает статический header:

#pragma once  namespace Waffle {  class SignalBase { public:     virtual ~SignalBase(); };  namespace Impl {     /* ... */ } // namespace Impl  template<typename SenderType, typename ReceiverType, typename... Args> void Connect(const SignalBase* sender, void(SenderType::*signal)(Args...),              const SignalBase* receiver, void(ReceiverType::*slot)(Args...)) {     /* ... */ }  } // namespace Waffle

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

Реализация класса в counter.h, почти такая же как в документации Qt:

#include <string>  #include <waffle/modules/signals/signals.h>  namespace model {  class Counter : public Waffle::SignalBase { public:     Counter();      int Value() const;      // @slot     void SetValue(int value);      // @signal     void ValueChanged(int newValue);  private:     int Value_; };  } // namespace model

В counter.cpp определяется только метод-«слот»:

#include "counter.h"  namespace model {  Counter::Counter()     : Value_{0} { }  int Counter::Value() const {     return Value_; }  void Counter::SetValue(int value) {     if (value != Value_) {         Value_ = value;         ValueChanged(value);     } }  } // namespace model

Кодогенератор сгенерирует counter.signals.cpp с «рантаймом». Хотя лучше было бы сделать рантайм в виде отдельной библиотеки. Но все модули, которые я описываю, можно улучшать до пригодности к использованию.

Тест связывает сигнал со слотом и проверяет, что связь работает (тоже как в документации Qt):

#include <gtest/gtest.h> #include "counter/counter.h"  TEST(Signals, Smoke) {     model::Counter a, b;     Waffle::Connect(&a, &model::Counter::ValueChanged,                     &b, &model::Counter::SetValue);      a.SetValue(12);     ASSERT_EQ(a.Value(), 12);     ASSERT_EQ(b.Value(), 12);      b.SetValue(48);     ASSERT_EQ(a.Value(), 12);     ASSERT_EQ(b.Value(), 48); }

Аналогичные проекты

В списке библиотек awesome-cpp нет раздела для кодогенераторов. Кодогенерация тесно связана с рефлексией, поэтому можно посмотреть на библиотеки из списка Reflection. Их объединяют общие черты:

  • Малая область действия, например всего лишь перевод из enum в строку и обратно.

  • Магические макросы, в которых все равно надо вручную перечислять все поля и значения.

  • В некоторых случаях — попытка дать пользователю «полную рефлексию», что вряд ли в принципе реализуемо. Например, в Clang для описания разных сущностей C++ используются сотни классов. Пример 1, пример 2.

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

У меня нет большого опыта в Qt, но выглядит, что на Waffle++ похож его Meta-Object Compiler — там по похожему принципу генерируются .cpp-файлы (на основе магических макросов).

Хороший пример кодогенерации на основе своего DSL есть у protobuf.

Некоторые проприетарные IDE имеют возможность что-то сгенерировать в коде проекта — пример с google mock.

Устройство кодогенератора: Общее

Исходники проекта находятся на GitHub: Izaron/WafflePlusPlus. Посмотрим на структуру директории src/:

  • bin/ — создание бинарника кодогенератора wafflec со всеми модулями.

  • cmake_scripts/ — набор функций CMake, чтобы было удобнее управлять кодогенерацией.

  • include/waffle/modules/ — статические header-ы, которых нужно подключать пользователям определенных модулей.

  • lib/ — общие библиотеки кодогенератора.

  • modules/ — реализация модулей.

  • thirdparty/include/ — header-only библиотеки, используемые кодогенератором.

Посмотрим на зависимости. Проект зависит от внешних библиотек:

  • GoogleTest — для тестирования.

  • LibClang — для чтения исходников C++.

  • pantor/inja — шаблонизатор (header-only библиотека).

  • nlohmann/json — работа с JSON (header-only библиотека). Шаблонизатор тоже зависит от этой библиотеки.

Самой важной библиотекой является LibClang. За ее счет происходит весь банкет, потому что она умеет парсить исходный код на C++ в абстрактное синтаксическое дерево (AST), с которым удобно работать. Пример AST на коде с комментариями можно увидеть здесь — godbolt.

Посмотрим на структуру src/lib/ по отдельным библиотекам:

  • comment/ — вытаскивает doxygen-style комментарии, относящиеся к определению (класса/енама/функции и т.д.)

  • driver/ — здесь есть определение int main(). Разбирает аргументы командной строки, парсит файл, вызывает доступные модули для кодогенерации.

  • file/ — методы для создания генерируемого файла.

  • registry/ — макрос для регистрации модуля, хранилище модулей.

  • string_util/ — нехитрые строковые алгоритмы.

Интересно то, что количество «доступных модулей» зависит от того, как слинковать бинарник кодогенератора. Кодогенератор максимально модулизированный. Можно указать не все модули, тогда в бинарник неуказанные модули не попадут и их код вызываться не будет. Подробнее об этой схеме можно почитать в блоге.

Устройство кодогенератора: Отдельный модуль

Теперь можно в src/modules/ посмотреть на какой-нибудь один модуль. Структура у них одинаковая. Модуль можно написать как угодно, но лучше делать это в структурированном виде. Для примера возьмем src/modules/google_mock/:

  • template.cpp — шаблон генерируемого файла. В нашем случае такой:

// Generated by the Waffle++ code generator. DO NOT EDIT! // source: {{ source_file }}  #include <gmock/gmock.h>  #include "{{ source_file }}"  namespace Waffle {  ## for struct in structs class Mock{{ struct.name }} : public {{ struct.qualified_name }} { public: ## for method in struct.methods     MOCK_METHOD({{ method.return_type }}, {{ method.name }}, ({{ method.signature }}), ({{ method.qualifiers }})); ## endfor };  ## endfor } // namespace Waffle

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

Как делается C++-строка из этого файла? Для этого в CMakeLists.txt вызывается наш метод waffle_generate_template_data(), который внутри себя вызывает утилиту xxd, из-за чего получаем в глубине build-директории файл template.cpp.data, с которым можно сделать так:

#include "template.cpp.data" const std::string TEMPLATE{(char*)template_cpp, template_cpp_len};
  • common.h — некие общие структуры. У google_mock это всего лишь ссылка на объявление класса:

using StructDecl = const clang::CXXRecordDecl*; using StructDecls = std::vector<StructDecl>;
  • collector.cpp/.h — сборщик данных. Также он должен вернуть список используемых команд (LibClang нужно об этом знать, без этого он не распарсит комментарии):

StructDecls Collect(clang::ASTContext& ctx); std::vector<std::string_view> Commands();

Исходник collector.cpp достаточно простой, мы используем clang::RecursiveASTVisitor, чтобы найти все классы помеченные командой // @gmock. В объекте clang::ASTContext содержится вся информация о распарсенном C++-файле.

  • printer.cpp/.h — создатель генерируемого файла. Он берет template.cpp, заполняет json-таблицу, и пихает эти две штуки в шаблонизатор, и записывает получившееся в файл:

void Print(Context& ctx, const StructDecls& decls);

(Context это данные про текущий анализируемый файл)

struct Context {     IFileManager& FileManager;     std::string_view InFile;     clang::ASTContext& AstContext; };
  • register.cpp — регистрация модуля с названием gmock:

using namespace Waffle;  static void Do(Context& ctx) {     if (const auto decls = GoogleMock::Collect(ctx.AstContext); !decls.empty()) {         GoogleMock::Print(ctx, decls);     } }  REGISTER_MODULE(gmock, GoogleMock::Commands(), Do);

Устройство кодогенератора: Трехуровневое тестирование

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

Чтобы дальше было понятнее, сначала посмотрим, как собирается бинарник wafflec со всеми модулями. Файл src/bin/CMakeLists.txt:

if(NOT DEFINED MODULES)     list(APPEND MODULES         data_class         enum_serializer         google_mock         json_dump         poly_ptr         rest_controller         signals) endif()  include(waffle) waffle_add_executable(wafflec "${MODULES}")

Понятно, что в тестах достаточно собрать бинарник только с одним модулем.

Посмотрим на структуру директории src/modules/google_mock/test:

  • CMakeLists.txt — кодогенерация и сборка теста.

  • test.cpp — юнит-тест с использованием кодогенерированных файлов.

  • turtle/turtle.h — исходный файл.

  • turtle/turtle.gmock.h — кодогенерированный файл.

Посмотрим по частям CMakeLists.txt:

# generate new files include(waffle) waffle_add_executable(google_mock_wafflec google_mock) waffle_generate(     google_mock_wafflec     turtle/turtle.h     ${CMAKE_CURRENT_SOURCE_DIR}/turtle/turtle.gmock.h)

Мы сделали бинарник google_mock_wafflec с одним модулем google_mock, и потом даем команду сгенерировать на основе turtle.h файл turtle.gmock.h.

Тем, кто непривычен к системам сборки (как CMake) нужно понимать, что «программирование» в системах сборки не императивное (как в языке по типу C++), а каузальное. То есть CMake описывает в основном не последовательность действий, а некие готовые «цели», которые могут по-разному зависеть от других «целей» (и вообще нетривиальным образом на все влиять).

Функция waffle_generate внутри себя задает такую каузальную связь, что файл turtle.gmock.h будет пересоздаваться каждый раз при изменении turtle.h, причем именно в стадии сборки. Это как раз то, что нужно от кодогенератора.

Обратите внимание, что в третьем аргументе мы явно задали префикс, куда хотим сохранить сгенерированный файл: ${CMAKE_CURRENT_SOURCE_DIR}. Это значит, что файлы генерируются в директории исходников, и все их изменения попадут в дифф коммитов Git.

Можно задать префикс ${CMAKE_CURRENT_BINARY_DIR}, тогда файл будет генерироваться только в директории сборки, и не попадет в сам репозиторий. Оба подхода хороши для разных кейсов.

# link new files with test add_executable(google_mock_test     test.cpp     ${CMAKE_CURRENT_SOURCE_DIR}/turtle/turtle.gmock.h) target_link_libraries(google_mock_test gtest_main gmock_main)

Бинарь тестов google_mock_test зависит от turtle.gmock.h. Таким образом, при сборке «цели» google_mock_test будет запущен кодогенератор, если этого файла еще нет (или исходник turtle.h поменялся).

include(GoogleTest) gtest_discover_tests(google_mock_test) enable_testing()

Это стандартный boilerplate для всех тестов.

Таким образом, получаем трехуровневое тестирование:

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

  2. Получившиеся .h/.cpp-файлы сразу компилируются. Нельзя закоммитить некомпилирующиеся сгенерированные файлы.

  3. Сгенерированные файлы после компиляции проверяются в юнит-тестах. Нельзя закоммитить поломанное поведение у сгенерированных файлов.

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

В данный момент Waffle++ пока не опробован в больших проектах, поэтому оптимальный метод подключения может измениться. В СMake подключить внешний проект можно кучей разных способов. Посмотрим, как это сделать со сборкой Waffle++ с нуля.

Сначала нужно в корневом CMakeLists.txt включить создание файла compile_commands.json в корне build-директории. В этом файле описываются все настройки, нужные для компиляции каждого .cpp-файла. Кодогенератор загружает этот файл, чтобы знать настройки компиляции.

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

Waffle++ можно загрузить прямо из GitHub:

include(FetchContent) FetchContent_Declare(   waffle_plus_plus   GIT_REPOSITORY https://github.com/Izaron/WafflePlusPlus.git   GIT_TAG main ) FetchContent_MakeAvailable(waffle_plus_plus)

Подключим его библиотеки, include-директорию (для статических header-ов) и cmake-скрипт с функциями:

include_directories(${waffle_plus_plus_SOURCE_DIR}/src/include) add_subdirectory("${waffle_plus_plus_SOURCE_DIR}/src") list(APPEND CMAKE_MODULE_PATH "${waffle_plus_plus_SOURCE_DIR}/src/cmake_scripts")

И теперь в вашем проекте в CMakeLists.txt какой-нибудь библиотеки можно использовать кодогенератор:

include(waffle) waffle_generate(     wafflec  # используется бинарь со всеми модулями, но можно сделать свой бинарь     piece.h     ${CMAKE_CURRENT_BINARY_DIR}/piece.enum_serializer.cpp)  add_library(piece piece.cc board_piece.cc piece_registry.cc piece_or_empty.cc ${CMAKE_CURRENT_BINARY_DIR}/piece.enum_serializer.cpp)

Единственное — может потребоваться в корневом CMakeLists.txt «найти» LibClang:

# add Clang find_package(Clang REQUIRED CONFIG)

That’s all Folks!

Кодогенератор можно развивать в разных направлениях:

  • Расширения списка модулей — ORM для баз данных, рандомайзер структур, etc…

  • Python-скрипт для создания своего модуля, чтобы не делать много ручной работы, как сделано в clang-tidy.

  • Доделывание фичей, фикс багов, патчи в Clang (там есть где доработать парсинг комментариев).

  • Внедрение в существующие проекты.

  • Файлы настроек у модулей — например, чтобы управлять стилем функций, названиями неймспейсов и т.д.

  • Поддержка в разных системах сборки (кроме CMake) и ОС (кроме Linux).

  • Интеграция с IDE.

… Все зависит от актуальности проекта в будущем.

Реклама

Подписывайтесь на мой канал про C++ и компиляторы: https://t.me/cxx95, где я пишу контент, который, без ложной скромности, сложно найти где-то еще =)

Поставьте звездочку у Izaron/WafflePlusPlus, если вам интересно следить за проектом!


ссылка на оригинал статьи https://habr.com/ru/post/710744/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *