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.
 
 
 
 
 
Sergey Marinkevich f261621c68 implement client C binding 1 week ago
include implement client C binding 1 week ago
src implement client C binding 1 week ago
tools implement client C binding 1 week ago
.gitignore base wo autocode 2 months ago
CMakeLists.txt implement client C binding 1 week ago
README.md implement client C binding 1 week ago

README.md

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).

Структура проекта

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+ с пакетами:

    pip3 install jinja2 clang
    
  • libclang (для Python) Если GCC используется только для сборки — libclang нужен только для генератора.

Важно: Сам libclang и пакет clang для Python должны быть одной версии.


Аннотации для экспорта

Управление тем, какие атрибуты каких классов следует экспортировать происходит с помощью аннотаций в исходном коде.

См. файл MyService.h:

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:
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -B build

Директива CMAKE_EXPORT_COMPILE_COMMANDS=ON нужна для корректного парсинга в libclang. Впрочем, в CMakeLists.txt эта директива уже должна быть включена.

  1. Собираем проект:
cmake --build build

В результате получаем бинарники:

./build/server          # сервер
./build/client          # C++ клиент
./build/client_c        # C клиент
./build/librpc_client_c.a  # статическая библиотека C-биндингов

Генерация кода

Автоматическая генерация прокси и скелета происходит при сборке через Python-скрипт tools/generate_rpc.py.

Пример ручного запуска генератора:

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

Сгенерированные файлы попадут в:

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. В одном терминале запускаем сервер:
./build/server

Сервер создаёт (при необходимости) именованные пайпы /tmp/fifo_to_server и /tmp/fifo_to_client, читает запросы из fifo_to_server и пишет ответы в fifo_to_client через IpcPipeChannel. На стороне сервера RpcRegistry регистрирует два объекта MyService, обёрнутых в MyServiceSkeleton, под ObjectId = 0 и ObjectId = 1.

  1. В другом терминале — клиент (C++ или C):

C++ клиент:

./build/client

C клиент:

./build/client_c

Оба клиента открывают те же FIFO, но логически пишут в fifo_to_server и читают из fifo_to_client (через тот же IpcPipeChannel).

C++ клиент использует шаблонные прокси MyServiceProxy, привязанные к разным ObjectId.

C клиент использует C API:

// Создание клиента и маршаллеров
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);

Ожидаемый вывод (одинаковый для обоих клиентов):

$ ./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)

// Создание 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 автоматически генерируются функции:

// Создание клиента сервиса
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);

Именование функций: имя класса преобразуется в нижний регистр (MyServicemyservice), методы сохраняют оригинальное имя.

Автоматическая генерация: при добавлении нового RPC_EXPORT класса C-биндинги генерируются автоматически при сборке, без необходимости изменять код генератора.