# Minimal C++ RPC PoC with Auto-Code Generation Простой Proof-of-Concept реализации RPC для C++ с автоматической генерацией прокси и скелета по аннотациям в исходниках. Включает C-биндинги для клиентской стороны, позволяющие использовать RPC из C-приложений. Проект демонстрирует: * Парсинг исходников C++ с помощью `libclang` для поиска аннотированных классов и методов. * Автоматическую генерацию файлов: * `*.proxy.h/cpp` — клиентский прокси для вызова удалённых методов (C++). * `*.skeleton.h/cpp` — серверный скелет для приёма запросов и вызова реальных методов. * `*Client_c.h/cpp` — C-биндинги для клиентской стороны (автоматически генерируются для каждого `RPC_EXPORT` класса). * Минимальный протокол передачи данных через **именованные каналы (FIFO)**. * Поддержка только типов `int` для аргументов и возвращаемого значения (PoC). --- ## Структура проекта ```text project/ ├── CMakeLists.txt ├── README.md ├── include/ │ ├── ipc/ │ │ ├── BaseIpcMessage.h # шаблонный класс сообщения │ │ ├── IpcChannel.h │ │ ├── IpcCodec.h │ │ ├── IpcConfig.h # type alias: using IpcMessage = BaseIpcMessage │ │ ├── IpcDispatcher.h │ │ ├── IpcPipeChannel.h │ │ ├── IpcMarshaller.h │ │ └── IpcSerializer.h # сериализаторы (TextIpcSerializer) │ └── rpc/ │ ├── rpc_export.h │ ├── RpcRegistry.h # реестр RPC-объектов (скелетонов) │ ├── RpcInvoker.h # тонкий фасад над RpcRegistry │ ├── RpcValue.h │ ├── rpc_client_c.h # базовый C API для RPC клиента │ └── rpc_client_c_impl.h # внутренний заголовок для C++ (не для C) ├── src/ │ ├── client.cpp # C++ клиент │ ├── client_c.c # C клиент │ ├── MyService.cpp │ ├── MyService.h │ ├── server.cpp │ └── rpc_client_c.cpp # реализация базовой C-обёртки ├── tools/ │ ├── generate_rpc.py │ └── templates/ │ ├── proxy.cpp.j2 │ ├── proxy.h.j2 │ ├── skeleton.cpp.j2 │ ├── skeleton.h.j2 │ ├── client_c.h.j2 # шаблон для C заголовка │ └── client_c.cpp.j2 # шаблон для C реализации └── build/ # создаётся при сборке ``` --- ## Архитектура (кратко) * **Уровень IPC-сообщений**: `IpcMessage` (type alias для `BaseIpcMessage`) * Построение (PoC): `msg.add(objectId); msg.add("MyService.add"); msg.add(7); msg.add(8);` * Разбор: `auto id = msg.get(); auto name = msg.get(); auto a = msg.get(); auto b = msg.get();` * Сериализация вынесена в отдельные сериализаторы (`TextIpcSerializer` и т.д.) * Тип сырых данных параметризован через сериализатор (по умолчанию `std::string`, можно использовать `std::vector` для бинарных форматов) * Выбор сериализатора делается один раз в `IpcConfig.h` через type alias * **Уровень канала**: `IpcChannel` + `IpcPipeChannel` * `IpcChannel` — абстракция транспорта: `send(const IpcMessage&)`, `receive() -> IpcMessage`. * `IpcPipeChannel` — реализация поверх двух FIFO (`/tmp/fifo_to_server`, `/tmp/fifo_to_client`), которая внутри работает со строками, но наружу — только с `IpcMessage`. * **Уровень RPC-ядра**: * `IpcMarshaller` — собирает `IpcMessage` из идентификатора объекта (`ObjectId`), имени метода и аргументов, отправляет через `IpcChannel` и читает ответ. * `RpcRegistry` — владеет RPC-объектами (`IRpcObject`), каждому выдаётся целочисленный `ObjectId` (PoC: `int`, совместимый с `BaseIpcMessage`). * `IRpcObject` — базовый интерфейс для серверных RPC-объектов (обычно скелетоны), реализующий виртуальный `invoke(method, args)`. * `RpcInvoker` — тонкий фасад: по `ObjectId` берёт объект из `RpcRegistry` и зовёт `obj->invoke(method, args)`. * **Сгенерированные обёртки**: * `*.proxy.*` — шаблонные классы (C++), зависящие только от абстрактного `impl` с методом `impl.callTyped(method, args...)` и не знающие про конкретный транспорт. В PoC роль `impl` выполняет `IpcMarshaller`, которому при создании передаётся `ObjectId`. Для разных удалённых объектов создаются разные экземпляры маршаллера (с разными `ObjectId`). * `*.skeleton.*` — реализуют `IRpcObject`: * внутри держат ссылку/указатель на реальный объект (`MyService`); * в `invoke()` по имени метода ищут соответствующий хендлер в статической `std::unordered_map`; * хендлеры (`call_`) распаковывают `RpcArgs`, вызывают реальный метод на `MyService` и упаковывают результат в `RpcValue`. * `*Client_c.*` — C-биндинги для клиентской стороны: * автоматически генерируются для каждого класса с аннотацией `RPC_EXPORT`; * предоставляют чистый C API с функциями вида `{service}_client_create()`, `{service}_{method}()`; * все исключения преобразуются в код ошибки `-1`; * используют базовую C-обёртку `rpc_client_c.h` для работы с IPC. Так достигается поддержка **нескольких объектов одного и того же сервиса** на сервере: каждому объекту соответствует свой skeleton, зарегистрированный в `RpcRegistry` под уникальным `ObjectId`, а клиентский код адресует конкретный объект через `IpcMarshaller`, «прошитый» этим `ObjectId`. --- ## Зависимости * CMake ≥ 3.10 * GCC или Clang с поддержкой C++17 и C11 * Python 3.8+ с пакетами: ```bash pip3 install jinja2 clang ``` * libclang (для Python) Если GCC используется только для сборки — libclang нужен только для генератора. **Важно**: Сам `libclang` и пакет `clang` для Python должны быть одной версии. --- ## Аннотации для экспорта Управление тем, какие атрибуты каких классов следует экспортировать происходит с помощью аннотаций в исходном коде. См. файл [MyService.h](src/MyService.h): ```c class RPC_EXPORT MyService { public: RPC_EXPORT int add(int a, int b); int minus(int a, int b); }; ``` * Экспортируем класс `MyService`: * Экспортируем метод `MyService::add`; * Но **не** экспортируем метод `MyService::minus`. --- ## Сборка проекта 1. Конфигурируем CMake: ```bash cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -B build ``` Директива `CMAKE_EXPORT_COMPILE_COMMANDS=ON` нужна для корректного парсинга в `libclang`. Впрочем, в CMakeLists.txt эта директива уже должна быть включена. 2. Собираем проект: ```bash cmake --build build ``` В результате получаем бинарники: ```text ./build/server # сервер ./build/client # C++ клиент ./build/client_c # C клиент ./build/librpc_client_c.a # статическая библиотека C-биндингов ``` --- ## Генерация кода Автоматическая генерация прокси и скелета происходит **при сборке** через Python-скрипт `tools/generate_rpc.py`. Пример ручного запуска генератора: ```bash python3 tools/generate_rpc.py \ --out-dir build/generated \ --compile-commands build/compile_commands.json \ --templates tools/templates \ --header src/MyService.h \ --source src/MyService.cpp \ --out-base MyService ``` Сгенерированные файлы попадут в: ```text build/generated/ ├─ MyService.proxy.h ├─ MyService.proxy.cpp ├─ MyService.skeleton.h ├─ MyService.skeleton.cpp ├─ MyServiceClient_c.h # C заголовок └─ MyServiceClient_c.cpp # C реализация ``` **Примечание**: C-биндинги (`*Client_c.*`) автоматически генерируются для каждого класса с аннотацией `RPC_EXPORT`. При добавлении нового сервиса достаточно пометить его `RPC_EXPORT` — C-биндинги будут созданы автоматически при сборке. --- ## Запуск 1. В одном терминале запускаем сервер: ```bash ./build/server ``` Сервер создаёт (при необходимости) именованные пайпы `/tmp/fifo_to_server` и `/tmp/fifo_to_client`, читает запросы из `fifo_to_server` и пишет ответы в `fifo_to_client` через `IpcPipeChannel`. На стороне сервера `RpcRegistry` регистрирует два объекта `MyService`, обёрнутых в `MyServiceSkeleton`, под `ObjectId = 0` и `ObjectId = 1`. 2. В другом терминале — клиент (C++ или C): **C++ клиент:** ```bash ./build/client ``` **C клиент:** ```bash ./build/client_c ``` Оба клиента открывают те же FIFO, но логически пишут в `fifo_to_server` и читают из `fifo_to_client` (через тот же `IpcPipeChannel`). **C++ клиент** использует шаблонные прокси `MyServiceProxy`, привязанные к разным `ObjectId`. **C клиент** использует C API: ```c // Создание клиента и маршаллеров RpcClient* client = rpc_client_create("/tmp/fifo_to_client", "/tmp/fifo_to_server"); IpcMarshallerHandle* m1 = rpc_client_create_marshaller(client, 0); IpcMarshallerHandle* m2 = rpc_client_create_marshaller(client, 1); // Создание сервисных клиентов MyServiceClient* obj1 = myservice_client_create(m1); MyServiceClient* obj2 = myservice_client_create(m2); // Вызовы методов (возвращают -1 при ошибке) myservice_add(obj1, 7, 9); int counter = myservice_get(obj1); myservice_add(obj2, 0, counter); ``` **Ожидаемый вывод (одинаковый для обоих клиентов):** ```text $ ./server & $ ./client OBJ1: 16 OBJ2: 16 $ ./client OBJ1: 32 OBJ2: 48 $ ./client_c OBJ1: 48 OBJ2: 96 $ ./client_c OBJ1: 64 OBJ2: 160 ``` где: - первый объект (`ObjectId = 0`) увеличивает свой счётчик через `add(7, 9)` → счётчик = 16; - второй объект (`ObjectId = 1`) увеличивает свой счётчик на `obj1.get()`, то есть также до 16. **Обработка ошибок в C API**: все функции возвращают `-1` при ошибке (исключения преобразуются автоматически). --- ## Использование C API ### Базовая обёртка (`rpc_client_c.h`) ```c // Создание RPC клиента (обёртка над IpcPipeChannel) RpcClient* rpc_client_create(const char* read_pipe, const char* write_pipe); // Создание маршаллера для конкретного объекта IpcMarshallerHandle* rpc_client_create_marshaller(RpcClient* client, int object_id); // Освобождение ресурсов void rpc_marshaller_destroy(IpcMarshallerHandle* marshaller); void rpc_client_destroy(RpcClient* client); ``` ### Генерируемые функции для сервисов Для каждого класса с `RPC_EXPORT` автоматически генерируются функции: ```c // Создание клиента сервиса MyServiceClient* myservice_client_create(IpcMarshallerHandle* marshaller); void myservice_client_destroy(MyServiceClient* client); // Вызовы методов (возвращают -1 при ошибке) int myservice_add(MyServiceClient* client, int a, int b); int myservice_get(MyServiceClient* client); ``` **Именование функций**: имя класса преобразуется в нижний регистр (`MyService` → `myservice`), методы сохраняют оригинальное имя. **Автоматическая генерация**: при добавлении нового `RPC_EXPORT` класса C-биндинги генерируются автоматически при сборке, без необходимости изменять код генератора.