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.

184 lines
7.2 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).
---
## Структура проекта
```
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
│ ├── RpcInvoker.h
│ └── 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>`)
* Построение: `msg.add<std::string>("MyService.add"); msg.add<int>(7); msg.add<int>(8);`
* Разбор: `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` из имени метода и аргументов, отправляет через `IpcChannel` и читает ответ.
* `RpcInvoker` — по имени метода (первое поле сообщения) находит зарегистрированную функцию-член и вызывает её, читая аргументы через `get<T>()`.
* **Сгенерированные обёртки**:
* `*.proxy.*` — шаблонные классы, зависящие только от абстрактного `impl` с методом `impl.callTyped<Ret>(method, args...)` и не знающие про конкретный транспорт.
* `*.skeleton.*` — используют `RpcInvoker` и принимают/возвращают `RpcValue` для диспетчеризации вызовов.
---
## Зависимости
* 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
```
В результате получаем два бинарника:
```
./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 \
src/MyService.h
```
Сгенерированные файлы попадут в:
```
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`.
2. В другом терминале — клиент:
```bash
./build/client
```
Клиент также открывает эти FIFO, но логически пишет в `fifo_to_server` и читает из `fifo_to_client` (через тот же `IpcPipeChannel`), а пользовательский код видит только вызов `MyServiceProxy`.
**Ожидаемый вывод:**
```text
RESULT: 15
```