把 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。
453 lines
16 KiB
Python
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()
|