You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

302 lines
14 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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-биндинги генерируются автоматически при сборке, без необходимости изменять код генератора.