Generate extension API shims

This commit is contained in:
2026-08-08 12:58:50 +02:00
parent 1291234e6f
commit 643762fd1d
19 changed files with 12766 additions and 1312 deletions

View File

@@ -21,11 +21,16 @@ COVERAGE = ROOT / "api/SHIM-COVERAGE.md"
TARGETS = {
"LibreMetaverse.Imaging.Abstractions": ROOT / "crates/libremetaverse-imaging/src/generated.rs",
"LibreMetaverse.Imaging.Skia": ROOT / "crates/libremetaverse-imaging-skia/src/generated.rs",
"LibreMetaverse.LslTools": ROOT / "crates/libremetaverse-lsl-tools/src/generated.rs",
"LibreMetaverse.PrimMesher": ROOT / "crates/libremetaverse-prim-mesher/src/generated.rs",
"LibreMetaverse.RLV": ROOT / "crates/libremetaverse-rlv/src/generated.rs",
"LibreMetaverse.Rendering.MeshFoundry": ROOT / "crates/libremetaverse-rendering-mesh-foundry/src/generated.rs",
"LibreMetaverse.Rendering.Simple": ROOT / "crates/libremetaverse-rendering-simple/src/generated.rs",
"LibreMetaverse.Types": ROOT / "crates/libremetaverse-types/src/generated.rs",
"LibreMetaverse.StructuredData": ROOT / "crates/libremetaverse-structured-data/src/generated.rs",
"LibreMetaverse.Utilities": ROOT / "crates/libremetaverse-utilities/src/generated.rs",
"LibreMetaverse.Voice.Vivox": ROOT / "crates/libremetaverse-voice-vivox/src/generated.rs",
"LibreMetaverse.Voice.WebRTC": ROOT / "crates/libremetaverse-voice-webrtc/src/generated.rs",
}
MAIN_TARGET = ROOT / "crates/libremetaverse/src/generated.rs"
@@ -297,12 +302,6 @@ PRIVATE_LAYOUTS = {
"T:LibreMetaverse.StructuredData.OSDUri": [("value", "libremetaverse_types::compat::Uri")],
"T:LibreMetaverse.Imaging.ManagedImage": [("data", "Vec<u8>")],
}
BOXED_FIELDS = {
"F:LibreMetaverse.Caps.Simulator",
"F:LibreMetaverse.Simulator.Caps",
}
def generic_names(item: dict) -> list[str]:
return [parameter["name"] for parameter in item.get("generic_parameters", [])]
@@ -418,16 +417,14 @@ def render_type(item: dict, type_row: dict, member_rows: dict[str, dict[str, str
lines.append(f"pub struct {rust_name};")
else:
lines.append(f"pub struct {rust_name}{suffix} {{")
generics = set(names)
for field in fields:
field_type = mapper.type(field["type"], field.get("nullability"), generics)
if field["doc_id"] in BOXED_FIELDS:
field_type = (
f"Option<Box<{field_type[7:-1]}>>"
if field_type.startswith("Option<")
else f"Box<{field_type}>"
)
lines += [f" /// C# member: `{field['doc_id']}`.", f" pub {mapping.snake(field['name'])}: {field_type},"]
field_signature = member_rows[field["doc_id"]]["rust_signature"]
field_name_match = re.fullmatch(r"pub ([^:]+): (.+)", field_signature)
if not field_name_match:
raise ValueError(f"invalid mapped field signature: {field_signature}")
field_name = field_name_match.group(1)
field_type = field_name_match.group(2)
lines += [f" /// C# member: `{field['doc_id']}`.", f" pub {field_name}: {field_type},"]
for field_name, field_type in private_fields:
lines.append(f" {field_name}: {field_type},")
if names:
@@ -534,10 +531,25 @@ def render_modules(items: list[tuple[dict, dict, bool]], member_rows: dict[str,
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}
member_by_id = {
member["doc_id"]: member
for assembly in catalog["assemblies"]
for item in assembly["types"]
for member in item["members"]
}
configured_main_ids = WIRE_ROOT_TYPES | CORE_ROOT_TYPES | AVATAR_ROOT_TYPES
missing_configured_ids = configured_main_ids - type_by_id.keys()
if missing_configured_ids:
raise ValueError(f"unknown configured main-slice IDs: {sorted(missing_configured_ids)}")
missing_boxed_fields = mapping.BOXED_FIELD_IDS - member_by_id.keys()
if missing_boxed_fields:
raise ValueError(f"unknown configured boxed fields: {sorted(missing_boxed_fields)}")
if invalid_boxed_fields := [
member_id
for member_id in mapping.BOXED_FIELD_IDS
if member_by_id[member_id]["kind"] != "field"
]:
raise ValueError(f"configured boxed members are not fields: {sorted(invalid_boxed_fields)}")
interfaces = {
*{mapping.type_parts(item["signature"])[0] for assembly in catalog["assemblies"] for item in assembly["types"] if item["kind"] == "interface"},
*{

View File

@@ -170,6 +170,21 @@ SUPPORT_TYPES = {
MEMBER_RETURN_OVERRIDES = {
"M:LibreMetaverse.Caps.Capabilities": "Vec<String>",
}
BOXED_FIELD_IDS = {
"F:LibreMetaverse.Caps.Simulator",
"F:LibreMetaverse.Simulator.Caps",
"F:LibreMetaverse.LslTools.CommentList.tail",
"F:LibreMetaverse.LslTools.CSymbol.m_parser",
"F:LibreMetaverse.LslTools.CSymbol.m_prod",
"F:LibreMetaverse.LslTools.CSymbol.m_refSymbol",
"F:LibreMetaverse.LslTools.Dfa.Action.a_next",
"F:LibreMetaverse.LslTools.LineList.tail",
"F:LibreMetaverse.LslTools.ParseState.m_sgen",
"F:LibreMetaverse.LslTools.Precedence.m_next",
"F:LibreMetaverse.LslTools.Regex.m_sub",
"F:LibreMetaverse.LslTools.SymbolSet.m_next",
"F:LibreMetaverse.LslTools.SymbolSet.m_symbols",
}
RUST_KEYWORDS = {
"as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn",
"for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref",
@@ -592,6 +607,12 @@ def member_signature(mapper: Mapper, owner: dict, item: dict, rust_name: str) ->
error_model = "infallible_placeholder"
if kind in {"field", "constant", "enum_value"}:
value_type = mapper.type(item["type"], item.get("nullability"), generics)
if item["doc_id"] in BOXED_FIELD_IDS:
value_type = (
f"Option<Box<{value_type[7:-1]}>>"
if value_type.startswith("Option<")
else f"Box<{value_type}>"
)
if kind == "enum_value":
signature = f"{rust_name}: {value_type}"
elif kind == "constant":
@@ -757,7 +778,20 @@ def validate(catalog: dict, type_rows: list[dict[str, str]], member_rows: list[d
raise ValueError("invalid type mapping status")
if {row["status"] for row in member_rows} - VALID_MEMBER_STATUSES:
raise ValueError("invalid member mapping status")
catalog_ids = {member["doc_id"] for assembly in catalog["assemblies"] for item in assembly["types"] for member in item["members"]}
catalog_members = {
member["doc_id"]: member
for assembly in catalog["assemblies"]
for item in assembly["types"]
for member in item["members"]
}
catalog_ids = catalog_members.keys()
missing_boxed_fields = BOXED_FIELD_IDS - catalog_ids
if missing_boxed_fields:
raise ValueError(f"unknown configured boxed fields: {sorted(missing_boxed_fields)}")
if invalid_boxed_fields := [
member_id for member_id in BOXED_FIELD_IDS if catalog_members[member_id]["kind"] != "field"
]:
raise ValueError(f"configured boxed members are not fields: {sorted(invalid_boxed_fields)}")
mapped_ids = {row["csharp_id"] for row in member_rows}
if catalog_ids != mapped_ids:
raise ValueError(f"catalog/mapping ID mismatch: missing={len(catalog_ids - mapped_ids)}, stale={len(mapped_ids - catalog_ids)}")