Compare commits

...

11 Commits

Author SHA1 Message Date
grayhook f261621c68 implement client C binding 2026-01-25 13:22:02 +07:00
Сергей Маринкевич 168b5fdfea enable export compile commands by default 2026-01-22 11:50:00 +07:00
Сергей Маринкевич 9f5969342e bump readme 2025-12-04 20:17:53 +07:00
Сергей Маринкевич 47092fc6f1 implement object registry 2025-12-04 20:03:00 +07:00
Сергей Маринкевич e7aa646a80 move marshaller to IPC component 2025-12-04 17:40:29 +07:00
Сергей Маринкевич 31d0496a93 improve example 2025-12-04 17:40:29 +07:00
Сергей Маринкевич 0334b9a42b fup zeroes from pipes 2025-12-04 17:40:29 +07:00
Сергей Маринкевич 8284a36883 disaggregate IpcMessage container and serdes 2025-12-04 13:07:55 +07:00
Сергей Маринкевич 43f67275e2 factor IPC out of server RPC 2025-12-03 18:27:08 +07:00
Сергей Маринкевич 00d359a064 Fix IpcMessage::empty so skeleton stops reading args and server replies 2025-12-03 17:42:51 +07:00
Сергей Маринкевич c93aea6501 fix build but factor IPC-dependency into Proxy again... 2025-12-02 20:57:19 +07:00
29 changed files with 1121 additions and 304 deletions
+40 -2
View File
@@ -1,8 +1,13 @@
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)
# include source includes
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src)
@@ -23,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
@@ -30,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
@@ -37,7 +46,7 @@ function(autocode_annotated_file OUT_BASENAME HEADER SOURCE)
--header ${HEADER}
--source ${SOURCE}
--out-base ${OUT_BASENAME}
DEPENDS ${RPC_GENERATOR} ${RPC_TEMPLATES} ${HEADER} ${SOURCE}
DEPENDS ${RPC_GENERATOR} ${RPC_TEMPLATES}/* ${HEADER} ${SOURCE}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Running RPC code generator for ${OUT_BASENAME}"
VERBATIM
@@ -50,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()
@@ -81,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)
+169 -45
View File
@@ -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).
@@ -15,59 +16,94 @@
## Структура проекта
```
```text
project/
├── CMakeLists.txt
├── README.md
├── src
   ├── client.cpp
   ├── common
   │   ├── ipc
   │   │   ├── IpcChannel.h
   │   │   ├── IpcMessage.h
   │   │   └── IpcPipeChannel.h
   │   ├── proxy
   │   │   └── ProxyMarshaller.h
   │   └── rpc
   │   ├── rpc_export.h
   │   ── RpcInvoker.h
   ├── MyService.cpp
   ├── MyService.h
   └── server.cpp
├── tools
├── 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
│ └── templates/
│ ├── proxy.cpp.j2
│ ├── proxy.h.j2
│ ├── skeleton.cpp.j2
── skeleton.h.j2
└─ build/ # создаётся при сборке
── skeleton.h.j2
│ ├── client_c.h.j2 # шаблон для C заголовка
│ └── client_c.cpp.j2 # шаблон для C реализации
└── build/ # создаётся при сборке
```
---
## Архитектура (кратко)
* **Уровень IPC-сообщений**: `IpcMessage`
* Построение: `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>();`
* Внутри хранится строка с простыми типовыми тегами (`i` для `int`, `s` для `std::string`), что позволяет в будущем перейти на бинарный формат без изменения API.
* **Уровень канала**: `RpcChannel` + `IpcPipeChannel`
* **Уровень 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-ядра**:
* `ProxyMarshaller` — собирает `IpcMessage` из имени метода и аргументов, отправляет через `RpcChannel` и читает ответ.
* `RpcInvoker` — по имени метода (первое поле сообщения) находит зарегистрированную функцию-член и вызывает её, читая аргументы через `get<T>()`.
* `IpcMarshaller` — собирает `IpcMessage` из идентификатора объекта (`ObjectId`), имени метода и аргументов, отправляет через `IpcChannel` и читает ответ.
* `RpcRegistry` — владеет RPC-объектами (`IRpcObject`), каждому выдаётся целочисленный `ObjectId` (PoC: `int`, совместимый с `BaseIpcMessage`).
* `IRpcObject` — базовый интерфейс для серверных RPC-объектов (обычно скелетоны), реализующий виртуальный `invoke(method, args)`.
* `RpcInvoker` — тонкий фасад: по `ObjectId` берёт объект из `RpcRegistry` и зовёт `obj->invoke(method, args)`.
* **Сгенерированные обёртки**:
* `*.proxy.*`используют `ProxyMarshaller` и `RpcChannel`, не зависят от конкретного транспорта.
* `*.skeleton.*` — используют `RpcInvoker` и принимают/возвращают `IpcMessage` для диспетчеризации вызовов.
* `*.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
* GCC или Clang с поддержкой C++17 и C11
* Python 3.8+ с пакетами:
```bash
@@ -110,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. Собираем проект:
@@ -118,11 +154,13 @@ cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -B build
cmake --build build
```
В результате получаем два бинарника:
В результате получаем бинарники:
```
./build/server
./build/client
```text
./build/server # сервер
./build/client # C++ клиент
./build/client_c # C клиент
./build/librpc_client_c.a # статическая библиотека C-биндингов
```
---
@@ -137,19 +175,26 @@ cmake --build build
python3 tools/generate_rpc.py \
--out-dir build/generated \
--compile-commands build/compile_commands.json \
src/MyService.h
--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
─ MyService.skeleton.cpp
├─ MyServiceClient_c.h # C заголовок
└─ MyServiceClient_c.cpp # C реализация
```
**Примечание**: C-биндинги (`*Client_c.*`) автоматически генерируются для каждого класса с аннотацией `RPC_EXPORT`. При добавлении нового сервиса достаточно пометить его `RPC_EXPORT` — C-биндинги будут созданы автоматически при сборке.
---
## Запуск
@@ -160,18 +205,97 @@ build/generated/
./build/server
```
Сервер создаёт (при необходимости) именованные пайпы `/tmp/fifo_to_server` и `/tmp/fifo_to_client`, читает запросы из `fifo_to_server` и пишет ответы в `fifo_to_client` через `IpcPipeChannel`.
Сервер создаёт (при необходимости) именованные пайпы `/tmp/fifo_to_server` и `/tmp/fifo_to_client`,
читает запросы из `fifo_to_server` и пишет ответы в `fifo_to_client` через `IpcPipeChannel`.
На стороне сервера `RpcRegistry` регистрирует два объекта `MyService`, обёрнутых в `MyServiceSkeleton`,
под `ObjectId = 0` и `ObjectId = 1`.
2. В другом терминале — клиент:
2. В другом терминале — клиент (C++ или C):
**C++ клиент:**
```bash
./build/client
```
Клиент также открывает эти FIFO, но логически пишет в `fifo_to_server` и читает из `fifo_to_client` (через тот же `IpcPipeChannel`), а пользовательский код видит только вызов `MyServiceProxy`.
**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
RESULT: 15
$ ./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-биндинги генерируются автоматически при сборке, без необходимости изменять код генератора.
+80
View File
@@ -0,0 +1,80 @@
#pragma once
#include <string>
#include <vector>
#include <variant>
// Базовый шаблонный класс IPC-сообщения с API add<T>() / get<T>().
// Сериализация вынесена в отдельные сериализаторы (см. IpcSerializer.h).
// Конкретный тип сообщения определяется через type alias в IpcConfig.h.
template<typename Serializer>
class BaseIpcMessage {
public:
// Тип сырых данных определяется сериализатором
using RawData = typename Serializer::RawData;
BaseIpcMessage() = default;
// Конструирование из сырых данных (тип определяется сериализатором)
explicit BaseIpcMessage(const RawData& raw) {
data_ = Serializer::deserialize(raw);
}
BaseIpcMessage(const BaseIpcMessage& other)
: data_(other.data_)
, pos_(0) {}
BaseIpcMessage& operator=(const BaseIpcMessage& other) {
if (this != &other) {
data_ = other.data_;
pos_ = 0;
}
return *this;
}
// Конструирование исходящего сообщения.
template<typename T>
void add(const T& v) {
data_.emplace_back(v);
}
// Чтение входящего сообщения по частям.
template<typename T>
T get() {
if (pos_ >= data_.size()) {
return T{}; // или throw
}
auto& v = data_[pos_++];
if constexpr (std::is_same_v<T, int>) {
if (auto* p = std::get_if<int>(&v)) {
return *p;
}
} else if constexpr (std::is_same_v<T, std::string>) {
if (auto* p = std::get_if<std::string>(&v)) {
return *p;
}
}
return T{};
}
bool empty() const {
return pos_ >= data_.size();
}
// Сериализация в сырые данные (тип определяется сериализатором)
RawData serialize() const {
return Serializer::serialize(data_);
}
// Получить сырые данные (для обратной совместимости или отладки)
RawData raw() const {
return serialize();
}
private:
std::vector<std::variant<int, std::string>> data_;
mutable std::size_t pos_{0};
};
+1 -1
View File
@@ -1,6 +1,6 @@
#pragma once
#include "IpcMessage.h"
#include "IpcConfig.h"
// Абстракция IPC‑канала, работающего с IpcMessage.
// Живёт отдельно от RPC‑кода, чтобы транспорт не зависел от конкретной RPC‑реализации.
+74
View File
@@ -0,0 +1,74 @@
#pragma once
#include <ipc/IpcConfig.h>
#include <rpc/RpcRegistry.h>
#include <rpc/RpcValue.h>
#include <string>
// Кодек, который знает, как упаковать/распаковать RPC-запросы/ответы
// в/IpcMessage. Живёт в IPC-слое, но опирается на типы RPC-ядра
// (RpcValue/RpcArgs).
namespace IpcCodec {
// Используем ObjectId из специализированного RPC-реестра.
using ObjectId = RpcRegistry::ObjectId;
// Запрос: ObjectId + имя метода + вектор аргументов.
inline IpcMessage encodeRequest(ObjectId objectId,
const std::string& method,
const RpcArgs& args) {
IpcMessage msg;
// ObjectId (PoC: приводим к int)
msg.add(static_cast<int>(objectId));
// имя метода
msg.add(method);
// аргументы (PoC: только int)
for (const auto& a : args) {
msg.add(a.asInt());
}
return msg;
}
inline void decodeRequest(const IpcMessage& msg,
ObjectId& objectId,
std::string& method,
RpcArgs& args) {
IpcMessage copy = msg;
// ObjectId (PoC: читаем как int и приводим к ObjectId)
int rawId = copy.get<int>();
objectId = static_cast<ObjectId>(rawId);
// имя метода
method = copy.get<std::string>();
// аргументы (PoC: только int, читаем до конца сообщения)
args.clear();
while (!copy.empty()) {
int v = copy.get<int>();
args.emplace_back(RpcValue::fromInt(v));
}
}
// Ответ: одно RpcValue (PoC: считаем, что это int).
inline IpcMessage encodeResponse(const RpcValue& result) {
IpcMessage msg;
msg.add(result.asInt()); // PoC: только int
return msg;
}
inline RpcValue decodeResponse(const IpcMessage& msg) {
IpcMessage copy = msg;
int v = copy.get<int>();
return RpcValue::fromInt(v);
}
} // namespace IpcCodec
+21
View File
@@ -0,0 +1,21 @@
#pragma once
// Конфигурация IPC: выбор конкретного сериализатора
// В один момент времени используется только один сериализатор,
// выбор делается через type alias для конкретного типа IpcMessage.
#include "BaseIpcMessage.h"
#include "IpcSerializer.h"
// По умолчанию используем текстовый сериализатор
// Для переключения на JSON достаточно заменить на:
// using IpcMessage = BaseIpcMessage<JsonIpcSerializer>;
using IpcMessage = BaseIpcMessage<TextIpcSerializer>;
// Альтернативный вариант через макрос (если нужен compile-time выбор):
// #ifdef USE_JSON_SERIALIZER
// using IpcMessage = BaseIpcMessage<JsonIpcSerializer>;
// #else
// using IpcMessage = BaseIpcMessage<TextIpcSerializer>;
// #endif
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#include <ipc/IpcChannel.h>
#include <ipc/IpcCodec.h>
#include <rpc/RpcInvoker.h>
#include <string>
#include <iostream>
#include <unistd.h>
// Серверный диспетчер, который получает IpcMessage с канала,
// декодирует его в RPC-вызов, вызывает RpcInvoker и шлёт ответ.
class IpcDispatcher {
public:
IpcDispatcher(IpcChannel& ch, RpcInvoker& invoker)
: channel_(ch)
, invoker_(invoker) {}
// Обработать один запрос. Возвращает false, если получили "пустое"
// сообщение и цикл стоит завершить.
bool handleOnce() {
IpcMessage req = channel_.receive();
if (req.empty()) {
return false;
}
IpcCodec::ObjectId objectId;
std::string method;
RpcArgs args;
IpcCodec::decodeRequest(req, objectId, method, args);
RpcValue result = invoker_.dispatch(objectId, method, args);
IpcMessage resp = IpcCodec::encodeResponse(result);
channel_.send(resp);
return true;
}
// Простой цикл обработки до тех пор, пока канал не вернёт пустое сообщение.
void loop() {
while (handleOnce()) {
}
}
private:
IpcChannel& channel_;
RpcInvoker& invoker_;
};
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include <ipc/IpcChannel.h>
#include <ipc/IpcCodec.h>
#include <rpc/RpcValue.h>
// Маршаллер, который знает, как превратить типизированный RPC-вызов
// в IpcMessage и обратно. Живёт в IPC-слое и опирается на IpcChannel
// и IpcCodec, но снаружи предъявляет только callTyped<T>(...).
class IpcMarshaller {
public:
explicit IpcMarshaller(IpcChannel& ch, IpcCodec::ObjectId objectId = 0)
: channel(ch)
, objectId_(objectId) {}
// Базовый type-erased вызов: принимает вектор RpcValue и возвращает RpcValue.
RpcValue call(const std::string& method, const RpcArgs& args) {
// упаковать запрос в IpcMessage
IpcMessage msg = IpcCodec::encodeRequest(objectId_, method, args);
// отправить
channel.send(msg);
// получить ответ и распаковать
IpcMessage resp = channel.receive();
return IpcCodec::decodeResponse(resp);
}
// Типизированный хелпер: контрактом является только наличие этого метода.
template<typename Ret, typename... Args>
Ret callTyped(const std::string& method, const Args&... args) {
RpcArgs packed;
packed.reserve(sizeof...(Args));
(packed.emplace_back(RpcValue::fromInt(args)), ...); // PoC: только int
RpcValue r = call(method, packed);
// PoC: Ret == int
return static_cast<Ret>(r.asInt());
}
private:
IpcChannel& channel;
IpcCodec::ObjectId objectId_;
};
-106
View File
@@ -1,106 +0,0 @@
#pragma once
#include <sstream>
#include <string>
// Примитивное IPC‑сообщение с API add<T>() / get<T>().
// Под капотом пока текстовый формат с типовыми тегами, но снаружи интерфейс не завязан на std::string.
class IpcMessage {
public:
IpcMessage() = default;
explicit IpcMessage(const std::string& raw)
: raw_(raw) {}
IpcMessage(const IpcMessage& other)
: raw_(other.raw_) {}
IpcMessage& operator=(const IpcMessage& other) {
if (this != &other) {
raw_ = other.raw_;
in_.clear();
in_initialized_ = false;
out_.str(std::string{});
}
return *this;
}
// Конструирование исходящего сообщения.
template<typename T>
void add(const T& v);
// Чтение входящего сообщения по частям.
template<typename T>
T get();
const std::string& raw() const {
return raw_;
}
bool empty() const {
return raw_.empty();
}
private:
std::string raw_;
std::ostringstream out_;
std::istringstream in_;
bool in_initialized_{false};
void ensureInput() {
if (!in_initialized_) {
in_.str(raw_);
in_initialized_ = true;
}
}
};
// ===== Специализации по типам =====
// int
template<>
inline void IpcMessage::add<int>(const int& v) {
out_ << 'i' << ' ' << v << ' ';
raw_ = out_.str();
}
template<>
inline int IpcMessage::get<int>() {
ensureInput();
char tag{};
in_ >> tag;
// в PoC просто доверяем, что тип совпадает
int v{};
in_ >> v;
return v;
}
// std::string
template<>
inline void IpcMessage::add<std::string>(const std::string& v) {
// формат: 's' <len> <bytes...>
out_ << 's' << ' ' << v.size() << ' ';
out_.write(v.data(), static_cast<std::streamsize>(v.size()));
out_ << ' ';
raw_ = out_.str();
}
template<>
inline std::string IpcMessage::get<std::string>() {
ensureInput();
char tag{};
in_ >> tag;
// ожидаем 's'
std::size_t len{};
in_ >> len;
// съесть одиночный пробел перед данными
in_.get();
std::string res(len, '\0');
in_.read(&res[0], static_cast<std::streamsize>(len));
// съесть завершающий пробел
in_.get();
return res;
}
+78 -9
View File
@@ -5,6 +5,8 @@
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <iostream>
// IPC‑канал поверх именованных pipe.
// Инкапсулирует работу с файловыми дескрипторами и обмен сообщениями IpcMessage.
@@ -18,6 +20,31 @@ public:
// При этом логически читаем только из readPipe, а пишем только в writePipe.
fdIn_ = ::open(readPipe, O_RDWR);
fdOut_ = ::open(writePipe, O_RDWR);
// Проверяем, что каналы открыты успешно
if (fdIn_ < 0) {
std::cerr << "Failed to open read pipe: " << readPipe << " (errno: " << errno << ")" << std::endl;
}
if (fdOut_ < 0) {
std::cerr << "Failed to open write pipe: " << writePipe << " (errno: " << errno << ")" << std::endl;
}
// Очищаем канал для чтения от остаточных данных
// Это важно, чтобы избежать чтения старых сообщений при повторном открытии канала
if (fdIn_ >= 0) {
char buf[4096];
// Устанавливаем неблокирующий режим для очистки
int flags = ::fcntl(fdIn_, F_GETFL);
::fcntl(fdIn_, F_SETFL, flags | O_NONBLOCK);
// Читаем все доступные данные (старые сообщения)
while (::read(fdIn_, buf, sizeof(buf)) > 0) {
// Просто выбрасываем старые данные
}
// Возвращаем блокирующий режим
::fcntl(fdIn_, F_SETFL, flags);
}
}
~IpcPipeChannel() override {
@@ -33,22 +60,64 @@ public:
if (fdOut_ < 0) {
return;
}
const std::string& data = msg.raw();
::write(fdOut_, data.c_str(), data.size());
::write(fdOut_, "\n", 1);
auto raw = msg.serialize(); // тип определяется сериализатором
// Для текстовых форматов (std::string):
if constexpr (std::is_same_v<typename IpcMessage::RawData, std::string>) {
const std::string& data = raw;
::write(fdOut_, data.c_str(), data.size());
::write(fdOut_, "\n", 1);
}
// Можно добавить другие форматы по мере необходимости
}
IpcMessage receive() override {
if (fdIn_ < 0) {
return IpcMessage{};
}
char buf[4096];
const int n = ::read(fdIn_, buf, sizeof(buf) - 1);
if (n <= 0) {
return IpcMessage{};
// Для текстовых форматов:
if constexpr (std::is_same_v<typename IpcMessage::RawData, std::string>) {
std::string line;
char c;
// Читаем построчно до символа новой строки
while (true) {
const int n = ::read(fdIn_, &c, 1);
if (n < 0) {
// Ошибка чтения
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// Неблокирующий режим и нет данных - это нормально, возвращаем пустое сообщение
return IpcMessage{};
}
// Другая ошибка - логируем и возвращаем пустое сообщение
std::cerr << "read() error: " << errno << std::endl;
return IpcMessage{};
}
if (n == 0) {
// EOF - канал закрыт на стороне записи или нет открытых дескрипторов записи
// Для именованных каналов это может означать, что клиент еще не открыл канал для записи
// В этом случае read() должен блокироваться, но если канал открыт в O_RDWR,
// то read() может вернуть 0 немедленно
// Если мы уже прочитали что-то, возвращаем это, иначе пустое сообщение
if (line.empty()) {
return IpcMessage{};
}
break; // EOF, но есть данные - возвращаем их
}
if (c == '\n') {
break; // Достигли конца строки
}
line += c;
}
if (line.empty()) {
return IpcMessage{};
}
return IpcMessage(line);
}
buf[n] = 0;
return IpcMessage(std::string(buf));
// Можно добавить другие форматы по мере необходимости
return IpcMessage{};
}
private:
+102
View File
@@ -0,0 +1,102 @@
#pragma once
#include <string>
#include <vector>
#include <variant>
#include <sstream>
// Сериализаторы для IPC-сообщений.
// Каждый сериализатор определяет:
// - RawData - тип сырых (сериализованных) данных
// - serialize() - сериализация данных в RawData
// - deserialize() - десериализация из RawData
// ===== Текстовый сериализатор (текущий формат) =====
struct TextIpcSerializer {
// Тип сырых данных - строка (текстовый формат)
using RawData = std::string;
static RawData serialize(const std::vector<std::variant<int, std::string>>& data) {
std::ostringstream out;
for (const auto& v : data) {
std::visit([&out](const auto& val) {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, int>) {
out << 'i' << ' ' << val << ' ';
} else if constexpr (std::is_same_v<T, std::string>) {
out << 's' << ' ' << val.size() << ' ';
out.write(val.data(), static_cast<std::streamsize>(val.size()));
out << ' ';
}
}, v);
}
return out.str();
}
static std::vector<std::variant<int, std::string>> deserialize(const RawData& raw) {
std::vector<std::variant<int, std::string>> result;
std::istringstream in(raw);
while (in.peek() != EOF) {
char tag{};
in >> tag;
if (tag == 'i') {
int v{};
in >> v;
result.emplace_back(v);
} else if (tag == 's') {
std::size_t len{};
in >> len;
in.get(); // пробел
std::string str(len, '\0');
in.read(&str[0], static_cast<std::streamsize>(len));
in.get(); // завершающий пробел
result.emplace_back(std::move(str));
}
}
return result;
}
};
// ===== JSON сериализатор (пример для будущего использования, например yami4) =====
struct JsonIpcSerializer {
// Тип сырых данных - строка (JSON - текстовый формат)
using RawData = std::string;
static RawData serialize(const std::vector<std::variant<int, std::string>>& data) {
// Упрощенный JSON формат: ["i:42", "s:5:hello"]
// В реальности использовать библиотеку JSON
std::ostringstream out;
out << "[";
bool first = true;
for (const auto& v : data) {
if (!first) out << ",";
first = false;
std::visit([&out](const auto& val) {
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, int>) {
out << "\"i:" << val << "\"";
} else if constexpr (std::is_same_v<T, std::string>) {
// Экранирование для JSON (упрощенно)
out << "\"s:" << val.size() << ":";
for (char c : val) {
if (c == '"' || c == '\\') out << '\\';
out << c;
}
out << "\"";
}
}, v);
}
out << "]";
return out.str();
}
static std::vector<std::variant<int, std::string>> deserialize(const RawData& raw) {
// Упрощенный парсинг JSON (в реальности использовать библиотеку JSON)
std::vector<std::variant<int, std::string>> result;
// TODO: реализовать парсинг JSON
return result;
}
};
-46
View File
@@ -1,46 +0,0 @@
#pragma once
#include <ipc/IpcChannel.h>
#include <rpc/RpcValue.h>
class ProxyMarshaller {
public:
explicit ProxyMarshaller(IpcChannel& ch) : channel(ch) {}
// Базовый type-erased вызов: принимает вектор RpcValue и возвращает RpcValue.
RpcValue call(const std::string& method, const RpcArgs& args) {
IpcMessage msg;
// имя метода
msg.add(method);
// аргументы (PoC: только int)
for (const auto& a : args) {
msg.add(a.asInt());
}
// отправить
channel.send(msg);
// получить ответ
IpcMessage resp = channel.receive();
return RpcValue::fromInt(resp.get<int>());
}
// Удобный шаблонный хелпер для сгенерированных прокси.
template<typename Ret, typename... Args>
Ret callTyped(const std::string& method, const Args&... args) {
RpcArgs packed;
packed.reserve(sizeof...(Args));
(packed.emplace_back(RpcValue::fromInt(args)), ...); // PoC: только int
RpcValue r = call(method, packed);
// PoC: Ret == int
return static_cast<Ret>(r.asInt());
}
private:
IpcChannel& channel;
};
+13 -36
View File
@@ -1,56 +1,33 @@
#pragma once
#include <rpc/RpcRegistry.h>
#include <rpc/RpcValue.h>
#include <functional>
#include <string>
#include <tuple>
#include <unordered_map>
// Инвокер — тонкий фасад над RpcRegistry:
// по ObjectId находит объект и делегирует вызов его IRpcObject::invoke().
class RpcInvoker {
public:
template<typename Obj, typename Ret, typename... Args>
void registerMethod(Obj* instance,
const std::string& name,
Ret (Obj::*method)(Args...)) {
handlers[name] =
[instance, method](const RpcArgs& args) -> RpcValue {
auto tuple = readArgs<Args...>(args);
Ret result = std::apply(
method, std::tuple_cat(std::make_tuple(instance), tuple));
using ObjectId = RpcRegistry::ObjectId; // согласован с IpcCodec::ObjectId
// PoC: считаем, что Ret == int.
return RpcValue::fromInt(result);
};
}
explicit RpcInvoker(RpcRegistry& registry)
: registry_(registry) {}
RpcValue dispatch(const std::string& method, const RpcArgs& args) const {
auto it = handlers.find(method);
if (it == handlers.end()) {
RpcValue dispatch(ObjectId objectId,
const std::string& method,
const RpcArgs& args) const {
IRpcObject* obj = registry_.get(objectId);
if (!obj) {
// PoC: в случае ошибки возвращаем 0.
return RpcValue::fromInt(0);
}
return it->second(args);
return obj->invoke(method, args);
}
private:
template<typename... Args, std::size_t... I>
static std::tuple<Args...>
readArgsImpl(const RpcArgs& args, std::index_sequence<I...>) {
return std::tuple<Args...>{
static_cast<Args>(args[I].asInt())...}; // PoC: только int
}
template<typename... Args>
static std::tuple<Args...> readArgs(const RpcArgs& args) {
return readArgsImpl<Args...>(
args, std::make_index_sequence<sizeof...(Args)>{});
}
std::unordered_map<std::string,
std::function<RpcValue(const RpcArgs&)>>
handlers;
RpcRegistry& registry_;
};
+61
View File
@@ -0,0 +1,61 @@
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include <type_traits>
#include <unordered_map>
#include <rpc/RpcValue.h>
// Базовый интерфейс для всех RPC-объектов, живущих в реестре.
// Каждая реализация (обычно *Skeleton) знает, как вызвать реальные методы.
struct IRpcObject {
virtual ~IRpcObject() = default;
virtual RpcValue invoke(const std::string& method,
const RpcArgs& args) = 0;
};
// Специализированный реестр объектов именно для RPC-уровня.
// Владеет скелетонами (IRpcObject) через unique_ptr. Сами доменные объекты
// (MyService и т.п.) живут снаружи и могут быть обёрнуты в shared_ptr.
class RpcRegistry {
public:
// PoC: ObjectId храним как int-совместимый тип, чтобы его можно было
// передавать через BaseIpcMessage, который поддерживает только int/string.
using ObjectId = int;
RpcRegistry() = default;
// Зарегистрировать объект-обёртку (обычно Skeleton) и вернуть его
// идентификатор. По сути, это registerSkeleton<T>(...).
template<typename T, typename... Args>
ObjectId registerObject(Args&&... args) {
static_assert(std::is_base_of_v<IRpcObject, T>,
"T must inherit IRpcObject");
const ObjectId id = nextId_++;
objects_.emplace(
id, std::make_unique<T>(std::forward<Args>(args)...));
return id;
}
// Уничтожить объект по идентификатору.
void destroyObject(ObjectId id) {
objects_.erase(id);
}
// Найти объект по идентификатору.
IRpcObject* get(ObjectId id) const {
auto it = objects_.find(id);
return it == objects_.end() ? nullptr : it->second.get();
}
private:
std::unordered_map<ObjectId, std::unique_ptr<IRpcObject>> objects_;
ObjectId nextId_ = 0; // первый объект будет иметь id = 0 (PoC)
};
+33
View File
@@ -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
+25
View File
@@ -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;
}
};
+6 -1
View File
@@ -1,5 +1,10 @@
#include "MyService.h"
int MyService::add(int a, int b) {
return a + b;
counter_ += (a + b);
return counter_;
}
int MyService::get() {
return counter_;
}
+5
View File
@@ -5,8 +5,13 @@
// annotate with clang attribute or via comment annotation recognized by libclang
// Use ANNOTATE attribute supported by clang: __attribute__((annotate("export")))
class RPC_EXPORT MyService {
private:
int counter_ = 0;
public:
RPC_EXPORT
int add(int a, int b);
RPC_EXPORT
int get();
int minus(int a, int b);
};
+18 -6
View File
@@ -1,6 +1,6 @@
#include "MyService.proxy.h"
#include "ipc/IpcPipeChannel.h"
#include "proxy/ProxyMarshaller.h"
#include "ipc/IpcMarshaller.h"
#include <sys/stat.h>
@@ -16,10 +16,22 @@ int main() {
// и читает из fifo, в который пишет сервер (fifo_to_client).
IpcPipeChannel ch("/tmp/fifo_to_client", "/tmp/fifo_to_server");
// RPC‑уровень: создаём marshaller поверх канала и передаём его в прокси.
ProxyMarshaller marshaller(ch);
MyServiceProxy proxy(marshaller);
// RPC‑уровень: создаём два маршаллера с разными ObjectId и два прокси.
// По договорённости на сервере:
// ObjectId = 0 -> первый MyService
// ObjectId = 1 -> второй MyService
IpcMarshaller m1(ch, 0);
IpcMarshaller m2(ch, 1);
MyServiceProxy<IpcMarshaller> obj1(m1);
MyServiceProxy<IpcMarshaller> obj2(m2);
int r = proxy.add(7, 9);
std::cout << "RESULT: " << r << std::endl;
// obj1 увеличивает свой счётчик.
obj1.add(7, 9);
// obj2 увеличивает счётчик на текущее значение счётчика obj1.
obj2.add(0, obj1.get());
int c1 = obj1.get();
int c2 = obj2.get();
std::cout << "OBJ1: " << c1 << " OBJ2: " << c2 << std::endl;
}
+79
View File
@@ -0,0 +1,79 @@
#include "MyServiceClient_c.h"
#include "rpc/rpc_client_c.h"
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
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;
}
+45
View File
@@ -0,0 +1,45 @@
#include "rpc/rpc_client_c.h"
#include "rpc/rpc_client_c_impl.h"
#include <stdexcept>
#include <cstring>
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"
+20 -11
View File
@@ -2,6 +2,9 @@
#include "MyService.skeleton.h"
#include "ipc/IpcPipeChannel.h"
#include "ipc/IpcDispatcher.h"
#include "rpc/RpcInvoker.h"
#include "rpc/RpcRegistry.h"
#include <sys/stat.h>
@@ -14,17 +17,23 @@ int main() {
// Сервер читает из fifo_to_server и пишет в fifo_to_client.
IpcPipeChannel ch("/tmp/fifo_to_server", "/tmp/fifo_to_client");
// RPC‑уровень: скелет поверх того же канала.
MyService realObj;
MyServiceSkeleton skeleton(realObj);
// RPC‑уровень: реестр объектов и инвокер, который знает, как их вызывать.
RpcRegistry registry;
RpcInvoker invoker(registry);
while (true) {
IpcMessage req = ch.receive();
if (req.empty()) {
break;
}
IpcMessage resp = skeleton.dispatch(req);
ch.send(resp);
}
// PoC: создаём два "реальных" объекта и оборачиваем их в Skeleton'ы,
// которыми владеет RpcRegistry.
MyService obj1;
MyService obj2;
RpcRegistry::ObjectId id1 =
registry.registerObject<MyServiceSkeleton>(obj1); // id1 == 0
RpcRegistry::ObjectId id2 =
registry.registerObject<MyServiceSkeleton>(obj2); // id2 == 1
(void)id1;
(void)id2;
// IPC‑диспетчер, который декодирует IpcMessage в RPC-вызовы и обратно.
IpcDispatcher dispatcher(ch, invoker);
dispatcher.loop();
}
+8
View File
@@ -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
+45
View File
@@ -0,0 +1,45 @@
#include "{{ cls.name }}Client_c.h"
#include "rpc/rpc_client_c_impl.h"
#include "ipc/IpcMarshaller.h"
#include <stdexcept>
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"
+32
View File
@@ -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
+3 -9
View File
@@ -1,11 +1,5 @@
#include "{{ cls.name }}.proxy.h"
{{ cls.name }}Proxy::{{ cls.name }}Proxy(ProxyMarshaller& marshaller)
: impl(marshaller) {}
{% for m in cls.methods %}
{{ m.return_type }} {{ cls.name }}Proxy::{{ m.name }}({% for a in m.args %}{{ a.type }} {{ a.name }}{% if not loop.last %}, {% endif %}{% endfor %}) {
return impl.callTyped<{{ m.return_type }}>("{{ cls.name }}.{{ m.name }}"{% for a in m.args %}, {{ a.name }}{% endfor %});
}
{% endfor %}
// Реализация шаблонного прокси целиком находится в заголовочном файле.
// Этот cpp остаётся пустым, чтобы сгенерированные файлы по‑прежнему
// могли участвовать в сборке как отдельная единица трансляции.
+14 -4
View File
@@ -1,14 +1,24 @@
#pragma once
#include "proxy/ProxyMarshaller.h"
#include "{{ cls.name }}.h"
// Шаблонный прокси, который зависит только от контракта Impl:
// Impl должен предоставлять метод
// template<typename Ret, typename... Args>
// Ret callTyped(const std::string& method, const Args&... args);
template<typename Impl>
class {{ cls.name }}Proxy {
public:
explicit {{ cls.name }}Proxy(ProxyMarshaller& marshaller);
explicit {{ cls.name }}Proxy(Impl& impl)
: impl(impl) {}
{% for m in cls.methods %}
{{ m.return_type }} {{ m.name }}({% for a in m.args %}{{ a.type }} {{ a.name }}{% if not loop.last %}, {% endif %}{% endfor %});
{{ m.return_type }} {{ m.name }}({% for a in m.args %}{{ a.type }} {{ a.name }}{% if not loop.last %}, {% endif %}{% endfor %}) {
return impl.template callTyped<{{ m.return_type }}>("{{ cls.name }}.{{ m.name }}"{% for a in m.args %}, {{ a.name }}{% endfor %});
}
{% endfor %}
private:
ProxyMarshaller& impl;
Impl& impl;
};
+33 -24
View File
@@ -1,32 +1,41 @@
#include "{{ cls.name }}.skeleton.h"
{{ cls.name }}Skeleton::{{ cls.name }}Skeleton({{ cls.name }}& obj)
{
{% for m in cls.methods %}
invoker.registerMethod(&obj,
"{{ cls.name }}.{{ m.name }}",
&{{ cls.name }}::{{ m.name }});
{% endfor %}
: obj_(obj) {
}
IpcMessage {{ cls.name }}Skeleton::dispatch(const IpcMessage& req) {
// Перепаковываем IpcMessage в RpcArgs и вызываем type-erased инвокер.
IpcMessage msg = req;
// имя метода
std::string method = msg.get<std::string>();
// аргументы (PoC: только int, читаем все до конца сообщения)
RpcArgs args;
while (!msg.empty()) {
int v = msg.get<int>();
args.emplace_back(RpcValue::fromInt(v));
RpcValue {{ cls.name }}Skeleton::invoke(const std::string& method,
const RpcArgs& args) {
const auto& table = handlers();
auto it = table.find(method);
if (it == table.end()) {
// Неизвестный метод — PoC: возвращаем 0.
return RpcValue::fromInt(0);
}
RpcValue result = invoker.dispatch(method, args);
IpcMessage resp;
resp.add(result.asInt()); // PoC: только int
return resp;
Handler fn = it->second;
return (this->*fn)(args);
}
const std::unordered_map<std::string, {{ cls.name }}Skeleton::Handler>&
{{ cls.name }}Skeleton::handlers() {
static const std::unordered_map<std::string, Handler> kHandlers = {
{% for m in cls.methods %}
{ "{{ cls.name }}.{{ m.name }}", &{{ cls.name }}Skeleton::call_{{ m.name }} }{% if not loop.last %},{% endif %}
{% endfor %}
};
return kHandlers;
}
{% for m in cls.methods %}
RpcValue {{ cls.name }}Skeleton::call_{{ m.name }}(const RpcArgs& args) {
// PoC: единственный тип аргументов и результата — int.
int idx = 0;
{% for a in m.args %}
int {{ a.name }} = args[idx++].asInt();
{% endfor %}
int result = obj_.{{ m.name }}({% for a in m.args %}{{ a.name }}{% if not loop.last %}, {% endif %}{% endfor %});
return RpcValue::fromInt(result);
}
{% endfor %}
+19 -4
View File
@@ -1,14 +1,29 @@
#pragma once
#include "{{ cls.name }}.h"
#include "rpc/RpcInvoker.h"
#include "rpc/RpcRegistry.h"
#include "rpc/RpcValue.h"
class {{ cls.name }}Skeleton {
#include <string>
#include <unordered_map>
// Skeleton-обёртка над {{ cls.name }}, реализующая IRpcObject::invoke().
class {{ cls.name }}Skeleton : public IRpcObject {
public:
explicit {{ cls.name }}Skeleton({{ cls.name }}& obj);
IpcMessage dispatch(const IpcMessage& req);
RpcValue invoke(const std::string& method,
const RpcArgs& args) override;
private:
RpcInvoker invoker;
using Handler = RpcValue ({{ cls.name }}Skeleton::*)(const RpcArgs&);
// Статическая таблица method-name -> member-function.
static const std::unordered_map<std::string, Handler>& handlers();
{% for m in cls.methods %}
RpcValue call_{{ m.name }}(const RpcArgs& args);
{% endfor %}
{{ cls.name }}& obj_;
};