From f261621c6845ad7c69f1d9e177f9b1e0236ceb30 Mon Sep 17 00:00:00 2001 From: GRayHook Date: Sun, 25 Jan 2026 13:16:29 +0700 Subject: [PATCH] implement client C binding --- CMakeLists.txt | 37 ++++++++++++- README.md | 120 +++++++++++++++++++++++++++++++++------- include/rpc/rpc_client_c.h | 33 +++++++++++ include/rpc/rpc_client_c_impl.h | 25 +++++++++ src/client_c.c | 79 ++++++++++++++++++++++++++ src/rpc_client_c.cpp | 45 +++++++++++++++ tools/generate_rpc.py | 8 +++ tools/templates/client_c.cpp.j2 | 45 +++++++++++++++ tools/templates/client_c.h.j2 | 32 +++++++++++ 9 files changed, 403 insertions(+), 21 deletions(-) create mode 100644 include/rpc/rpc_client_c.h create mode 100644 include/rpc/rpc_client_c_impl.h create mode 100644 src/client_c.c create mode 100644 src/rpc_client_c.cpp create mode 100644 tools/templates/client_c.cpp.j2 create mode 100644 tools/templates/client_c.h.j2 diff --git a/CMakeLists.txt b/CMakeLists.txt index 82d2708..7df6439 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,10 @@ cmake_minimum_required(VERSION 3.10) -project(SimpleRPCExample CXX) +project(SimpleRPCExample CXX C) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) # Generate compile_commands.json for code generation tools set(CMAKE_EXPORT_COMPILE_COMMANDS ON) @@ -26,6 +28,8 @@ function(autocode_annotated_file OUT_BASENAME HEADER SOURCE) set(out_proxy_cpp "${GENERATED_DIR}/${OUT_BASENAME}.proxy.cpp") set(out_skeleton_h "${GENERATED_DIR}/${OUT_BASENAME}.skeleton.h") set(out_skeleton_cpp "${GENERATED_DIR}/${OUT_BASENAME}.skeleton.cpp") + set(out_client_c_h "${GENERATED_DIR}/${OUT_BASENAME}Client_c.h") + set(out_client_c_cpp "${GENERATED_DIR}/${OUT_BASENAME}Client_c.cpp") add_custom_command( OUTPUT @@ -33,6 +37,8 @@ function(autocode_annotated_file OUT_BASENAME HEADER SOURCE) ${out_proxy_cpp} ${out_skeleton_h} ${out_skeleton_cpp} + ${out_client_c_h} + ${out_client_c_cpp} COMMAND ${CMAKE_COMMAND} -E echo "Generating RPC stubs for ${OUT_BASENAME}..." COMMAND ${Python3_EXECUTABLE} ${RPC_GENERATOR} --out-dir ${GENERATED_DIR} --compile-commands ${CMAKE_BINARY_DIR}/compile_commands.json @@ -53,6 +59,8 @@ function(autocode_annotated_file OUT_BASENAME HEADER SOURCE) ${out_proxy_cpp} ${out_skeleton_h} ${out_skeleton_cpp} + ${out_client_c_h} + ${out_client_c_cpp} ) endfunction() @@ -84,3 +92,30 @@ add_executable(client ) add_dependencies(client rpc_generated) + +# C bindings library +add_library(rpc_client_c STATIC + src/rpc_client_c.cpp +) + +target_include_directories(rpc_client_c PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${GENERATED_DIR} +) + +# C client +add_executable(client_c + src/client_c.c + ${GENERATED_DIR}/MyServiceClient_c.cpp +) + +target_include_directories(client_c PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${GENERATED_DIR} +) + +target_link_libraries(client_c + rpc_client_c +) + +add_dependencies(client_c rpc_generated) diff --git a/README.md b/README.md index cc692bd..578aa80 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ # Minimal C++ RPC PoC with Auto-Code Generation -Простой Proof-of-Concept реализации RPC для C++ с автоматической генерацией прокси и скелета по аннотациям в исходниках. +Простой Proof-of-Concept реализации RPC для C++ с автоматической генерацией прокси и скелета по аннотациям в исходниках. Включает C-биндинги для клиентской стороны, позволяющие использовать RPC из C-приложений. Проект демонстрирует: * Парсинг исходников C++ с помощью `libclang` для поиска аннотированных классов и методов. * Автоматическую генерацию файлов: - * `*.proxy.h/cpp` — клиентский прокси для вызова удалённых методов. + * `*.proxy.h/cpp` — клиентский прокси для вызова удалённых методов (C++). * `*.skeleton.h/cpp` — серверный скелет для приёма запросов и вызова реальных методов. + * `*Client_c.h/cpp` — C-биндинги для клиентской стороны (автоматически генерируются для каждого `RPC_EXPORT` класса). * Минимальный протокол передачи данных через **именованные каналы (FIFO)**. * Поддержка только типов `int` для аргументов и возвращаемого значения (PoC). @@ -33,19 +34,25 @@ project/ │ ├── rpc_export.h │ ├── RpcRegistry.h # реестр RPC-объектов (скелетонов) │ ├── RpcInvoker.h # тонкий фасад над RpcRegistry -│ └── RpcValue.h +│ ├── RpcValue.h +│ ├── rpc_client_c.h # базовый C API для RPC клиента +│ └── rpc_client_c_impl.h # внутренний заголовок для C++ (не для C) ├── src/ -│ ├── client.cpp +│ ├── client.cpp # C++ клиент +│ ├── client_c.c # C клиент │ ├── MyService.cpp │ ├── MyService.h -│ └── server.cpp +│ ├── server.cpp +│ └── rpc_client_c.cpp # реализация базовой C-обёртки ├── tools/ │ ├── generate_rpc.py │ └── templates/ │ ├── proxy.cpp.j2 │ ├── proxy.h.j2 │ ├── skeleton.cpp.j2 -│ └── skeleton.h.j2 +│ ├── skeleton.h.j2 +│ ├── client_c.h.j2 # шаблон для C заголовка +│ └── client_c.cpp.j2 # шаблон для C реализации └── build/ # создаётся при сборке ``` @@ -73,7 +80,7 @@ project/ * `RpcInvoker` — тонкий фасад: по `ObjectId` берёт объект из `RpcRegistry` и зовёт `obj->invoke(method, args)`. * **Сгенерированные обёртки**: - * `*.proxy.*` — шаблонные классы, зависящие только от абстрактного `impl` с методом + * `*.proxy.*` — шаблонные классы (C++), зависящие только от абстрактного `impl` с методом `impl.callTyped(method, args...)` и не знающие про конкретный транспорт. В PoC роль `impl` выполняет `IpcMarshaller`, которому при создании передаётся `ObjectId`. Для разных удалённых объектов создаются разные экземпляры маршаллера (с разными `ObjectId`). @@ -81,6 +88,11 @@ project/ * внутри держат ссылку/указатель на реальный объект (`MyService`); * в `invoke()` по имени метода ищут соответствующий хендлер в статической `std::unordered_map`; * хендлеры (`call_`) распаковывают `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`, @@ -91,7 +103,7 @@ project/ ## Зависимости * CMake ≥ 3.10 -* GCC или Clang с поддержкой C++17 +* GCC или Clang с поддержкой C++17 и C11 * Python 3.8+ с пакетами: ```bash @@ -134,7 +146,7 @@ public: cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -B build ``` -Директива `CMAKE_EXPORT_COMPILE_COMMANDS=ON` нужна для корректного парсинга в `libclang`. +Директива `CMAKE_EXPORT_COMPILE_COMMANDS=ON` нужна для корректного парсинга в `libclang`. Впрочем, в CMakeLists.txt эта директива уже должна быть включена. 2. Собираем проект: @@ -142,11 +154,13 @@ cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -B build cmake --build build ``` -В результате получаем два бинарника: +В результате получаем бинарники: ```text -./build/server -./build/client +./build/server # сервер +./build/client # C++ клиент +./build/client_c # C клиент +./build/librpc_client_c.a # статическая библиотека C-биндингов ``` --- @@ -174,9 +188,13 @@ build/generated/ ├─ MyService.proxy.h ├─ MyService.proxy.cpp ├─ MyService.skeleton.h -└─ MyService.skeleton.cpp +├─ MyService.skeleton.cpp +├─ MyServiceClient_c.h # C заголовок +└─ MyServiceClient_c.cpp # C реализация ``` +**Примечание**: C-биндинги (`*Client_c.*`) автоматически генерируются для каждого класса с аннотацией `RPC_EXPORT`. При добавлении нового сервиса достаточно пометить его `RPC_EXPORT` — C-биндинги будут созданы автоматически при сборке. + --- ## Запуск @@ -192,17 +210,41 @@ build/generated/ На стороне сервера `RpcRegistry` регистрирует два объекта `MyService`, обёрнутых в `MyServiceSkeleton`, под `ObjectId = 0` и `ObjectId = 1`. -2. В другом терминале — клиент: +2. В другом терминале — клиент (C++ или C): +**C++ клиент:** ```bash ./build/client ``` -Клиент также открывает эти FIFO, но логически пишет в `fifo_to_server` и читает из `fifo_to_client` -(через тот же `IpcPipeChannel`), а пользовательский код видит только два прокси `MyServiceProxy`, -привязанных к разным `ObjectId`. +**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 & @@ -210,12 +252,50 @@ $ ./client OBJ1: 16 OBJ2: 16 $ ./client OBJ1: 32 OBJ2: 48 -$ ./client +$ ./client_c OBJ1: 48 OBJ2: 96 -$ ./client +$ ./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-биндинги генерируются автоматически при сборке, без необходимости изменять код генератора. diff --git a/include/rpc/rpc_client_c.h b/include/rpc/rpc_client_c.h new file mode 100644 index 0000000..45674f4 --- /dev/null +++ b/include/rpc/rpc_client_c.h @@ -0,0 +1,33 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +// Непрозрачный тип для RPC клиента (обёртка над IpcPipeChannel) +typedef struct RpcClient RpcClient; + +// Непрозрачный тип для маршаллера (обёртка над IpcMarshaller) +typedef struct IpcMarshallerHandle IpcMarshallerHandle; + +// Создание RPC клиента +// read_pipe - путь к FIFO для чтения ответов +// write_pipe - путь к FIFO для отправки запросов +// Возвращает указатель на клиент или NULL при ошибке +RpcClient* rpc_client_create(const char* read_pipe, const char* write_pipe); + +// Уничтожение RPC клиента +void rpc_client_destroy(RpcClient* client); + +// Создание маршаллера для конкретного объекта +// client - RPC клиент +// object_id - идентификатор удалённого объекта +// Возвращает указатель на маршаллер или NULL при ошибке +IpcMarshallerHandle* rpc_client_create_marshaller(RpcClient* client, int object_id); + +// Уничтожение маршаллера +void rpc_marshaller_destroy(IpcMarshallerHandle* marshaller); + +#ifdef __cplusplus +} +#endif diff --git a/include/rpc/rpc_client_c_impl.h b/include/rpc/rpc_client_c_impl.h new file mode 100644 index 0000000..9c4870e --- /dev/null +++ b/include/rpc/rpc_client_c_impl.h @@ -0,0 +1,25 @@ +#pragma once + +// Внутренний заголовок для C++ файлов, использующих C-биндинги +// Не должен включаться в C файлы + +#include "ipc/IpcMarshaller.h" +#include "ipc/IpcPipeChannel.h" + +struct RpcClient { + IpcPipeChannel* channel; + + RpcClient(IpcPipeChannel* ch) : channel(ch) {} + ~RpcClient() { + delete channel; + } +}; + +struct IpcMarshallerHandle { + IpcMarshaller* marshaller; + + IpcMarshallerHandle(IpcMarshaller* m) : marshaller(m) {} + ~IpcMarshallerHandle() { + delete marshaller; + } +}; diff --git a/src/client_c.c b/src/client_c.c new file mode 100644 index 0000000..aa7bf7f --- /dev/null +++ b/src/client_c.c @@ -0,0 +1,79 @@ +#include "MyServiceClient_c.h" +#include "rpc/rpc_client_c.h" + +#include +#include +#include + +int main() { + // Создание FIFO — часть пользовательского IPC‑кода. + mkfifo("/tmp/fifo_to_server", 0666); + mkfifo("/tmp/fifo_to_client", 0666); + + // Создание RPC клиента + RpcClient* client = rpc_client_create("/tmp/fifo_to_client", "/tmp/fifo_to_server"); + if (!client) { + fprintf(stderr, "Failed to create RPC client\n"); + return 1; + } + + // Создание маршаллеров для двух объектов + // По договорённости на сервере: + // ObjectId = 0 -> первый MyService + // ObjectId = 1 -> второй MyService + IpcMarshallerHandle* m1 = rpc_client_create_marshaller(client, 0); + IpcMarshallerHandle* m2 = rpc_client_create_marshaller(client, 1); + + if (!m1 || !m2) { + fprintf(stderr, "Failed to create marshallers\n"); + rpc_client_destroy(client); + return 1; + } + + // Создание клиентов для MyService + MyServiceClient* obj1 = myservice_client_create(m1); + MyServiceClient* obj2 = myservice_client_create(m2); + + if (!obj1 || !obj2) { + fprintf(stderr, "Failed to create service clients\n"); + rpc_marshaller_destroy(m1); + rpc_marshaller_destroy(m2); + rpc_client_destroy(client); + return 1; + } + + // obj1 увеличивает свой счётчик. + int result = myservice_add(obj1, 7, 9); + if (result == -1) { + fprintf(stderr, "Error calling myservice_add on obj1\n"); + } + + // obj2 увеличивает счётчик на текущее значение счётчика obj1. + int counter1 = myservice_get(obj1); + if (counter1 == -1) { + fprintf(stderr, "Error calling myservice_get on obj1\n"); + } else { + result = myservice_add(obj2, 0, counter1); + if (result == -1) { + fprintf(stderr, "Error calling myservice_add on obj2\n"); + } + } + + int c1 = myservice_get(obj1); + int c2 = myservice_get(obj2); + + if (c1 == -1 || c2 == -1) { + fprintf(stderr, "Error getting final counters\n"); + } else { + printf("OBJ1: %d OBJ2: %d\n", c1, c2); + } + + // Освобождение ресурсов + myservice_client_destroy(obj1); + myservice_client_destroy(obj2); + rpc_marshaller_destroy(m1); + rpc_marshaller_destroy(m2); + rpc_client_destroy(client); + + return 0; +} diff --git a/src/rpc_client_c.cpp b/src/rpc_client_c.cpp new file mode 100644 index 0000000..7104e66 --- /dev/null +++ b/src/rpc_client_c.cpp @@ -0,0 +1,45 @@ +#include "rpc/rpc_client_c.h" +#include "rpc/rpc_client_c_impl.h" + +#include +#include + +extern "C" { + +RpcClient* rpc_client_create(const char* read_pipe, const char* write_pipe) { + try { + IpcPipeChannel* channel = new IpcPipeChannel(read_pipe, write_pipe); + // Проверяем, что канал открылся успешно + // (IpcPipeChannel может вывести ошибку, но не бросает исключение) + // Для простоты считаем, что если конструктор завершился, всё ОК + return new RpcClient(channel); + } catch (...) { + return nullptr; + } +} + +void rpc_client_destroy(RpcClient* client) { + if (client) { + delete client; + } +} + +IpcMarshallerHandle* rpc_client_create_marshaller(RpcClient* client, int object_id) { + if (!client || !client->channel) { + return nullptr; + } + try { + IpcMarshaller* marshaller = new IpcMarshaller(*client->channel, object_id); + return new IpcMarshallerHandle(marshaller); + } catch (...) { + return nullptr; + } +} + +void rpc_marshaller_destroy(IpcMarshallerHandle* marshaller) { + if (marshaller) { + delete marshaller; + } +} + +} // extern "C" diff --git a/tools/generate_rpc.py b/tools/generate_rpc.py index de9146b..369b627 100755 --- a/tools/generate_rpc.py +++ b/tools/generate_rpc.py @@ -262,6 +262,8 @@ def main(): proxy_cpp = env.get_template("proxy.cpp.j2") skeleton_h = env.get_template("skeleton.h.j2") skeleton_cpp = env.get_template("skeleton.cpp.j2") + client_c_h = env.get_template("client_c.h.j2") + client_c_cpp = env.get_template("client_c.cpp.j2") base = args.out_base @@ -277,6 +279,12 @@ def main(): with open(f"{out_dir}/{base}.skeleton.cpp", "w") as f: f.write(skeleton_cpp.render(cls=cls)) + with open(f"{out_dir}/{base}Client_c.h", "w") as f: + f.write(client_c_h.render(cls=cls)) + + with open(f"{out_dir}/{base}Client_c.cpp", "w") as f: + f.write(client_c_cpp.render(cls=cls)) + print("Generated files for class", cls.name, "into base", base) return 0 diff --git a/tools/templates/client_c.cpp.j2 b/tools/templates/client_c.cpp.j2 new file mode 100644 index 0000000..750d262 --- /dev/null +++ b/tools/templates/client_c.cpp.j2 @@ -0,0 +1,45 @@ +#include "{{ cls.name }}Client_c.h" +#include "rpc/rpc_client_c_impl.h" +#include "ipc/IpcMarshaller.h" + +#include + +extern "C" { + +struct {{ cls.name }}Client { + IpcMarshaller* marshaller; + + {{ cls.name }}Client(IpcMarshaller* m) : marshaller(m) {} +}; + +{{ cls.name }}Client* {{ cls.name|lower }}_client_create(IpcMarshallerHandle* marshaller) { + if (!marshaller || !marshaller->marshaller) { + return nullptr; + } + try { + return new {{ cls.name }}Client(marshaller->marshaller); + } catch (...) { + return nullptr; + } +} + +void {{ cls.name|lower }}_client_destroy({{ cls.name }}Client* client) { + if (client) { + delete client; + } +} + +{% for m in cls.methods %} +{{ m.return_type }} {{ cls.name|lower }}_{{ m.name }}({{ cls.name }}Client* client{% for a in m.args %}, {{ a.type }} {{ a.name }}{% endfor %}) { + if (!client || !client->marshaller) { + return -1; + } + try { + return client->marshaller->template callTyped<{{ m.return_type }}>("{{ cls.name }}.{{ m.name }}"{% for a in m.args %}, {{ a.name }}{% endfor %}); + } catch (...) { + return -1; + } +} +{% endfor %} + +} // extern "C" diff --git a/tools/templates/client_c.h.j2 b/tools/templates/client_c.h.j2 new file mode 100644 index 0000000..8ea6df0 --- /dev/null +++ b/tools/templates/client_c.h.j2 @@ -0,0 +1,32 @@ +#pragma once + +#include "rpc/rpc_client_c.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// Непрозрачный тип для клиента {{ cls.name }} +typedef struct {{ cls.name }}Client {{ cls.name }}Client; + +// Создание клиента {{ cls.name }} +// marshaller - маршаллер, созданный через rpc_client_create_marshaller +// Возвращает указатель на клиент или NULL при ошибке +{{ cls.name }}Client* {{ cls.name|lower }}_client_create(IpcMarshallerHandle* marshaller); + +// Уничтожение клиента {{ cls.name }} +void {{ cls.name|lower }}_client_destroy({{ cls.name }}Client* client); + +{% for m in cls.methods %} +// Вызов метода {{ m.name }} +// client - клиент {{ cls.name }} +{% for a in m.args %} +// {{ a.name }} - {{ a.type }} аргумент +{% endfor %} +// Возвращает результат или -1 при ошибке +{{ m.return_type }} {{ cls.name|lower }}_{{ m.name }}({{ cls.name }}Client* client{% for a in m.args %}, {{ a.type }} {{ a.name }}{% endfor %}); +{% endfor %} + +#ifdef __cplusplus +} +#endif