Compare commits

..

No commits in common. 'e7aa646a8041b7dc4f8906b1b386c7c349a7b0cb' and '43f67275e28b3dbdb0dde85868707130904f3f50' have entirely different histories.

@ -19,54 +19,48 @@
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/
├── 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
│ ├── generate_rpc.py
│ └── templates/
│ └── templates
│ ├── proxy.cpp.j2
│ ├── proxy.h.j2
│ ├── skeleton.cpp.j2
│ └── skeleton.h.j2
└─ build/ # создаётся при сборке
└─ build/ # создаётся при сборке
```
---
## Архитектура (кратко)
* **Уровень IPC-сообщений**: `IpcMessage` (type alias для `BaseIpcMessage<TextIpcSerializer>`)
* **Уровень 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>();`
* Сериализация вынесена в отдельные сериализаторы (`TextIpcSerializer` и т.д.)
* Тип сырых данных параметризован через сериализатор (по умолчанию `std::string`, можно использовать `std::vector<std::byte>` для бинарных форматов)
* Выбор сериализатора делается один раз в `IpcConfig.h` через type alias
* **Уровень канала**: `IpcChannel` + `IpcPipeChannel`
* Внутри хранится строка с простыми типовыми тегами (`i` для `int`, `s` для `std::string`), что позволяет в будущем перейти на бинарный формат без изменения API.
* **Уровень канала**: `RpcChannel` + `IpcPipeChannel`
* `IpcChannel` — абстракция транспорта: `send(const IpcMessage&)`, `receive() -> IpcMessage`.
* `IpcPipeChannel` — реализация поверх двух FIFO (`/tmp/fifo_to_server`, `/tmp/fifo_to_client`), которая внутри работает со строками, но наружу — только с `IpcMessage`.
* **Уровень RPC-ядра**:
* `IpcMarshaller` — собирает `IpcMessage` из имени метода и аргументов, отправляет через `IpcChannel` и читает ответ.
* `ProxyMarshaller` — собирает `IpcMessage` из имени метода и аргументов, отправляет через `RpcChannel` и читает ответ.
* `RpcInvoker` — по имени метода (первое поле сообщения) находит зарегистрированную функцию-член и вызывает её, читая аргументы через `get<T>()`.
* **Сгенерированные обёртки**:
* `*.proxy.*` — шаблонные классы, зависящие только от абстрактного `impl` с методом `impl.callTyped<Ret>(method, args...)` и не знающие про конкретный транспорт.
* `*.skeleton.*` — используют `RpcInvoker` и принимают/возвращают `RpcValue` для диспетчеризации вызовов.
* `*.proxy.*` — используют `ProxyMarshaller` и `RpcChannel`, не зависят от конкретного транспорта.
* `*.skeleton.*` — используют `RpcInvoker` и принимают/возвращают `IpcMessage` для диспетчеризации вызовов.
---

@ -1,80 +0,0 @@
#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,6 +1,6 @@
#pragma once
#include "IpcConfig.h"
#include "IpcMessage.h"
// Абстракция IPCканала, работающего с IpcMessage.
// Живёт отдельно от RPCкода, чтобы транспорт не зависел от конкретной RPCреализации.

@ -1,6 +1,6 @@
#pragma once
#include <ipc/IpcConfig.h>
#include <ipc/IpcMessage.h>
#include <rpc/RpcValue.h>
#include <string>

@ -1,21 +0,0 @@
#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

@ -5,8 +5,6 @@
#include <rpc/RpcInvoker.h>
#include <string>
#include <iostream>
#include <unistd.h>
// Серверный диспетчер, который получает IpcMessage с канала,
// декодирует его в RPC-вызов, вызывает RpcInvoker и шлёт ответ.

@ -0,0 +1,117 @@
#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 {
// Для входящих сообщений считаем "пустым" то, у которого
// больше не осталось непрочитанных данных во входном потоке.
//
// Для свежесозданного сообщения (ещё не инициализирован in_)
// поведение остаётся прежним: пусто == raw_.empty().
if (!in_initialized_) {
return raw_.empty();
}
// Если поток уже инициализирован, смотрим, остались ли данные.
// peek() вернёт EOF, когда всё прочитано.
return in_.peek() == EOF;
}
private:
std::string raw_;
std::ostringstream out_;
mutable std::istringstream in_;
mutable bool in_initialized_{false};
void ensureInput() const {
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;
}

@ -5,8 +5,6 @@
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <iostream>
// IPCканал поверх именованных pipe.
// Инкапсулирует работу с файловыми дескрипторами и обмен сообщениями IpcMessage.
@ -20,31 +18,6 @@ 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 {
@ -60,64 +33,22 @@ public:
if (fdOut_ < 0) {
return;
}
auto raw = msg.serialize(); // тип определяется сериализатором
// Для текстовых форматов (std::string):
if constexpr (std::is_same_v<typename IpcMessage::RawData, std::string>) {
const std::string& data = raw;
const std::string& data = msg.raw();
::write(fdOut_, data.c_str(), data.size());
::write(fdOut_, "\n", 1);
}
// Можно добавить другие форматы по мере необходимости
}
IpcMessage receive() override {
if (fdIn_ < 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()) {
char buf[4096];
const int n = ::read(fdIn_, buf, sizeof(buf) - 1);
if (n <= 0) {
return IpcMessage{};
}
return IpcMessage(line);
}
// Можно добавить другие форматы по мере необходимости
return IpcMessage{};
buf[n] = 0;
return IpcMessage(std::string(buf));
}
private:

@ -1,102 +0,0 @@
#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;
}
};

@ -4,13 +4,9 @@
#include <ipc/IpcCodec.h>
#include <rpc/RpcValue.h>
// Маршаллер, который знает, как превратить типизированный RPC-вызов
// в IpcMessage и обратно. Живёт в IPC-слое и опирается на IpcChannel
// и IpcCodec, но снаружи предъявляет только callTyped<T>(...).
class IpcMarshaller {
class ProxyMarshaller {
public:
explicit IpcMarshaller(IpcChannel& ch)
: channel(ch) {}
explicit ProxyMarshaller(IpcChannel& ch) : channel(ch) {}
// Базовый type-erased вызов: принимает вектор RpcValue и возвращает RpcValue.
RpcValue call(const std::string& method, const RpcArgs& args) {
@ -25,7 +21,7 @@ public:
return IpcCodec::decodeResponse(resp);
}
// Типизированный хелпер: контрактом является только наличие этого метода.
// Удобный шаблонный хелпер для сгенерированных прокси.
template<typename Ret, typename... Args>
Ret callTyped(const std::string& method, const Args&... args) {
RpcArgs packed;
@ -42,4 +38,3 @@ private:
};

@ -1,10 +1,5 @@
#include "MyService.h"
int MyService::add(int a, int b) {
counter_ += (a + b);
return counter_;
}
int MyService::get() {
return counter_;
return a + b;
}

@ -5,13 +5,8 @@
// 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);
};

@ -1,6 +1,6 @@
#include "MyService.proxy.h"
#include "ipc/IpcPipeChannel.h"
#include "ipc/IpcMarshaller.h"
#include "proxy/ProxyMarshaller.h"
#include <sys/stat.h>
@ -16,11 +16,10 @@ int main() {
// и читает из fifo, в который пишет сервер (fifo_to_client).
IpcPipeChannel ch("/tmp/fifo_to_client", "/tmp/fifo_to_server");
// RPCуровень: создаём IpcMarshaller поверх канала и передаём его в прокси.
IpcMarshaller marshaller(ch);
MyServiceProxy<IpcMarshaller> proxy(marshaller);
// RPCуровень: создаём marshaller поверх канала и передаём его в прокси.
ProxyMarshaller marshaller(ch);
MyServiceProxy proxy(marshaller);
proxy.add(7, 9);
int counter = proxy.get();
std::cout << "RESULT: " << counter << std::endl;
int r = proxy.add(7, 9);
std::cout << "RESULT: " << r << std::endl;
}

@ -1,5 +1,11 @@
#include "{{ cls.name }}.proxy.h"
// Реализация шаблонного прокси целиком находится в заголовочном файле.
// Этот cpp остаётся пустым, чтобы сгенерированные файлы по‑прежнему
// могли участвовать в сборке как отдельная единица трансляции.
{{ 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 %}

@ -1,24 +1,14 @@
#pragma once
#include "{{ cls.name }}.h"
#include "proxy/ProxyMarshaller.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(Impl& impl)
: impl(impl) {}
explicit {{ cls.name }}Proxy(ProxyMarshaller& marshaller);
{% 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 %}) {
return impl.template callTyped<{{ m.return_type }}>("{{ cls.name }}.{{ m.name }}"{% for a in m.args %}, {{ a.name }}{% endfor %});
}
{{ m.return_type }} {{ m.name }}({% for a in m.args %}{{ a.type }} {{ a.name }}{% if not loop.last %}, {% endif %}{% endfor %});
{% endfor %}
private:
Impl& impl;
ProxyMarshaller& impl;
};

Loading…
Cancel
Save