real PoC of autocode

Sergey Marinkevich 2 weeks ago
parent ee2fd0ee2e
commit 4638a40cff

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

@ -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
```

@ -1,8 +1,12 @@
// MyService.h
#pragma once
class [[annotate("export")]] MyService {
#include <rpc_export.h>
// 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);
};

@ -1,23 +0,0 @@
// MyService.proxy.cpp
#include "MyService.proxy.h"
#include <unistd.h>
#include <fcntl.h>
#include <sstream>
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);
}

@ -1,14 +0,0 @@
// MyService.proxy.h
#pragma once
#include <string>
class MyServiceProxy {
public:
MyServiceProxy(const char* pipeIn, const char* pipeOut);
int add(int a, int b);
private:
int fdIn; // клиент пишет сюда → сервер читает
int fdOut; // сервер пишет сюда → клиент читает
};

@ -1,20 +0,0 @@
// MyService.skeleton.cpp
#include "MyService.skeleton.h"
#include <sstream>
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";
}

@ -1,15 +0,0 @@
// MyService.skeleton.h
#pragma once
#include "MyService.h"
#include <string>
class MyServiceSkeleton {
public:
MyServiceSkeleton(MyService& obj);
// обработка одной строки запроса и возврат ответа
std::string handleRequest(const std::string& req);
private:
MyService& obj;
};

@ -0,0 +1,2 @@
#pragma once
// placeholder for shared RPC declarations in future

@ -0,0 +1,7 @@
#pragma once
#ifdef __clang__
# define RPC_EXPORT __attribute__((annotate("export")))
#else
# define RPC_EXPORT
#endif

@ -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 <file>
# 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())

@ -0,0 +1,38 @@
#include "{{ cls.name }}.proxy.h"
#include <unistd.h>
#include <fcntl.h>
#include <sstream>
#include <cstdlib>
#include <cstring>
{{ 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 %}

@ -0,0 +1,17 @@
#pragma once
#include <string>
#include <memory>
#include <cstdint>
#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;
};

@ -0,0 +1,19 @@
#include "{{ cls.name }}.skeleton.h"
#include <sstream>
{{ 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");
}

@ -0,0 +1,11 @@
#pragma once
#include "{{ cls.name }}.h"
#include <string>
class {{ cls.name }}Skeleton {
public:
{{ cls.name }}Skeleton({{ cls.name }}& obj);
std::string handleRequest(const std::string& req);
private:
{{ cls.name }}& obj;
};
Loading…
Cancel
Save