port 2A step 4: platform skeleton + rest of the 2V3 header closure
- Mirror the remaining non-Python 2V3 closure headers (98 more, 173 total): EffectLib, EterGrnLib, SpeedTreeLib, SphereLib, PRTerrainLib, EterImageLib, EterBase/Poly, cipher/tea, EterLib Grp*/IME/Input/MSWindow/StateManager, GameLib actor/map/race headers, EterPack. - SDK shims: granny.h, SpeedTreeRT.h, imm.h, Dimm.h (the SDK MIDL header, not mirrored), dinput/d3dx8 additions; D3D8 render-state enums, FVF macros and the COM resource hierarchy; more Win32 GDI/handle types with layout static_asserts. - PORT edits for MSVC-permissive code (typename, friend visibility, <algorithm>, pointer→uintptr_t) and PRTerrainLib/StdAfx.h dropping the ScriptLib/Python include. - port_logic links the vendored cryptopp/minilzo. - extension/src/platform: port_platform target with 103 generated stub files (1917 function stubs, 146 static members) from the new platform_stub.py (clang -ast-dump-filter); MT_PLATFORM_STUB() marks every unimplemented body. libmtgodot links port_platform and port_gate.sh builds it. Gate: macOS, Android, iOS, Windows (mingw) PASS; Linux BLOCKED (no toolchain). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -151,6 +151,10 @@ behavior verified from code inspection alone.
|
||||
- `scripts/port_copy.py copy <Lib/File>... | diff [<Lib/File>...]` — copy 40250 files into the
|
||||
`extension/src/port/` mirror with mechanical edits only (CP949→UTF-8, LF, include-path case); `diff`
|
||||
lists the manual `// PORT:` edits.
|
||||
- `scripts/platform_stub.py list | gen [<Lib/File.h>...] [--force]` — generate the empty platform
|
||||
skeleton `extension/src/platform/<Lib>/<File>.cpp` for platform-layer mirror headers from clang's AST
|
||||
(needs `script/port_gate.sh macos` for compile commands); never overwrites without `--force`.
|
||||
`grep -r MT_PLATFORM_STUB extension/src/platform` lists the platform functions still unimplemented.
|
||||
- `scripts/port_deps.py closure <file> | slices [--write] | order [--write]` — `#include` graph with the
|
||||
implicit edges each library's `StdAfx.h` supplies; writes the 2V0-2V3 unit lists and the batch-2
|
||||
topological order to `audit/slices/`. Regenerate after changing a slice definition.
|
||||
|
||||
@@ -0,0 +1,398 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the empty platform skeleton for mirrored platform-layer headers (docs/PORT-PLAN.md 2A).
|
||||
|
||||
platform_stub.py list platform-layer headers in the mirror, with their stub state
|
||||
platform_stub.py gen [<header>...] [--force] write extension/src/platform/<Lib>/<File>.cpp
|
||||
(no args: every platform header; never overwrites
|
||||
without --force, so hand-written implementations stay)
|
||||
|
||||
`<header>` is a mirror path such as `EterLib/GrpBase.h`. A header is platform-layer when its same-stem
|
||||
40250 .cpp unit is (port_map.py LAYER_PATTERNS), or, without one, when its own path matches.
|
||||
|
||||
A stub file defines everything the header declares and does not define: non-pure, non-template member
|
||||
functions, constructors/destructors, free functions, and static data members. Declarations come from
|
||||
clang's AST of the header's gate translation unit (the library StdAfx.h + the header, with the exact
|
||||
flags from the macOS gate build's compile_commands.json: `script/port_gate.sh macos` first), dumped per
|
||||
class/function name with -ast-dump-filter. Definitions use trailing return types, so types nested in the
|
||||
class resolve in class scope, and `decltype(C::x) C::x{}` for static data. Each body is
|
||||
MT_PLATFORM_STUB() plus a default return (platform/PlatformStub.h); `grep -r MT_PLATFORM_STUB` is the
|
||||
list of platform functions still to implement.
|
||||
|
||||
Not covered: platform .cpp units that define members of logic headers (GameLib/MapOutdoorRender*.cpp for
|
||||
MapOutdoor.h, ...); those members get their stubs by hand when the logic side needs them.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import concurrent.futures
|
||||
import json
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import port_map
|
||||
import refroot # script directory is on sys.path when run directly
|
||||
|
||||
REPO = refroot.repo_root()
|
||||
PORT = REPO / "extension" / "src" / "port"
|
||||
PLATFORM = REPO / "extension" / "src" / "platform"
|
||||
GATE_BUILD = REPO / "build-port-gate" / "macos"
|
||||
MARK = "platform_stub.py"
|
||||
|
||||
COMMENT = re.compile(r"//[^\n]*|/\*.*?\*/", re.S)
|
||||
LITERAL = re.compile(r'"(?:\\.|[^"\\\n])*"|\'(?:\\.|[^\'\\\n])*\'')
|
||||
PREPROC = re.compile(r"^[ \t]*#(?:[^\n]*\\\n)*[^\n]*", re.M)
|
||||
RECORD = re.compile(r"\b(?:class|struct|union)\s+(?:__declspec\s*\([^)]*\)\s*)?([A-Za-z_]\w*)\s*(?:final\s*)?(?::[^;{}]*)?$")
|
||||
|
||||
|
||||
# --- header scan: names of top-level records and free functions --------------------------------------
|
||||
|
||||
def blank(text: str) -> str:
|
||||
"""Comments, literals and preprocessor lines replaced by spaces (offsets and newlines kept)."""
|
||||
keep_nl = lambda m: re.sub(r"[^\n]", " ", m.group(0))
|
||||
text = COMMENT.sub(keep_nl, text)
|
||||
text = LITERAL.sub(lambda m: '""', text)
|
||||
return PREPROC.sub(keep_nl, text)
|
||||
|
||||
|
||||
def scan(header: Path) -> tuple[list[str], list[str]]:
|
||||
"""Record names defined, and function names declared, at namespace scope of the header."""
|
||||
text = blank(header.read_text(encoding="utf-8"))
|
||||
records: list[str] = []
|
||||
functions: list[str] = []
|
||||
stack: list[str] = [] # "ns" (namespace / extern "C") or "body"
|
||||
stmt_start = 0
|
||||
for i, ch in enumerate(text):
|
||||
if ch == "{":
|
||||
head = text[stmt_start:i].strip()
|
||||
if re.search(r"\bnamespace\b[^;]*$|\bextern\s*\"\"\s*$", head):
|
||||
stack.append("ns")
|
||||
else:
|
||||
if all(k == "ns" for k in stack) and (m := RECORD.search(head)) \
|
||||
and not re.search(r"\btemplate\s*<", head):
|
||||
records.append(m.group(1))
|
||||
stack.append("body")
|
||||
stmt_start = i + 1
|
||||
elif ch == "}":
|
||||
if stack:
|
||||
stack.pop()
|
||||
stmt_start = i + 1
|
||||
elif ch == ";":
|
||||
if all(k == "ns" for k in stack):
|
||||
head = " ".join(text[stmt_start:i].split())
|
||||
if (head and not re.match(r"(?:typedef|using|template|friend|static_assert|enum|class|struct|union)\b", head)
|
||||
and not re.search(r"\binline\b", head)):
|
||||
if m := re.search(r"([A-Za-z_]\w*|operator\s*\S+?)\s*\((?!\s*\*)", head):
|
||||
if m.group(1) not in ("__declspec", "decltype", "sizeof", "alignas"):
|
||||
functions.append(m.group(1))
|
||||
stmt_start = i + 1
|
||||
return list(dict.fromkeys(records)), list(dict.fromkeys(functions))
|
||||
|
||||
|
||||
# --- clang AST ---------------------------------------------------------------------------------------
|
||||
|
||||
def gate_command(rel: str) -> tuple[list[str], str]:
|
||||
db = GATE_BUILD / "compile_commands.json"
|
||||
if not db.is_file():
|
||||
raise SystemExit(f"{db}: missing; run `script/port_gate.sh macos` (it exports compile commands)")
|
||||
tu = re.sub(r"[^A-Za-z0-9_]", "_", rel)
|
||||
for entry in json.loads(db.read_text()):
|
||||
if Path(entry["file"]).name == f"{tu}.cpp":
|
||||
args = shlex.split(entry["command"])
|
||||
out = args.index("-o")
|
||||
del args[out:out + 2]
|
||||
args.remove("-c")
|
||||
args.remove(entry["file"])
|
||||
return args, entry["file"]
|
||||
raise SystemExit(f"{rel}: no gate translation unit in {db}; reconfigure the gate build")
|
||||
|
||||
|
||||
def dump(args: list[str], tu: str, name: str) -> list[dict]:
|
||||
cmd = args + ["-w", "-fsyntax-only", "-Xclang", "-ast-dump=json", "-Xclang", f"-ast-dump-filter={name}", tu]
|
||||
res = subprocess.run(cmd, capture_output=True, text=True, cwd=GATE_BUILD)
|
||||
if res.returncode:
|
||||
raise SystemExit(f"clang failed for {tu} ({name}):\n{res.stderr[-2000:]}")
|
||||
s, i, out = res.stdout, 0, []
|
||||
dec = json.JSONDecoder()
|
||||
while True:
|
||||
while i < len(s) and s[i].isspace():
|
||||
i += 1
|
||||
if i >= len(s):
|
||||
return out
|
||||
if s[i] != "{": # "Dumping X:" banner
|
||||
i = s.find("\n", i) + 1 or len(s)
|
||||
continue
|
||||
node, i = dec.raw_decode(s, i)
|
||||
out.append(node)
|
||||
|
||||
|
||||
def annotate_files(nodes: list[dict]) -> None:
|
||||
"""Give every node `_file`: clang writes a loc's file only when it changes, in document order."""
|
||||
last = [""]
|
||||
|
||||
def visit_loc(loc):
|
||||
if isinstance(loc, dict):
|
||||
for key in ("spellingLoc", "expansionLoc", "begin", "end"):
|
||||
if key in loc:
|
||||
visit_loc(loc[key])
|
||||
if "file" in loc:
|
||||
last[0] = loc["file"]
|
||||
|
||||
def visit(node):
|
||||
loc = node.get("loc", {})
|
||||
if "expansionLoc" in loc:
|
||||
visit_loc(loc["spellingLoc"])
|
||||
visit_loc(loc["expansionLoc"])
|
||||
else:
|
||||
visit_loc(loc)
|
||||
node["_file"] = last[0]
|
||||
visit_loc(node.get("range"))
|
||||
for child in node.get("inner", []):
|
||||
visit(child)
|
||||
|
||||
for n in nodes:
|
||||
visit(n)
|
||||
|
||||
|
||||
def has_body(node: dict) -> bool:
|
||||
return any(c.get("kind") == "CompoundStmt" for c in node.get("inner", [])) \
|
||||
or "explicitlyDefaulted" in node or "explicitlyDeleted" in node
|
||||
|
||||
|
||||
def split_function_type(qual: str) -> tuple[str, str]:
|
||||
"""`R (P...) quals` -> (R, quals)."""
|
||||
depth = 0
|
||||
for i in range(len(qual) - 1, -1, -1):
|
||||
c = qual[i]
|
||||
if c == ")":
|
||||
depth += 1
|
||||
elif c == "(":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
close = match_close(qual, i)
|
||||
rest = qual[close + 1:].strip()
|
||||
if re.fullmatch(r"(?:const|volatile|&&?|noexcept(?:\(.*\))?|throw\(.*\)|\s)*", rest):
|
||||
return qual[:i].strip(), rest
|
||||
raise ValueError(qual)
|
||||
|
||||
|
||||
def match_close(s: str, open_idx: int) -> int:
|
||||
depth = 0
|
||||
for j in range(open_idx, len(s)):
|
||||
if s[j] == "(":
|
||||
depth += 1
|
||||
elif s[j] == ")":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return j
|
||||
raise ValueError(s)
|
||||
|
||||
|
||||
def params(node: dict) -> str:
|
||||
ps = [c["type"]["qualType"] for c in node.get("inner", []) if c.get("kind") == "ParmVarDecl"]
|
||||
if node.get("variadic"):
|
||||
ps.append("...")
|
||||
return ", ".join(ps)
|
||||
|
||||
|
||||
def function_def(qname: str, node: dict) -> str:
|
||||
ret, quals = split_function_type(node["type"]["qualType"])
|
||||
quals = f" {quals}" if quals else ""
|
||||
kind = node["kind"]
|
||||
if kind == "CXXDestructorDecl": # clang spells the implicit noexcept of a destructor into its type
|
||||
quals = re.sub(r"\s*\bnoexcept\b", "", quals)
|
||||
if kind in ("CXXConstructorDecl", "CXXDestructorDecl"):
|
||||
return f"{qname}({params(node)}){quals}\n{{\n\tMT_PLATFORM_STUB();\n}}\n"
|
||||
if kind == "CXXConversionDecl":
|
||||
return (f"{qname}({params(node)}){quals}\n{{\n\tMT_PLATFORM_STUB();\n"
|
||||
f"\treturn mt_platform_stub_return<{ret}>();\n}}\n")
|
||||
body = "\tMT_PLATFORM_STUB();\n" + ("" if ret == "void" else f"\treturn mt_platform_stub_return<{ret}>();\n")
|
||||
return f"auto {qname}({params(node)}){quals} -> {ret}\n{{\n{body}}}\n"
|
||||
|
||||
|
||||
class Collector:
|
||||
def __init__(self, header_file: str):
|
||||
self.header_file = header_file
|
||||
self.defined: set[str] = set() # mangled names with a body anywhere in the dump
|
||||
self.decls: list[tuple[str, str, dict]] = [] # (kind, qualified name, node)
|
||||
self.seen: set[str] = set()
|
||||
|
||||
def note_bodies(self, node: dict) -> None:
|
||||
if node.get("mangledName") and has_body(node):
|
||||
self.defined.add(node["mangledName"])
|
||||
for c in node.get("inner", []):
|
||||
self.note_bodies(c)
|
||||
|
||||
def record(self, node: dict, scope: str) -> None:
|
||||
if node.get("kind") != "CXXRecordDecl" or not node.get("completeDefinition") or not node.get("name"):
|
||||
return
|
||||
q = f"{scope}{node['name']}"
|
||||
for c in node.get("inner", []):
|
||||
kind = c.get("kind")
|
||||
if kind == "CXXRecordDecl" and c.get("isImplicit"):
|
||||
continue
|
||||
if kind == "CXXRecordDecl":
|
||||
self.record(c, f"{q}::")
|
||||
elif kind in ("CXXMethodDecl", "CXXConstructorDecl", "CXXDestructorDecl", "CXXConversionDecl"):
|
||||
if c.get("isImplicit") or c.get("pure") or has_body(c):
|
||||
continue
|
||||
self.add("fn", f"{q}::{c['name']}", c)
|
||||
elif kind == "VarDecl" and c.get("storageClass") == "static":
|
||||
if c.get("init") or c.get("constexpr") or c.get("inline"):
|
||||
continue
|
||||
self.add("var", f"{q}::{c['name']}", c)
|
||||
|
||||
def add(self, kind: str, qname: str, node: dict) -> None:
|
||||
key = node.get("mangledName") or qname
|
||||
if key in self.seen:
|
||||
return
|
||||
self.seen.add(key)
|
||||
self.decls.append((kind, qname, node))
|
||||
|
||||
def root(self, node: dict, scope: str) -> None:
|
||||
if node["_file"] != self.header_file:
|
||||
return
|
||||
kind = node.get("kind")
|
||||
if kind == "CXXRecordDecl":
|
||||
self.record(node, scope)
|
||||
elif kind == "FunctionDecl" and not has_body(node) and node.get("storageClass") != "static" \
|
||||
and not node.get("inline"):
|
||||
self.add("fn", f"{scope}{node['name']}", node)
|
||||
|
||||
def render(self) -> list[str]:
|
||||
out = []
|
||||
for kind, qname, node in self.decls:
|
||||
if node.get("mangledName") in self.defined:
|
||||
continue
|
||||
if kind == "var":
|
||||
out.append(f"decltype({qname}) {qname}{{}};\n")
|
||||
else:
|
||||
out.append(function_def(qname, node))
|
||||
return out
|
||||
|
||||
|
||||
def namespaces(header: Path) -> dict[str, str]:
|
||||
"""Name -> enclosing namespace prefix, for names declared inside `namespace X {`."""
|
||||
text = blank(header.read_text(encoding="utf-8"))
|
||||
result: dict[str, str] = {}
|
||||
for m in re.finditer(r"\bnamespace\s+(\w+)\s*\{", text):
|
||||
depth, j = 1, m.end()
|
||||
while depth and j < len(text):
|
||||
depth += {"{": 1, "}": -1}.get(text[j], 0)
|
||||
j += 1
|
||||
for n in re.findall(r"\b(?:class|struct)\s+(\w+)|(\w+)\s*\(", text[m.end():j]):
|
||||
result.setdefault(n[0] or n[1], f"{m.group(1)}::")
|
||||
return result
|
||||
|
||||
|
||||
def generate(rel: str) -> str:
|
||||
header = PORT / rel
|
||||
records, functions = scan(header)
|
||||
args, tu = gate_command(rel)
|
||||
ns = namespaces(header)
|
||||
col = Collector(str(header))
|
||||
with concurrent.futures.ThreadPoolExecutor(8) as pool:
|
||||
dumps = list(pool.map(lambda n: (n, dump(args, tu, n)), records + functions))
|
||||
for name, nodes in dumps:
|
||||
annotate_files(nodes)
|
||||
for n in nodes:
|
||||
col.note_bodies(n)
|
||||
for name, nodes in dumps:
|
||||
for n in nodes:
|
||||
if n.get("name") == name:
|
||||
col.root(n, ns.get(name, ""))
|
||||
body = col.render()
|
||||
if not body:
|
||||
return ""
|
||||
lib = rel.split("/")[0]
|
||||
stdafx = next((p.name for p in (PORT / lib).glob("*") if p.name.lower() == "stdafx.h"), None)
|
||||
unit = unit_of(rel)
|
||||
lines = [f"// Platform skeleton for {rel}"
|
||||
+ (f" (40250 {unit})" if unit else "") + f", generated by {MARK}.\n",
|
||||
"// Every MT_PLATFORM_STUB() body is unimplemented: replace it with the platform implementation.\n"]
|
||||
if stdafx and f"{lib}/{stdafx}" != rel:
|
||||
lines.append(f'#include "{lib}/{stdafx}"\n')
|
||||
lines += [f'#include "{rel}"\n', "\n", f'#include "{"../" * rel.count("/")}PlatformStub.h"\n', "\n"]
|
||||
return "".join(lines) + "\n".join(body)
|
||||
|
||||
|
||||
# --- selection ---------------------------------------------------------------------------------------
|
||||
|
||||
_UNITS: dict[str, str] | None = None
|
||||
|
||||
|
||||
def unit_of(rel: str) -> str | None:
|
||||
global _UNITS
|
||||
if _UNITS is None:
|
||||
_UNITS = {u.lower(): u for u in port_map.inventory(refroot.reference_root())}
|
||||
return _UNITS.get((rel.rsplit(".", 1)[0] + ".cpp").lower())
|
||||
|
||||
|
||||
def platform_headers() -> list[str]:
|
||||
out = []
|
||||
for h in sorted(PORT.rglob("*.h")):
|
||||
rel = h.relative_to(PORT).as_posix()
|
||||
if rel.startswith("common/"):
|
||||
continue
|
||||
unit = unit_of(rel)
|
||||
if port_map.layer(unit or rel) == "platform":
|
||||
out.append(rel)
|
||||
return out
|
||||
|
||||
|
||||
def stub_path(rel: str) -> Path:
|
||||
return PLATFORM / (rel.rsplit(".", 1)[0] + ".cpp")
|
||||
|
||||
|
||||
def cmd_list(_args) -> int:
|
||||
for rel in platform_headers():
|
||||
p = stub_path(rel)
|
||||
state = "-" if not p.exists() else ("generated" if MARK in p.read_text(encoding="utf-8")[:300] else "edited")
|
||||
pending = p.read_text(encoding="utf-8").count("MT_PLATFORM_STUB();") if p.exists() else 0
|
||||
print(f"{rel:48} {state:9} {pending:4} stubs")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_gen(args) -> int:
|
||||
headers = platform_headers()
|
||||
targets = headers if not args.headers else []
|
||||
for h in args.headers:
|
||||
hit = [r for r in headers if r.lower() == h.lower()]
|
||||
if not hit:
|
||||
raise SystemExit(f"{h}: not a platform-layer header in the mirror")
|
||||
targets += hit
|
||||
written = 0
|
||||
for rel in targets:
|
||||
path = stub_path(rel)
|
||||
if path.exists() and not args.force:
|
||||
print(f"skip {path.relative_to(REPO)} (exists; --force to regenerate)")
|
||||
continue
|
||||
text = generate(rel)
|
||||
if not text:
|
||||
print(f"none {rel} (declares nothing to define)")
|
||||
continue
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text, encoding="utf-8")
|
||||
written += 1
|
||||
print(f"wrote {path.relative_to(REPO)} ({text.count('MT_PLATFORM_STUB();')} stubs)")
|
||||
print(f"{written} file(s) written")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
sub.add_parser("list").set_defaults(fn=cmd_list)
|
||||
g = sub.add_parser("gen")
|
||||
g.add_argument("headers", nargs="*")
|
||||
g.add_argument("--force", action="store_true")
|
||||
g.set_defaults(fn=cmd_gen)
|
||||
args = ap.parse_args()
|
||||
return args.fn(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user