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.

222 lines
9.7 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++ с помощью `libclang` для поиска аннотированных классов и методов.
* Автоматическую генерацию файлов:
* `*.proxy.h/cpp` — клиентский прокси для вызова удалённых методов.
* `*.skeleton.h/cpp` — серверный скелет для приёма запросов и вызова реальных методов.
* Минимальный протокол передачи данных через **именованные каналы (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
├── src/
│ ├── client.cpp
│ ├── MyService.cpp
│ ├── MyService.h
│ └── server.cpp
├── tools/
│ ├── generate_rpc.py
│ └── templates/
│ ├── proxy.cpp.j2
│ ├── proxy.h.j2
│ ├── skeleton.cpp.j2
│ └── skeleton.h.j2
└── 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.*` — шаблонные классы, зависящие только от абстрактного `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`.
Так достигается поддержка **нескольких объектов одного и того же сервиса** на сервере:
каждому объекту соответствует свой skeleton, зарегистрированный в `RpcRegistry` под уникальным `ObjectId`,
а клиентский код адресует конкретный объект через `IpcMarshaller`, «прошитый» этим `ObjectId`.
---
## Зависимости
* CMake ≥ 3.10
* GCC или Clang с поддержкой C++17
* 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`.
2. Собираем проект:
```bash
cmake --build build
```
В результате получаем два бинарника:
```text
./build/server
./build/client
```
---
## Генерация кода
Автоматическая генерация прокси и скелета происходит **при сборке** через 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
```
---
## Запуск
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. В другом терминале — клиент:
```bash
./build/client
```
Клиент также открывает эти FIFO, но логически пишет в `fifo_to_server` и читает из `fifo_to_client`
(через тот же `IpcPipeChannel`), а пользовательский код видит только два прокси `MyServiceProxy`,
привязанных к разным `ObjectId`.
**Ожидаемый вывод:**
```text
$ ./server &
$ ./client
OBJ1: 16 OBJ2: 16
$ ./client
OBJ1: 32 OBJ2: 48
$ ./client
OBJ1: 48 OBJ2: 96
$ ./client
OBJ1: 64 OBJ2: 160
```
где:
- первый объект (`ObjectId = 0`) увеличивает свой счётчик через `add(7, 9)` → счётчик = 16;
- второй объект (`ObjectId = 1`) увеличивает свой счётчик на `obj1.get()`, то есть также до 16.