mirror of
https://github.com/ZuneDev/MicrosoftIris.git
synced 2026-07-27 13:13:29 -07:00
150 lines
5.6 KiB
Python
150 lines
5.6 KiB
Python
#!/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()
|