Files
raven/scripts/generate-com-tables.py

111 lines
5.0 KiB
Python

#!/usr/bin/env python3
"""Write the method tables of the COM interfaces that a Raven component implements.
Every interface starts as a table of stubs, one per method in the order of the public headers, so an object can be completed one method
at a time and a call to a method nobody wrote shows up by name. The output holds, for each interface, an enumeration of the method
positions, the stubs that report themselves through the component's `unimplemented` function, and the default table.
The methods are read from the MinGW headers in any of their three forms: a C method table that starts from its base interface's table
(`IBaseVtbl Base;`), a C method table that lists every method, or only the C++ declaration `interface Name : public Base { ... };`.
usage: generate-com-tables.py INCLUDE_DIRECTORY OUTPUT.h TITLE HEADER:INTERFACE...
"""
import re
import sys
from pathlib import Path
UNKNOWN = [("QueryInterface", 2), ("AddRef", 0), ("Release", 0)]
METHOD = re.compile(r"STDMETHOD_?\(\s*(?:[^,()]+,\s*)?(\w+)\s*\)|\(STDMETHODCALLTYPE\s*\*\s*(\w+)\)")
# Arguments wider than a 32-bit word when passed by value: a 32-bit caller pushes this many words for them.
WIDE = {"VARIANT": 4, "DECIMAL": 4, "DATE": 2, "double": 2, "LONGLONG": 2, "ULONGLONG": 2, "INT64": 2, "UINT64": 2, "LARGE_INTEGER": 2,
"ULARGE_INTEGER": 2, "FILETIME": 2, "CY": 2, "POINT": 2, "SIZE": 2, "RECT": 4, "D2D1_POINT_2F": 2, "D2D1_SIZE_F": 2}
def body_of(text, opening):
"""The text between the brace that ends `opening` and its matching close."""
start = text.index("{", opening.end() - 1)
depth = 0
for at in range(start, len(text)):
if text[at] == "{":
depth += 1
elif text[at] == "}":
depth -= 1
if not depth:
return text[start + 1:at]
raise SystemExit("unbalanced braces")
def words(body, match):
"""The 32-bit stack words of the arguments after `This` that the C method table gives the method, which a stub must take so that it
leaves a 32-bit caller's stack as the real method would. A C++ declaration has no table and counts none."""
if not match.group(2):
return 0
rest = body[match.end():]
start = rest.index("(")
depth, at = 0, start
for at in range(start, len(rest)):
depth += {"(": 1, ")": -1}.get(rest[at], 0)
if not depth:
break
arguments = [argument.strip() for argument in rest[start + 1:at].split(",")][1:]
total = 0
for argument in arguments:
if "*" in argument:
total += 1
continue
kind = argument.replace("const ", "").split()[0] if argument else ""
total += WIDE.get(kind, 1)
return total
def methods(headers, name):
if name == "IUnknown":
return list(UNKNOWN)
for text in headers:
table = re.search(r"typedef struct %sVtbl\s*\{" % name, text)
if table:
body = body_of(text, table)
base = re.search(r"\b(\w+)Vtbl\s+Base\s*;", body)
found = methods(headers, base.group(1)) if base else []
found += [(m.group(1) or m.group(2), words(body, m)) for m in METHOD.finditer(body)]
return found
for text in headers:
declaration = re.search(r"interface\s+%s\s*:\s*public\s+(\w+)\s*\{" % name, text)
if declaration:
body = body_of(text, declaration)
return methods(headers, declaration.group(1)) + [(m.group(1) or m.group(2), 0) for m in METHOD.finditer(body)]
raise SystemExit(f"no methods found for {name}")
def main():
include, output, title = Path(sys.argv[1]), Path(sys.argv[2]), sys.argv[3]
wanted = [item.split(":") for item in sys.argv[4:]]
headers = [(include / header).read_text() for header in dict.fromkeys(header for header, _ in wanted)]
out = [f"/* Generated by scripts/generate-com-tables.py from the {title} headers. Do not edit. */\n",
"/* The stubs return E_NOTIMPL and say which method was called; a table starts as all stubs. */\n"]
for _, name in wanted:
found = methods(headers, name)
names = [method for method, _ in found]
# An overloaded name takes its position as a suffix after its first use.
seen = {}
for index, method in enumerate(names):
if method in seen:
names[index] = f"{method}_{index}"
seen.setdefault(method, index)
out.append("\nenum {\n")
for index, method in enumerate(names):
out.append(f"\t{name}_{method}_slot = {index},\n")
out.append(f"\t{name}_methods = {len(names)}\n}};\n")
for index, method in enumerate(names):
arguments = "".join(f", DWORD a{word}" for word in range(found[index][1]))
out.append(f'static HRESULT STDMETHODCALLTYPE {name}_stub_{index}(void *self{arguments}) {{ return unimplemented("{name}", "{method}"); }}\n')
out.append(f"static void *{name}_table[{len(names)}] = {{\n")
for index in range(len(names)):
out.append(f"\t(void *){name}_stub_{index},\n")
out.append("};\n")
output.write_text("".join(out))
main()