|
|
# 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<TextIpcSerializer>
|
|
|
│ │ ├── 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<TextIpcSerializer>`)
|
|
|
* Построение (PoC):
|
|
|
`msg.add<int>(objectId); msg.add<std::string>("MyService.add"); msg.add<int>(7); msg.add<int>(8);`
|
|
|
* Разбор:
|
|
|
`auto id = msg.get<int>(); auto name = msg.get<std::string>(); auto a = msg.get<int>(); auto b = msg.get<int>();`
|
|
|
* Сериализация вынесена в отдельные сериализаторы (`TextIpcSerializer` и т.д.)
|
|
|
* Тип сырых данных параметризован через сериализатор (по умолчанию `std::string`, можно использовать `std::vector<std::byte>` для бинарных форматов)
|
|
|
* Выбор сериализатора делается один раз в `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<Ret>(method, args...)` и не знающие про конкретный транспорт.
|
|
|
В PoC роль `impl` выполняет `IpcMarshaller`, которому при создании передаётся `ObjectId`.
|
|
|
Для разных удалённых объектов создаются разные экземпляры маршаллера (с разными `ObjectId`).
|
|
|
* `*.skeleton.*` — реализуют `IRpcObject`:
|
|
|
* внутри держат ссылку/указатель на реальный объект (`MyService`);
|
|
|
* в `invoke()` по имени метода ищут соответствующий хендлер в статической `std::unordered_map<std::string, Handler>`;
|
|
|
* хендлеры (`call_<method>`) распаковывают `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-биндинги генерируются автоматически при сборке, без необходимости изменять код генератора.
|