Кодогенератор это программа, которая на основе исходного кода или какого-нибудь файла настроек генерирует вспомогательный код, который потом компилируется вместе с исходным кодом. Это нужно, чтобы не писать 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, если вам интересно следить за проектом!
Комментарии (11)
ionicman
19.01.2023 09:50Можно вопрос от неспециалиста в C++?
А почему нельзя просто заинклюдить библиотеку и вызывать оттуда нужные фии?
Чем лучше писать какие-то сущности в комментариях, которые потом развернутся в код?
Izaron Автор
19.01.2023 10:16Это особенность работы с шаблонами в C++. Пусть есть объявление такой шаблонной функции:
template<typename EnumType> EnumType FromInt(int value);
Тогда, подставляя вместо
EnumType
разные типы данных (напримерFromInt<Color>(100500)
, мы получим разные функции - по одной функции на каждыйEnumType
. Шаблонная функция (и вообще шаблонный код) это просто заготовка реального кода.Вывод компилятора с двумя функциями
Таким образом, кодогенератор создает определения подобных частных функций рядом с исходниками, и затем компилятор связывает вызов шаблонной функции с конкретным определением функции.
В какой-то совсем внешней библиотеке эти определения находиться не смогут, потому что внешние библиотеки не имеют никакого понятия про типы данных в конкретной программе. Пусть внутри программы есть тип
enum Foo
, тогда программа должна сама заботиться о наличии определения функцииFromInt<Foo>
.ionicman
19.01.2023 10:35В какой-то совсем внешней библиотеке эти определения находиться не смогут
Да, но если вдруг понадобиться в этой библиотеке обратиться к какой-то шаблонной фии с каким-то типом, что мешает просто заинклюдить файл с этой фй и вызвать ее с нужным типом?
Я правильно понимаю, что у нас есть некая шаблонная функция?
Ну для примера пусть будет minimum:
template <typename T> T minimum (const T &a, const T &b) { return a < b ? a : b; }
Есть внешний файл, где она объявлена, допустим utils.cpp
Если понадобилось вызвать эту фю для какого-либо произвольного типа где-либо в другом файле, можно же сделать так:
#include "utils.cpp" typedef unsigned int MYTYPE; MYTYPE a = 5, b = 7; cout << minimum( a, b );
Или я задачу не верно понял?
Кодогенератор это программа, которая на основе исходного кода или какого-нибудь файла настроек генерирует вспомогательный код, который потом компилируется вместе с исходным кодом. Это нужно, чтобы не писать boilerplate-код (копипаст)
Или на примере того-же мока - напиши его один раз, вынеси в отдельный файл - и где надо - заинклюдь чтобы не было boilerpate-а.
Sazonov
19.01.2023 18:40+1Начните с основ. Почитайте про то что такое препроцесстрование, компиляция и линковка.
После - чем объявление функции отличается от определения. Потом про шаблонные функции. Вы найдете ответы на все ваши вопросы. Пока лишь создаётся впечатление что вы очень плохо понимаете то, о чём спрашиваете.
thevlad
20.01.2023 02:04Примерно так это и работает только темплейты обычно пишут в хидерах, чтобы можно было инстанцировать произвольными типами, в том числе не известными во время создания библиотеки.
По мне так, если будет более менее равнозначный выбор между кодом на темплейтах и кодогенератор для которого надо еще и дополнительные модули поддерживать, я всегда выберу темплейты.
Sazonov
19.01.2023 10:18Не совсем понял, по вашему развернутся в код кем и когда? И что значит заинклюдить библиотеку?
Инклюдятся заголовочные файлы библиотеки (если речь не о новомодных модулях из последнего стандарта). "Вызов функций" происходит во время работы приложения, а нам же надо во время написания кода, до компиляции.
Kelbon
19.01.2023 10:21почему нельзя просто заинклюдить библиотеку и вызывать оттуда нужные фии?
Потому что что-то нельзя сделать в рамках языка без дополнительного бойлерплейта
Helltraitor
19.01.2023 17:11+1Кодогенераторы схожи с надмножествами языков программирования. Тут, обратная совместимость (без учета генерации) сохраняется за счет комментариев, ну и это в целом аналогично кодогенераторам на Qt (как написали выше).
На данный момент возможности реализовать это в макросах нет?
P. S. Rust serde передает привет
Keeper9
Офигеть, дайте два.
Такие-то статы при таком манакосте -- это вообще законно?