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