Prepare for UIXrender and UIXsup stage 1 & 2

This commit is contained in:
Yoshi Askharoun
2026-07-22 02:24:26 -05:00
parent 08332e0d82
commit dee717296c
6 changed files with 4752 additions and 3 deletions
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""
Extracts a machine-readable schema of the Splash wire protocol from the
decompiled Microsoft.Iris.Render.Protocols.Splash.* proxy classes, so the
native UIXrender.dll message structs and msgid dispatch tables can be
generated instead of hand-transcribed.
Source of truth: UIX.RenderApi/Microsoft/Iris/Render/Protocols/Splash/**/*.cs
(decompiled from the original UIX.RenderApi.dll; not hand-written, so its
Msg*_Name structs and _priv_msgid values are exactly what shipped).
See logs/UIXrender/Architecture.md for the reasoning behind this approach.
"""
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SPLASH_DIR = ROOT / "UIX.RenderApi/Microsoft/Iris/Render/Protocols/Splash"
RE_NAMESPACE = re.compile(r"^namespace\s+([\w.]+)", re.MULTILINE)
RE_CLASS = re.compile(
r"^\s*(?:internal|public)\s+(?:sealed\s+)?class\s+(\w+)\s*(?::\s*([\w<>,.\s]+?))?\s*$",
re.MULTILINE,
)
RE_MSG_STRUCT = re.compile(
r"private struct (Msg(\d+)_(\w+))\s*\{(.*?)\}", re.DOTALL
)
RE_FIELD = re.compile(r"public\s+([\w.<>\[\]*]+)\s+(\w+);")
RE_INIT_REMOTE_CLASS = re.compile(r'InitRemoteClass\("([^"]+)"\)')
RE_BIND_ASSIGN = re.compile(
r"_priv_remoteClass_(\w+)\s*=\s*port\.InitRemoteClass\(\"([^\"]+)\"\)"
)
RE_MSGID_ASSIGN = re.compile(r"->_priv_msgid\s*=\s*(\d+)U;")
RE_INSTANCE_SCOPED = re.compile(r"this\.m_renderHandle")
FIXED_HEADER_FIELDS = {"_priv_size", "_priv_msgid", "_priv_idObjectSubject"}
def parse_file(path: Path):
text = path.read_text(encoding="utf-8-sig")
ns_match = RE_NAMESPACE.search(text)
namespace = ns_match.group(1) if ns_match else None
classes = []
for cls_match in RE_CLASS.finditer(text):
cls_name = cls_match.group(1)
base = cls_match.group(2)
base = base.strip() if base else None
# Slice out this class's body (up to the next top-level class or EOF)
start = cls_match.end()
next_match = RE_CLASS.search(text, start)
body = text[start : next_match.start() if next_match else len(text)]
messages = []
for msg_match in RE_MSG_STRUCT.finditer(body):
struct_name, msgid, msg_name, field_block = msg_match.groups()
fields = [
{"type": ftype, "name": fname}
for ftype, fname in RE_FIELD.findall(field_block)
if fname not in FIXED_HEADER_FIELDS
]
# Find the Build* method for this message to see whether it's
# instance-scoped (uses this.m_renderHandle as the subject) or
# class/static-scoped (uses a resolved *_ClassHandle field).
build_method_match = re.search(
r"Build" + re.escape(msg_name) + r"\s*\((.*?)\)\s*\{(.*?)\n \}",
body,
re.DOTALL,
)
instance_scoped = bool(
build_method_match
and RE_INSTANCE_SCOPED.search(build_method_match.group(2))
)
has_blob = any(f["type"] == "BLOBREF" for f in fields)
messages.append(
{
"msgid": int(msgid),
"name": msg_name,
"fields": fields,
"instanceScoped": instance_scoped,
"hasVariableLengthData": has_blob,
}
)
messages.sort(key=lambda m: m["msgid"])
# Class-name -> wire-name bindings only appear in the owning
# ProtocolInstance's Init() (e.g. ProtocolSplashMessaging.Init()),
# not in the Remote* file itself, so this stays empty here and is
# cross-referenced in a second pass (see collect_wire_names).
if messages or cls_name.startswith(("Remote", "Local")):
classes.append(
{
"namespace": namespace,
"class": cls_name,
"baseClass": base,
"sourceFile": str(path.relative_to(ROOT)),
"messages": messages,
}
)
return classes
def collect_wire_names(all_text: str):
"""Map short binding name (e.g. 'Broker') -> wire class name string,
from every '_priv_remoteClass_X = port.InitRemoteClass("Wire::Name")'
assignment found across all ProtocolInstance-derived binder classes."""
return {m.group(1): m.group(2) for m in RE_BIND_ASSIGN.finditer(all_text)}
def main():
if not SPLASH_DIR.is_dir():
print(f"error: {SPLASH_DIR} not found", file=sys.stderr)
sys.exit(1)
all_classes = []
all_text_parts = []
cs_files = sorted(SPLASH_DIR.rglob("*.cs"))
for path in cs_files:
text = path.read_text(encoding="utf-8-sig")
all_text_parts.append(text)
all_classes.extend(parse_file(path))
wire_names = collect_wire_names("\n".join(all_text_parts))
total_messages = sum(len(c["messages"]) for c in all_classes)
classes_with_messages = [c for c in all_classes if c["messages"]]
schema = {
"generatedFrom": str(SPLASH_DIR.relative_to(ROOT)),
"sourceFileCount": len(cs_files),
"wireClassNameBindings": wire_names,
"classes": classes_with_messages,
}
out_path = ROOT / "Tools" / "splash-schema.json"
out_path.write_text(json.dumps(schema, indent=2), encoding="utf-8")
print(f"Parsed {len(cs_files)} source files")
print(f"Found {len(classes_with_messages)} classes with message structs")
print(f"Found {total_messages} total messages")
print(f"Resolved {len(wire_names)} wire class-name bindings")
print(f"Wrote {out_path.relative_to(ROOT)}")
if __name__ == "__main__":
main()