From 4638a40cffd5160b4a54f199b6ebabd1123dac19 Mon Sep 17 00:00:00 2001 From: GRayHook Date: Mon, 1 Dec 2025 18:51:38 +0700 Subject: [PATCH] real PoC of autocode --- CMakeLists.txt | 56 ++++++++-- README.md | 147 ++++++++++++++++++++++++++ src/MyService.h | 10 +- src/MyService.proxy.cpp | 23 ---- src/MyService.proxy.h | 14 --- src/MyService.skeleton.cpp | 20 ---- src/MyService.skeleton.h | 15 --- src/rpc_common.h | 2 + src/rpc_export.h | 7 ++ tools/generate_rpc.py | 227 ++++++++++++++++++++++++++++++++++++++++ tools/templates/proxy.cpp.j2 | 38 +++++++ tools/templates/proxy.h.j2 | 17 +++ tools/templates/skeleton.cpp.j2 | 19 ++++ tools/templates/skeleton.h.j2 | 11 ++ 14 files changed, 525 insertions(+), 81 deletions(-) create mode 100644 README.md delete mode 100644 src/MyService.proxy.cpp delete mode 100644 src/MyService.proxy.h delete mode 100644 src/MyService.skeleton.cpp delete mode 100644 src/MyService.skeleton.h create mode 100644 src/rpc_common.h create mode 100644 src/rpc_export.h create mode 100755 tools/generate_rpc.py create mode 100644 tools/templates/proxy.cpp.j2 create mode 100644 tools/templates/proxy.h.j2 create mode 100644 tools/templates/skeleton.cpp.j2 create mode 100644 tools/templates/skeleton.h.j2 diff --git a/CMakeLists.txt b/CMakeLists.txt index b0951d2..1bf2cb3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,22 +1,66 @@ cmake_minimum_required(VERSION 3.10) - project(SimpleRPCExample CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -# Папка с исходниками +# include source includes include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src) -# Сервер +# find python interpreter +find_package(Python3 COMPONENTS Interpreter REQUIRED) + +set(GENERATED_DIR ${CMAKE_BINARY_DIR}/generated) +file(MAKE_DIRECTORY ${GENERATED_DIR}) + +set(RPC_GENERATOR ${CMAKE_CURRENT_SOURCE_DIR}/tools/generate_rpc.py) +set(RPC_TEMPLATES ${CMAKE_CURRENT_SOURCE_DIR}/tools/templates) + +# inputs to parse +set(RPC_INPUTS + ${CMAKE_CURRENT_SOURCE_DIR}/src/MyService.h +) + +# command to run generator +add_custom_command( + OUTPUT + ${GENERATED_DIR}/MyService.proxy.h + ${GENERATED_DIR}/MyService.proxy.cpp + ${GENERATED_DIR}/MyService.skeleton.h + ${GENERATED_DIR}/MyService.skeleton.cpp + COMMAND ${CMAKE_COMMAND} -E echo "Generating RPC stubs..." + COMMAND ${Python3_EXECUTABLE} ${RPC_GENERATOR} --out-dir ${GENERATED_DIR} + --compile-commands ${CMAKE_BINARY_DIR}/compile_commands.json + --templates ${RPC_TEMPLATES} + ${RPC_INPUTS} + DEPENDS ${RPC_GENERATOR} ${RPC_TEMPLATES} ${RPC_INPUTS} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Running RPC code generator" + VERBATIM +) + +include_directories(${GENERATED_DIR}) + +add_custom_target(rpc_generated DEPENDS + ${GENERATED_DIR}/MyService.proxy.h + ${GENERATED_DIR}/MyService.proxy.cpp + ${GENERATED_DIR}/MyService.skeleton.h + ${GENERATED_DIR}/MyService.skeleton.cpp +) + +# Server add_executable(server src/server.cpp src/MyService.cpp - src/MyService.skeleton.cpp + ${GENERATED_DIR}/MyService.skeleton.cpp ) -# Клиент +add_dependencies(server rpc_generated) + +# Client add_executable(client src/client.cpp - src/MyService.proxy.cpp + ${GENERATED_DIR}/MyService.proxy.cpp ) + +add_dependencies(client rpc_generated) diff --git a/README.md b/README.md new file mode 100644 index 0000000..a3c1926 --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +# Minimal C++ RPC PoC with Auto-Code Generation + +Простой Proof-of-Concept реализации RPC для C++ с автоматической генерацией прокси и скелета по аннотациям в исходниках. + +Проект демонстрирует: + +* Парсинг исходников C++ с помощью `libclang` для поиска аннотированных классов и методов. +* Автоматическую генерацию файлов: + * `*.proxy.h/cpp` — клиентский прокси для вызова удалённых методов. + * `*.skeleton.h/cpp` — серверный скелет для приёма запросов и вызова реальных методов. +* Минимальный протокол передачи данных через **именованные каналы (FIFO)**. +* Поддержка только типов `int` для аргументов и возвращаемого значения (PoC). + +--- + +## Структура проекта + +``` +project/ +├── CMakeLists.txt +├── README.md +├── src +│   ├── client.cpp +│   ├── MyService.cpp +│   ├── MyService.h +│   ├── rpc_common.h +│   ├── rpc_export.h +│   └── server.cpp +├── tools +│ ├── generate_rpc.py +│ └── templates +│ ├── proxy.cpp.j2 +│ ├── proxy.h.j2 +│ ├── skeleton.cpp.j2 +│ └── skeleton.h.j2 +└─ build/ # создаётся при сборке +``` + +--- + +## Зависимости + +* CMake ≥ 3.10 +* GCC или Clang с поддержкой C++17 +* Python 3.8+ с пакетами: + + ```bash + pip3 install jinja2 clang + ``` +* libclang (для Python) + Если GCC используется только для сборки — libclang нужен только для генератора. + +**Важно**: Сам `libclang` и пакет `clang` для Python должны быть одной версии. + +--- + +## Аннотации для экспорта + +Управление тем, какие атрибуты каких классов следует экспортировать происходит с помощью +аннотаций в исходном коде. + +См. файл [MyService.h](src/MyService.h): + +```c +class RPC_EXPORT MyService { +public: + RPC_EXPORT + int add(int a, int b); + int minus(int a, int b); +}; +``` + +* Экспортируем класс `MyService`: + * Экспортируем метод `MyService::add`; + * Но **не** экспортируем метод `MyService::minus`. + +--- + +## Сборка проекта + +1. Конфигурируем CMake: + +```bash +cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -B build +``` + +Директива `CMAKE_EXPORT_COMPILE_COMMANDS=ON` нужна для корректного парсинга в `libclang`. + +2. Собираем проект: + +```bash +cmake --build build +``` + +В результате получаем два бинарника: + +``` +./build/server +./build/client +``` + +--- + +## Генерация кода + +Автоматическая генерация прокси и скелета происходит **при сборке** через Python-скрипт `tools/generate_rpc.py`. + +Пример ручного запуска генератора: + +```bash +python3 tools/generate_rpc.py \ + --out-dir build/generated \ + --compile-commands build/compile_commands.json \ + src/MyService.h +``` + +Сгенерированные файлы попадут в: + +``` +build/generated/ +├─ MyService.proxy.h +├─ MyService.proxy.cpp +├─ MyService.skeleton.h +└─ MyService.skeleton.cpp +``` + +--- + +## Запуск + +1. В одном терминале запускаем сервер: + +```bash +./server +``` + +2. В другом терминале — клиент: + +```bash +./client +``` + +**Ожидаемый вывод:** + +``` +RESULT: 15 +``` diff --git a/src/MyService.h b/src/MyService.h index a4af91a..445e070 100644 --- a/src/MyService.h +++ b/src/MyService.h @@ -1,8 +1,12 @@ -// MyService.h #pragma once -class [[annotate("export")]] MyService { +#include + +// annotate with clang attribute or via comment annotation recognized by libclang +// Use ANNOTATE attribute supported by clang: __attribute__((annotate("export"))) +class RPC_EXPORT MyService { public: - [[annotate("export")]] + RPC_EXPORT int add(int a, int b); + int minus(int a, int b); }; diff --git a/src/MyService.proxy.cpp b/src/MyService.proxy.cpp deleted file mode 100644 index 28c93ee..0000000 --- a/src/MyService.proxy.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// MyService.proxy.cpp -#include "MyService.proxy.h" -#include -#include -#include - -MyServiceProxy::MyServiceProxy(const char* pipeIn, const char* pipeOut) { - fdIn = open(pipeIn, O_WRONLY); - fdOut = open(pipeOut, O_RDONLY); -} - -int MyServiceProxy::add(int a, int b) { - std::ostringstream out; - out << "add " << a << " " << b << "\n"; - - write(fdIn, out.str().c_str(), out.str().size()); - - char buf[128]; - int n = read(fdOut, buf, sizeof(buf)-1); - buf[n] = 0; - - return std::atoi(buf); -} diff --git a/src/MyService.proxy.h b/src/MyService.proxy.h deleted file mode 100644 index 7e04b96..0000000 --- a/src/MyService.proxy.h +++ /dev/null @@ -1,14 +0,0 @@ -// MyService.proxy.h -#pragma once -#include - -class MyServiceProxy { -public: - MyServiceProxy(const char* pipeIn, const char* pipeOut); - - int add(int a, int b); - -private: - int fdIn; // клиент пишет сюда → сервер читает - int fdOut; // сервер пишет сюда → клиент читает -}; diff --git a/src/MyService.skeleton.cpp b/src/MyService.skeleton.cpp deleted file mode 100644 index b90ccea..0000000 --- a/src/MyService.skeleton.cpp +++ /dev/null @@ -1,20 +0,0 @@ -// MyService.skeleton.cpp -#include "MyService.skeleton.h" -#include - -MyServiceSkeleton::MyServiceSkeleton(MyService& o) : obj(o) {} - -std::string MyServiceSkeleton::handleRequest(const std::string& req) { - std::istringstream in(req); - std::string method; - in >> method; - - if (method == "add") { - int a, b; - in >> a >> b; - int r = obj.add(a, b); - return std::to_string(r); - } - - return "ERR"; -} diff --git a/src/MyService.skeleton.h b/src/MyService.skeleton.h deleted file mode 100644 index 38ca0c8..0000000 --- a/src/MyService.skeleton.h +++ /dev/null @@ -1,15 +0,0 @@ -// MyService.skeleton.h -#pragma once -#include "MyService.h" -#include - -class MyServiceSkeleton { -public: - MyServiceSkeleton(MyService& obj); - - // обработка одной строки запроса и возврат ответа - std::string handleRequest(const std::string& req); - -private: - MyService& obj; -}; diff --git a/src/rpc_common.h b/src/rpc_common.h new file mode 100644 index 0000000..607cfe7 --- /dev/null +++ b/src/rpc_common.h @@ -0,0 +1,2 @@ +#pragma once +// placeholder for shared RPC declarations in future diff --git a/src/rpc_export.h b/src/rpc_export.h new file mode 100644 index 0000000..f7c071e --- /dev/null +++ b/src/rpc_export.h @@ -0,0 +1,7 @@ +#pragma once + +#ifdef __clang__ +# define RPC_EXPORT __attribute__((annotate("export"))) +#else +# define RPC_EXPORT +#endif diff --git a/tools/generate_rpc.py b/tools/generate_rpc.py new file mode 100755 index 0000000..ec8fb9b --- /dev/null +++ b/tools/generate_rpc.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +""" +Minimal RPC code generator PoC. + +Parses C++ sources with libclang, finds classes and methods annotated with +ANNOTATE("export") (or [[annotate("export")]]), and generates proxy + skeleton +for methods that use only `int` arguments and return type `int`. + +Usage: + tools/generate_rpc.py --out-dir build/generated --compile-commands build/compile_commands.json src/MyService.h src/other.h +""" +import argparse +import json +import os +import sys +from dataclasses import dataclass, field +from typing import List + +# Try imports +try: + from clang.cindex import Index, CursorKind, Config +except Exception as e: + print("ERROR: clang.cindex import failed:", e) + print("Make sure libclang python bindings are installed and libclang is reachable.") + sys.exit(1) + +try: + from jinja2 import Environment, FileSystemLoader +except Exception as e: + print("ERROR: jinja2 import failed:", e) + print("Install with: pip3 install jinja2") + sys.exit(1) + +# ========== IR dataclasses ========== +@dataclass +class Arg: + name: str + type: str + +@dataclass +class Method: + name: str + args: List[Arg] = field(default_factory=list) + return_type: str = "void" + signature: str = "" # unique signature string used for dispatch (name:types) + +@dataclass +class Class: + name: str + methods: List[Method] = field(default_factory=list) + +# ========== helpers for compile flags ========== +def load_compile_flags(compile_commands_path, src_path): + if not compile_commands_path or not os.path.exists(compile_commands_path): + return [] + with open(compile_commands_path) as f: + compile_commands = json.load(f) + # find matching entry by file (exact or basename) + abs_src = os.path.abspath(src_path) + for entry in compile_commands: + entry_file = os.path.abspath(entry.get('file') or "") + if entry_file == abs_src or os.path.basename(entry_file) == os.path.basename(abs_src): + # get arguments + if 'arguments' in entry and isinstance(entry['arguments'], list): + cmd = entry['arguments'] + else: + cmd = (entry.get('command') or "").split() + # remove compiler and -c + # keep flags like -I, -D + filtered = [] + skip_next = False + for i, tok in enumerate(cmd): + if skip_next: + skip_next = False + continue + if tok.endswith('g++') or tok.endswith('clang++') or tok.endswith('clang') or tok.endswith('g++') or tok.endswith('cc'): + continue + if tok == "-c": + skip_next = True + continue + filtered.append(tok) + return filtered + return [] + +# ========== AST utilities ========== +def has_annotation(cursor, annotation_text="export"): + # Check children for ANNOTATE_ATTR or spelling containing annotation_text + for c in cursor.get_children(): + if c.kind == CursorKind.ANNOTATE_ATTR and annotation_text in (c.spelling or ""): + return True + # also check attributes spelling (fallback) + # cursor.get_tokens() might include annotate attribute for new attribute syntax but libclang may not expose + return False + +def type_is_int(t): + if not t: + return False + # check canonical type spelling - this may vary; keep simple + s = t.spelling + # accept "int" and "signed int" + return s in ("int", "signed int", "int32_t", "long int") # minimal + +# ========== parse file ========== +def parse_file(index, filepath, args): + tu = index.parse(filepath, args=args) + classes = [] + + for cursor in tu.cursor.get_children(): + # top-level class/struct declarations + if cursor.kind == CursorKind.CLASS_DECL or cursor.kind == CursorKind.STRUCT_DECL: + # only records with a name + if not cursor.spelling: + continue + if has_annotation(cursor, "export"): + cls = Class(name=cursor.spelling) + # iterate its children for methods + for c in cursor.get_children(): + if c.kind == CursorKind.CXX_METHOD or c.kind == CursorKind.FUNCTION_DECL: + if not has_annotation(c, "export"): + continue + # build method IR + # only support non-template, non-variadic, simple methods + m = Method(name=c.spelling) + # return type + try: + m.return_type = c.result_type.spelling + except Exception: + m.return_type = "void" + # args + ok = True + for arg in c.get_arguments(): + t = arg.type + if not type_is_int(t): + ok = False + break + m.args.append(Arg(name=arg.spelling or "arg", type=t.spelling)) + if not ok: + print(f"Skipping method {c.spelling} of {cursor.spelling}: unsupported arg types") + continue + # check return type is int + if not type_is_int(c.result_type): + print(f"Skipping method {c.spelling} of {cursor.spelling}: unsupported return type {c.result_type.spelling}") + continue + # signature: name:comma-separated types + sig_types = ",".join(a.type for a in m.args) + m.signature = f"{m.name}:{sig_types}" + cls.methods.append(m) + if cls.methods: + classes.append(cls) + return classes + +# ========== templating ========== +def render_templates(classes, out_dir, templates_dir): + env = Environment( + loader=FileSystemLoader(templates_dir), + autoescape=False, + trim_blocks=True, + lstrip_blocks=True, + ) + + proxy_h = env.get_template("proxy.h.j2") + proxy_cpp = env.get_template("proxy.cpp.j2") + skeleton_h = env.get_template("skeleton.h.j2") + skeleton_cpp = env.get_template("skeleton.cpp.j2") + + for cls in classes: + name = cls.name + + with open(f"{out_dir}/{name}.proxy.h", "w") as f: + f.write(proxy_h.render(cls=cls)) + + with open(f"{out_dir}/{name}.proxy.cpp", "w") as f: + f.write(proxy_cpp.render(cls=cls)) + + with open(f"{out_dir}/{name}.skeleton.h", "w") as f: + f.write(skeleton_h.render(cls=cls)) + + with open(f"{out_dir}/{name}.skeleton.cpp", "w") as f: + f.write(skeleton_cpp.render(cls=cls)) + +# ========== main ========== +def main(): + p = argparse.ArgumentParser() + p.add_argument("--out-dir", "-o", required=True) + p.add_argument("--compile-commands", "-c", default=None) + p.add_argument("inputs", nargs="+") + p.add_argument("--templates", "-t", default=os.path.join(os.path.dirname(__file__), "templates")) + args = p.parse_args() + + out_dir = args.out_dir + os.makedirs(out_dir, exist_ok=True) + + # configure libclang: optionally allow user to set LIBCLANG_PATH env var + # If needed, user can set: export LIBCLANG_PATH=/usr/lib/llvm-10/lib + libclang_path = os.environ.get("LIBCLANG_PATH") + if libclang_path: + try: + Config.set_library_path(libclang_path) + except Exception as e: + print("WARNING: cannot set libclang path:", e) + + index = Index.create() + all_classes = [] + + for inp in args.inputs: + if not os.path.exists(inp): + print("WARN: input file not found:", inp) + continue + compile_args = load_compile_flags(args.compile_commands, inp) + # ensure -x c++ if missing + if not any(a.startswith("-x") for a in compile_args): + compile_args = ["-x", "c++", "-std=c++17"] + compile_args + print("Parsing", inp, "with args:", compile_args) + classes = parse_file(index, inp, compile_args) + all_classes.extend(classes) + + if not all_classes: + print("No exported classes/methods found. Nothing to generate.") + return 0 + + # render templates + render_templates(all_classes, out_dir, args.templates) + print("Generated files for classes:", ", ".join(c.name for c in all_classes)) + return 0 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/templates/proxy.cpp.j2 b/tools/templates/proxy.cpp.j2 new file mode 100644 index 0000000..0fcef15 --- /dev/null +++ b/tools/templates/proxy.cpp.j2 @@ -0,0 +1,38 @@ +#include "{{ cls.name }}.proxy.h" +#include +#include +#include +#include +#include + +{{ cls.name }}Proxy::{{ cls.name }}Proxy(const char* pipeIn, const char* pipeOut) { + fdIn = open(pipeIn, O_WRONLY); + if (fdIn < 0) { + perror("open pipeIn"); + } + fdOut = open(pipeOut, O_RDONLY); + if (fdOut < 0) { + perror("open pipeOut"); + } +} + +{% 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 %}) { + std::ostringstream out; + out << "{{ m.name }}"; + {% for a in m.args %} + out << " " << {{ a.name }}; + {% endfor %} + out << "\n"; + std::string s = out.str(); + write(fdIn, s.c_str(), (ssize_t)s.size()); + + char buf[256]; + ssize_t n = read(fdOut, buf, sizeof(buf)-1); + if (n <= 0) { + return 0; + } + buf[n] = '\0'; + return std::atoi(buf); +} +{% endfor %} diff --git a/tools/templates/proxy.h.j2 b/tools/templates/proxy.h.j2 new file mode 100644 index 0000000..071ecc2 --- /dev/null +++ b/tools/templates/proxy.h.j2 @@ -0,0 +1,17 @@ +#pragma once +#include +#include +#include +#include "rpc_common.h" // we'll add this helper header in src/ +#include "{{ cls.name }}.skeleton.h" // for type name consistency + +class {{ cls.name }}Proxy { +public: + {{ cls.name }}Proxy(const char* pipeIn, const char* pipeOut); + {% 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 %}); + {% endfor %} +private: + int fdIn; + int fdOut; +}; diff --git a/tools/templates/skeleton.cpp.j2 b/tools/templates/skeleton.cpp.j2 new file mode 100644 index 0000000..26b7023 --- /dev/null +++ b/tools/templates/skeleton.cpp.j2 @@ -0,0 +1,19 @@ +#include "{{ cls.name }}.skeleton.h" +#include + +{{ cls.name }}Skeleton::{{ cls.name }}Skeleton({{ cls.name }}& o) : obj(o) {} + +std::string {{ cls.name }}Skeleton::handleRequest(const std::string& req) { + std::istringstream in(req); + std::string method; + in >> method; + {% for m in cls.methods %} + if (method == "{{ m.name }}") { + {% for a in m.args %}int {{ a.name }}; in >> {{ a.name }}; + {% endfor %} + int res = obj.{{ m.name }}({% for a in m.args %}{{ a.name }}{% if not loop.last %}, {% endif %}{% endfor %}); + return std::to_string(res); + } + {% endfor %} + return std::string("ERR"); +} diff --git a/tools/templates/skeleton.h.j2 b/tools/templates/skeleton.h.j2 new file mode 100644 index 0000000..f2d2936 --- /dev/null +++ b/tools/templates/skeleton.h.j2 @@ -0,0 +1,11 @@ +#pragma once +#include "{{ cls.name }}.h" +#include + +class {{ cls.name }}Skeleton { +public: + {{ cls.name }}Skeleton({{ cls.name }}& obj); + std::string handleRequest(const std::string& req); +private: + {{ cls.name }}& obj; +};