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:
shenlei
2026-09-22 21:46:24 +09:00
co-authored by Claude Opus 5
parent fc47582017
commit 9a8e11f3dd
220 changed files with 24287 additions and 25 deletions
@@ -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())