Generate wire and data model API shims

This commit is contained in:
2026-08-08 12:18:28 +02:00
parent e2889d758d
commit 7086af9a6f
13 changed files with 153952 additions and 9018 deletions

View File

@@ -28,6 +28,45 @@ TARGETS = {
"LibreMetaverse.StructuredData": ROOT / "crates/libremetaverse-structured-data/src/generated.rs",
}
MAIN_TARGET = ROOT / "crates/libremetaverse/src/generated.rs"
WIRE_NAMESPACES = (
"LibreMetaverse.Assets",
"LibreMetaverse.Formatters",
"LibreMetaverse.Imaging",
"LibreMetaverse.ImportExport",
"LibreMetaverse.Materials",
"LibreMetaverse.Messages",
"LibreMetaverse.Packets",
"LibreMetaverse.Rendering",
)
WIRE_ROOT_TYPES = {
"T:LibreMetaverse.Animation",
"T:LibreMetaverse.BitPack",
"T:LibreMetaverse.Bumpiness",
"T:LibreMetaverse.FieldType",
"T:LibreMetaverse.GLTFMaterialUpdate",
"T:LibreMetaverse.Grass",
"T:LibreMetaverse.GrassDefinition",
"T:LibreMetaverse.Interfaces.IMessage",
"T:LibreMetaverse.LiveParticle",
"T:LibreMetaverse.MappingType",
"T:LibreMetaverse.MediaControls",
"T:LibreMetaverse.MediaEntry",
"T:LibreMetaverse.MediaPermission",
"T:LibreMetaverse.NameValue",
"T:LibreMetaverse.NameValue.ClassType",
"T:LibreMetaverse.NameValue.SendtoType",
"T:LibreMetaverse.NameValue.ValueType",
"T:LibreMetaverse.PacketFrequency",
"T:LibreMetaverse.PermissionMask",
"T:LibreMetaverse.PermissionWho",
"T:LibreMetaverse.Permissions",
"T:LibreMetaverse.Shininess",
"T:LibreMetaverse.TextureAttributes",
"T:LibreMetaverse.Tree",
"T:LibreMetaverse.TreeDefinition",
}
UNDERLYING = {
"System.SByte": "i8", "System.Byte": "u8", "System.Int16": "i16", "System.UInt16": "u16",
"System.Int32": "i32", "System.UInt32": "u32", "System.Int64": "i64", "System.UInt64": "u64",
@@ -134,6 +173,15 @@ def render_body(signature: str, member_id: str, error_model: str, trait: bool) -
return f" {signature} {{ {failure} }}"
def rust_constant_value(item: dict) -> str:
value = item["value"]["value"]
if item["type"] == "System.String":
return json.dumps(value)
if item["type"] == "System.Boolean":
return value.lower()
return value
def render_type(item: dict, type_row: dict, member_rows: dict[str, dict[str, str]], mapper: mapping.Mapper) -> str:
rust_name = type_name(type_row)
if item["kind"] == "enum":
@@ -179,8 +227,7 @@ def render_type(item: dict, type_row: dict, member_rows: dict[str, dict[str, str
row = member_rows[member["doc_id"]]
lines.append(f" /// C# member: `{member['doc_id']}`.")
if member["kind"] == "constant":
value = member["value"]["value"]
lines.append(f" {row['rust_signature']} = {value};")
lines.append(f" {row['rust_signature']} = {rust_constant_value(member)};")
continue
signatures = row["rust_signature"].split(" ; ")
for index, signature in enumerate(signatures):
@@ -199,7 +246,53 @@ def render_type(item: dict, type_row: dict, member_rows: dict[str, dict[str, str
return "\n".join(lines)
def generate_sources(catalog: dict) -> tuple[dict[Path, str], dict[str, tuple[int, int]]]:
def render_pending_type(item: dict, type_row: dict) -> str:
rust_name = type_name(type_row)
names = generic_names(item)
suffix = generic_suffix(names)
lines = [f"/// Pending catalog type `{item['doc_id']}`; completed by a later milestone issue."]
if item["kind"] == "interface":
lines.append(f"pub trait {rust_name}{suffix} {{}}")
elif names:
tuple_type = "(" + ", ".join(names) + ("," if len(names) == 1 else "") + ")"
lines += [f"pub struct {rust_name}{suffix} {{", f" _marker: std::marker::PhantomData<{tuple_type}>,", "}"]
else:
lines.append(f"pub struct {rust_name};")
return "\n".join(lines)
def wire_type(item: dict) -> bool:
return (
item["doc_id"] in WIRE_ROOT_TYPES
or item["doc_id"].startswith("T:LibreMetaverse.Primitive")
or item["namespace"].startswith(WIRE_NAMESPACES)
)
def render_modules(items: list[tuple[dict, dict, bool]], member_rows: dict[str, dict[str, str]], mapper: mapping.Mapper) -> str:
tree: dict = {"items": [], "children": {}}
for item, type_row, complete in items:
node = tree
for segment in filter(None, type_row["rust_module"].split("::")):
node = node["children"].setdefault(segment, {"items": [], "children": {}})
node["items"].append((item, type_row, complete))
def render_node(node: dict) -> list[str]:
lines = []
for item, type_row, complete in node["items"]:
rendered = render_type(item, type_row, member_rows, mapper) if complete else render_pending_type(item, type_row)
lines += [rendered, ""]
for name, child in sorted(node["children"].items()):
body = render_node(child)
lines.append(f"pub mod {name} {{")
lines += [(" " + line) if line else "" for line in body]
lines += ["}", ""]
return lines
return "\n".join(render_node(tree))
def generate_sources(catalog: dict) -> tuple[dict[Path, str], dict[str, tuple[int, int, bool]]]:
type_rows, resolved = mapping.build_type_rows(catalog)
type_by_id = {row["csharp_type_id"]: row for row in type_rows}
interfaces = {
@@ -216,7 +309,7 @@ def generate_sources(catalog: dict) -> tuple[dict[Path, str], dict[str, tuple[in
coverage = {}
for assembly in catalog["assemblies"]:
name = assembly["identity"]["name"]
if name not in TARGETS:
if name not in TARGETS and name != "LibreMetaverse":
continue
chunks = [
"// @generated by tools/generate_api_shims.py; do not edit by hand.",
@@ -227,11 +320,16 @@ def generate_sources(catalog: dict) -> tuple[dict[Path, str], dict[str, tuple[in
"#![allow(non_snake_case)]",
"",
]
for item in assembly["types"]:
chunks += [render_type(item, type_by_id[item["doc_id"]], member_rows, mapper), ""]
selected = assembly["types"] if name in TARGETS else [item for item in assembly["types"] if wire_type(item)]
selected_ids = {item["doc_id"] for item in selected}
rendered_items = [
(item, type_by_id[item["doc_id"]], item["doc_id"] in selected_ids)
for item in assembly["types"]
]
chunks.append(render_modules(rendered_items, member_rows, mapper))
source = format_rust("\n".join(chunks).rstrip() + "\n")
expected_types = {item["doc_id"] for item in assembly["types"]}
expected_members = {member["doc_id"] for item in assembly["types"] for member in item["members"]}
expected_types = selected_ids
expected_members = {member["doc_id"] for item in selected for member in item["members"]}
emitted_types = set(re.findall(r"C# type: `(.*)`\.", source))
emitted_members = set(re.findall(r"C# member: `(.*)`\.", source))
if emitted_types != expected_types or emitted_members != expected_members:
@@ -240,12 +338,12 @@ def generate_sources(catalog: dict) -> tuple[dict[Path, str], dict[str, tuple[in
f"types missing/stale={len(expected_types - emitted_types)}/{len(emitted_types - expected_types)}, "
f"members missing/stale={len(expected_members - emitted_members)}/{len(emitted_members - expected_members)}"
)
outputs[TARGETS[name]] = source
coverage[name] = (len(assembly["types"]), sum(len(item["members"]) for item in assembly["types"]))
outputs[TARGETS.get(name, MAIN_TARGET)] = source
coverage[name] = (len(selected), sum(len(item["members"]) for item in selected), len(selected) == len(assembly["types"]))
return outputs, coverage
def coverage_report(catalog: dict, coverage: dict[str, tuple[int, int]]) -> str:
def coverage_report(catalog: dict, coverage: dict[str, tuple[int, int, bool]]) -> str:
lines = [
"# Callable shim coverage", "", "Generated by `python3 tools/generate_api_shims.py`; do not edit by hand.", "",
"| Assembly | Types | Members | Status |", "|---|---:|---:|---|",
@@ -256,15 +354,22 @@ def coverage_report(catalog: dict, coverage: dict[str, tuple[int, int]]) -> str:
types = len(assembly["types"])
members = sum(len(item["members"]) for item in assembly["types"])
if name in coverage:
got_types, got_members = coverage[name]
if (types, members) != (got_types, got_members):
got_types, got_members, complete = coverage[name]
if complete and (types, members) != (got_types, got_members):
raise ValueError(f"incomplete generated slice for {name}")
status = "callable failure-only shim"
total_types += types
total_members += members
status = "callable failure-only shim" if complete else f"partial callable shim ({got_types:,} types / {got_members:,} members)"
total_types += got_types
total_members += got_members
else:
status = "pending milestone issue"
lines.append(f"| `{name}` | {types:,} | {members:,} | {status} |")
if "LibreMetaverse" in coverage and not coverage["LibreMetaverse"][2]:
lines += [
"",
"The partial `LibreMetaverse` row is milestone issue #7's wire/data slice: packets, messages, assets,",
"GLTF, archives/import-export, materials, rendering DTOs, bit packing, permissions, primitives, and",
"texture/media/particle records. Other main-assembly types remain pending shells for issues #8-#10.",
]
lines += ["", f"Current callable coverage: **{total_types:,} types / {total_members:,} members**.", ""]
return "\n".join(lines)