Files
Viper_VRTE/fltk/gen_ffi.py
T
TermiNexus c0d56b127d chore(comments): includes 头注释英文化(R11 收尾)
把 includes 下 64 个 .vp 的文件头注释(含 traceback.vp 第 13-427 行的整块文件级说明)以及 2 处漏网的正文中文注释(ast/lexer.vp、json/__writer.vp)与 1 处中文 docstring(memhub.vp)逐行译为英文;同步把 fltk/gen_ffi.py 的生成模板与其自身注释英文化,使重新生成仍为英文。

只改注释/文档串文字:行数与代码逐字节不变(骨架比对 140 文件漂移 0),运行时中文字符串字面量 305 处保持原样。验证:三重重编 SUCCESS(170.21s)+ 全量 77/77 顺序回归通过 + VET 编译 SUCCESS / --search-paths 7 条 exit 0。
2026-10-06 09:42:25 +08:00

453 lines
16 KiB
Python

# -*- coding: utf-8 -*-
"""
fltk/gen_ffi.py - cfltk (FLTK C API) -> Viper extern FFI generator
Reads all C declarations from cfltk/include/cfltk2/*.h:
* Parse all `#define XXX_DECLARE(param) ...` macro definitions (WIDGET/GROUP/WINDOW/
BUTTON/INPUT/BROWSER/IMAGE/TABLE/DISPLAY/VALUATOR/MENU),
* Expand each `XXX_DECLARE(Class)` call in every header,
* Convert them, along with the standalone function prototypes in the headers, into extern def.
Generates the .vp FFI files under includes/fltk:
_types.vp opaque pointer typedefs (one Fl_*Ptr per Fl_* class)
_enums.vp all enum constants of cfl_enums.h
_cfl.vp global Fl functions (cfl.h)
_cfl_<header>.vp extern def per header file
Usage: python includes/fltk/gen_ffi.py
"""
import os
import re
HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, "cfltk", "include", "cfltk2")
# ============================================================
# C -> Viper type mapping
# ============================================================
# Function pointer types (not expressible in Viper; always mapped to opaque bytes)
CALLBACK_TYPES = {
"Fl_Callback", "custom_handler_callback", "custom_draw_callback",
"Fl_Awake_Handler", "Fl_Text_Modify_Cb", "Fl_Text_Predelete_Cb",
}
# Viper/Python keywords cannot be used as parameter names (e.g. C `int Fl_compose(int *del)`)
_KEYWORDS = {
"def", "class", "from", "import", "global", "nonlocal", "pass", "lambda",
"del", "in", "is", "and", "or", "not", "for", "while", "if", "elif",
"else", "return", "break", "continue", "try", "except", "finally", "with",
"yield", "assert", "async", "await", "raise", "True", "False", "None",
"int", "str", "float", "bool", "len",
}
# C type keywords: when the last word of an unnamed parameter, they belong to the type, not the name
# (`unsigned int` -> trailing int is not the name; `Fl_color_average(unsigned int, ...)`).
_C_TYPE_WORDS = {
"int", "char", "short", "long", "unsigned", "signed", "float", "double",
"void", "size_t", "uintptr_t",
}
def map_type(t: str) -> str | None:
"""Return the Viper annotation type; returns None for void."""
t = t.strip()
t = re.sub(r"\bconst\b", "", t).strip()
t = re.sub(r"\bstruct\b", "", t).strip()
t = re.sub(r"\s+", " ", t)
if t in CALLBACK_TYPES:
return "bytes"
if "(" in t: # function pointer (unnamed typedef form)
return "bytes"
if t.endswith("[]"):
t = t[:-2].strip() + "*"
star_cnt = t.count("*")
if star_cnt:
base = t.replace("*", "").strip()
if star_cnt >= 2: # pointer-to-pointer (int** etc.) always treated as opaque pointer
return "bytes"
if base == "char":
return "str"
if base in CALLBACK_TYPES:
return "bytes"
if base.startswith("Fl_") or base.startswith("custom_"):
return base + "Ptr"
return "bytes"
if t == "void":
return None
if t == "int":
return "int"
if t == "unsigned int":
return "t.CUInt32T"
if t == "long":
return "t.CInt32T"
if t == "unsigned long":
return "t.CUInt32T"
if t == "short":
return "t.CInt16T"
if t == "unsigned short":
return "t.CUInt16T"
if t == "char":
return "t.CChar"
if t == "unsigned char":
return "t.CUInt8T"
if t == "double":
return "t.CDouble"
if t == "float":
return "t.CFloat"
if t == "uintptr_t":
return "t.CSizeT"
if t == "size_t":
return "t.CSizeT"
if t.startswith("Fl_") or t.startswith("custom_"):
return t + "Ptr"
return "bytes" # unknown types always treated as opaque pointer
def split_args(args: str) -> list[tuple[str, str]]:
"""Split a C argument string -> [(type, name)]; function pointers / unnamed args get an auto name."""
if not args.strip():
return []
parts: list[str] = []
depth = 0
cur: list[str] = []
for ch in args:
if ch == "(":
depth += 1
elif ch == ")":
depth -= 1
if ch == "," and depth == 0:
parts.append("".join(cur).strip())
cur = []
else:
cur.append(ch)
if cur:
parts.append("".join(cur).strip())
out: list[tuple[str, str]] = []
for p in parts:
p = p.strip()
if not p or p == "void": # C's (void) means no parameters
continue
if "(" in p and (")(" in p or "*)" in p or re.search(r"\(\s*\*", p)):
out.append((p, "cb%d" % len(out)))
continue
# A C parameter looks like `Fl_Tree *self` / `const char *title`: the '*' sticks to the name,
# so the last word is the name and the rest is the type
m = re.search(r"(\w+)\s*$", p)
if m:
typ = p[:m.start()].strip()
nm = m.group(1)
# Pure type (unnamed) parameter: no type part (`int`) or the last word is itself a C type
# keyword (`unsigned int` -> trailing int is part of the type, not the name).
if not typ or nm in _C_TYPE_WORDS:
typ, nm = p, "a%d" % len(out)
if nm in _KEYWORDS:
nm = nm + "_"
out.append((typ, nm))
else:
out.append((p, "a%d" % len(out)))
return out
def emit_extern(name: str, params: list[tuple[str, str]], ret_t: str | None,
indent: str = "") -> str:
ps = ", ".join("%s: %s" % (pn, map_type(pt)) for pt, pn in params)
rt = map_type(ret_t) if ret_t else None
if rt is None: # void / no return
return "%sextern def %s(%s)" % (indent, name, ps)
return "%sextern def %s(%s) -> %s" % (indent, name, ps, rt)
# ============================================================
# Header parsing
# ============================================================
MACRO_CALL_RE = re.compile(r"(?m)^\s*(\w+_DECLARE)\((\w+)\)\s*$")
def strip_macro_blocks(text: str) -> str:
"""Remove #define macro definition blocks (including all backslash continuations); line-by-line to tolerate CRLF."""
out: list[str] = []
skip = False
for line in text.split("\n"):
if re.match(r"\s*#define\b", line):
skip = line.rstrip().endswith("\\")
continue
if skip:
if line.rstrip().endswith("\\"):
continue
skip = False
continue
out.append(line)
return "\n".join(out)
def strip_preprocessor(text: str) -> str:
text = strip_macro_blocks(text)
text = re.sub(r"typedef[^;]*;", "", text, flags=re.S) # typedef
text = re.sub(r"/\*.*?\*/", "", text, flags=re.S) # comments
return text
def extract_protos(text: str) -> list[tuple[str, str, str]]:
"""Extract standalone function prototypes -> [(return type, function name, argument string)].
Note that in C `type *name` the '*' sticks to the function name and the space is before the type,
so it cannot be split by the whitespace-separated `type name(` form; instead take the last word
before the first '(' as the function name and the rest as the return type.
"""
text = strip_preprocessor(text)
text = re.sub(r"(?m)^\s*#.*$", "", text) # other preprocessor lines
text = re.sub(MACRO_CALL_RE, "", text)
protos: list[tuple[str, str, str]] = []
buf: list[str] = []
depth = 0
for ch in text:
if ch == "(":
depth += 1
elif ch == ")":
depth -= 1
if depth == 0 and ch in "{}":
buf = [] # extern "C" { / block braces do not enter the prototype buffer
continue
if ch == ";" and depth == 0:
stmt = "".join(buf).strip()
buf = []
if "(" in stmt and stmt.rstrip().endswith(")"):
open_i = stmt.find("(")
before = stmt[:open_i].strip()
args = stmt[open_i + 1:].rstrip()[:-1].strip()
mm = re.search(r"(\w+)\s*$", before)
if mm:
name = mm.group(1)
if name and not name.startswith(("Fl_EventNames",)):
ret = re.sub(r"\s+", " ", before[:mm.start()].strip())
protos.append((ret, name, args))
continue
buf.append(ch)
return protos
def extract_macro_defs(text: str) -> dict:
"""Parse `#define XXX_DECLARE(param)` macro bodies -> method templates.
Returns {macro name: (param, [(method suffix, argument string, return type, 'SELF'|'PARAMS'), ...])}
'SELF' means the first argument is the class's own pointer (replaced with self: <class>Ptr on expansion),
'PARAMS' means the arguments are kept as-is (new/from_dyn_ptr/super_draw etc.).
"""
out: dict = {}
lines = text.split("\n")
i = 0
n = len(lines)
while i < n:
m = re.match(r"#define\s+(\w+_DECLARE)\((\w+)\)\s*\\\s*$", lines[i])
if m:
name, param = m.group(1), m.group(2)
body: list[str] = []
i += 1
while i < n:
line = lines[i]
if line.rstrip().endswith("\\"):
body.append(line.rstrip()[:-1])
i += 1
else:
body.append(line)
break
methods: list = []
body_src = "\n".join(body).replace("##", "") # widget##_x -> widget_x
for ret, fname, args in extract_protos(body_src):
fn = fname.replace("##", "")
suffix = fn[len(param):].lstrip("_")
parts = split_args(args)
if parts and parts[0][0].replace("const", "").strip().rstrip("*").strip() == param:
kind = "SELF"
else:
kind = "PARAMS"
methods.append((suffix, args, ret, kind))
out[name] = (param, methods)
i += 1
return out
def extract_typedefs(text: str) -> list[str]:
"""Extract top-level typedef names (strip #define macro bodies first to avoid scanning macro params like widget)."""
names = []
text = strip_macro_blocks(text)
for m in re.finditer(r"typedef\s+(?:struct\s+)?[\w\s\*\(\)]*?(\w+)\s*;", text):
n = m.group(1)
if n and n not in names:
names.append(n)
return names
def eval_enum_expr(s: str | None, last: int) -> int:
if s is None:
return last + 1
s = s.strip()
if not s:
return last + 1
s2 = re.sub(r"0[xX]([0-9a-fA-F]+)", lambda m: str(int(m.group(1), 16)), s)
try:
return int(eval(s2))
except Exception:
return last + 1
def extract_enums(text: str) -> list[tuple[str, int]]:
out = []
for m in re.finditer(r"enum\s+(\w+)\s*\{(.*?)\}", text, re.S):
body = m.group(2)
last = -1
for item in re.split(r"\s*,\s*(?=[A-Za-z_])", body):
item = item.strip()
if not item or item.startswith("/"):
continue
mm = re.match(r"^(\w+)(?:\s*=\s*(.+))?$", item, re.S)
if not mm:
continue
last = eval_enum_expr(mm.group(2), last)
out.append((mm.group(1), last))
return out
# ============================================================
# Generation
# ============================================================
CLASS_PTRS: set[str] = set()
def gen_class_methods(cls: str, param: str, methods) -> list[str]:
lines = []
for suffix, args, ret, kind in methods:
fname = "%s_%s" % (cls, suffix)
ret_t = re.sub(r"\b%s\b" % re.escape(param), cls, ret)
if kind == "SELF":
rest = split_args(args)[1:] # drop the self parameter inside the macro
ps = [(cls + " *", "self")] + rest
else:
ps = split_args(args)
ps = [(re.sub(r"\b%s\b" % re.escape(param), cls, t), nm)
for t, nm in ps]
lines.append(emit_extern(fname, ps, ret_t))
return lines
def gen_header(path: str, out_name: str, defs: dict) -> str:
with open(path, "r", encoding="utf-8", errors="replace") as f:
text = f.read()
base = os.path.basename(path)
lines = [
"import t, c",
"from fltk._types import *",
"",
"# ============================================================",
"# fltk/%s — extern FFI declarations for %s (cfltk C API)" % (out_name, base),
"# ============================================================",
"",
"# Every function maps directly to a cfltk-generated C symbol (extern \"C\"); link target: cfltk2.",
"# Type conventions: Fl_* objects are opaque pointers (Fl_XxxPtr); const char* -> str;",
"# function pointers / void* -> bytes (pass 0/NULL or leave unused on the Viper side).",
"",
]
# expand in the order of macro calls in the header
for mm in MACRO_CALL_RE.finditer(text):
kind, cls = mm.group(1), mm.group(2)
if kind in defs:
param, methods = defs[kind]
lines.append("# ----- %s (%s) -----" % (cls, kind.lower()))
lines.extend(gen_class_methods(cls, param, methods))
lines.append("")
# standalone prototypes
protos = extract_protos(text)
for ret, name, args in protos:
lines.append(emit_extern(name, split_args(args), ret))
return "\n".join(lines) + "\n"
def main() -> None:
os.makedirs(HERE, exist_ok=True)
headers = sorted(f for f in os.listdir(SRC) if f.endswith(".h"))
# 1. Collect all macro definitions + class pointer typedefs
defs: dict = {}
for h in headers:
with open(os.path.join(SRC, h), "r", encoding="utf-8",
errors="replace") as f:
text = f.read()
defs.update(extract_macro_defs(text))
if h == "cfl_macros.h":
for n in extract_typedefs(text):
CLASS_PTRS.add(n + "Ptr")
if h == "cfl_enums.h":
continue
for mm in MACRO_CALL_RE.finditer(text):
ptr_name(mm.group(2))
for ret, name, args in extract_protos(text):
for t, _ in split_args(args):
mt = map_type(t)
if mt and mt.startswith("Fl_") and mt.endswith("Ptr"):
CLASS_PTRS.add(mt)
mt = map_type(ret)
if mt and mt.startswith("Fl_") and mt.endswith("Ptr"):
CLASS_PTRS.add(mt)
# 2. _types.vp
sorted_ptrs = sorted(CLASS_PTRS)
typ_lines = [
"import t, c",
"",
"# ============================================================",
"# fltk/_types.vp — opaque FLTK object pointer typedefs (cfltk C API)",
"# ============================================================",
"",
"# Every FLTK C++ object is handled as an opaque pointer in FFI; its lifetime is owned",
"# by the cfltk/fltk library (see each *_delete / *_free).",
"",
]
for p in sorted_ptrs:
typ_lines.append("%s: t.CTypedef = t.CPtr" % p)
with open(os.path.join(HERE, "_types.vp"), "w", encoding="utf-8") as f:
f.write("\n".join(typ_lines) + "\n")
# 3. _enums.vp
with open(os.path.join(SRC, "cfl_enums.h"), "r", encoding="utf-8",
errors="replace") as f:
enum_text = f.read()
enums = extract_enums(enum_text)
enum_lines = [
"import t, c",
"",
"# ============================================================",
"# fltk/_enums.vp — cfltk enum constants (t.CInt)",
"# ============================================================",
"",
]
for name, val in enums:
enum_lines.append("%s: t.CInt = %d" % (name, val))
with open(os.path.join(HERE, "_enums.vp"), "w", encoding="utf-8") as f:
f.write("\n".join(enum_lines) + "\n")
# 4. Per header file
for h in headers:
if h in ("cfl_macros.h", "cfl_enums.h"):
continue
stem = h[:-2] if h.endswith(".h") else h
if stem == "cfl":
out_name = "_cfl.vp"
elif stem.startswith("cfl_"):
out_name = "_cfl_%s.vp" % stem[4:]
else:
out_name = "_cfl_%s.vp" % stem
content = gen_header(os.path.join(SRC, h), out_name, defs)
with open(os.path.join(HERE, out_name), "w", encoding="utf-8") as f:
f.write(content)
print("generated: _types.vp, _enums.vp, %d header files" % len(headers))
def ptr_name(cls: str) -> str:
p = cls + "Ptr"
CLASS_PTRS.add(p)
return p
if __name__ == "__main__":
main()