
Кодогенератор это программа, которая на основе исходного кода или какого-нибудь файла настроек генерирует вспомогательный код, который потом компилируется вместе с исходным кодом. Это нужно, чтобы не писать 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 для всех тестов.
Таким образом, получаем трехуровневое тестирование:
Изменение шаблона, данных для него и т.д. - сразу отображаются на генерируемом файле в репозитории. Нельзя закоммитить, допустив какие-то нежелательные стилистические изменения.
Получившиеся
.h/.cpp-файлы сразу компилируются. Нельзя закоммитить некомпилирующиеся сгенерированные файлы.Сгенерированные файлы после компиляции проверяются в юнит-тестах. Нельзя закоммитить поломанное поведение у сгенерированных файлов.
Как использовать кодогенератор в своем проекте
В данный момент 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, если вам интересно следить за проектом!
