You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

228 lines
8.4 KiB
Python

#!/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())