Skip to content

Commit d358b32

Browse files
committed
wip rework type spec in extension_api_parser
1 parent 13c5472 commit d358b32

File tree

8 files changed

+1554
-0
lines changed

8 files changed

+1554
-0
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""
2+
`extension_api.json` is pretty big, hence it's much easier to have it
3+
format reproduced here as typed classes, especially given we want to cook
4+
it a bit before using it in the templates
5+
"""
6+
7+
from .builtins import *
8+
from .classes import *
9+
from .api import *
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""
2+
Helper to debug the parser
3+
"""
4+
5+
import os
6+
import sys
7+
from pathlib import Path
8+
9+
from . import parse_extension_api_json, BuildConfig
10+
from .type_spec import TYPES_DB
11+
12+
13+
try:
14+
extension_api_path = Path(sys.argv[1])
15+
except IndexError:
16+
extension_api_path = (
17+
(Path(__file__).parent / "../../godot_headers/extension_api.json")
18+
.resolve()
19+
.relative_to(os.getcwd())
20+
)
21+
22+
23+
try:
24+
build_configs = [BuildConfig(sys.argv[2])]
25+
except IndexError:
26+
build_configs = BuildConfig
27+
28+
29+
for build_config in build_configs:
30+
initial_types_db = TYPES_DB.copy()
31+
print(f"Checking {extension_api_path} with config {build_config.value}")
32+
parse_extension_api_json(extension_api_path, build_config, skip_classes=False)
33+
TYPES_DB.clear()
34+
TYPES_DB.update(initial_types_db)
Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
from typing import List, Dict, Tuple, Optional
2+
from collections import OrderedDict
3+
from dataclasses import dataclass, replace
4+
from pathlib import Path
5+
import json
6+
from enum import Enum
7+
8+
from .type_spec import *
9+
from .builtins import *
10+
from .classes import *
11+
from .utils import *
12+
13+
14+
class BuildConfig(Enum):
15+
FLOAT_32 = "float_32"
16+
DOUBLE_32 = "double_32"
17+
FLOAT_64 = "float_64"
18+
DOUBLE_64 = "double_64"
19+
20+
21+
@dataclass
22+
class GlobalConstantSpec:
23+
@classmethod
24+
def parse(cls, item: dict) -> "GlobalConstantSpec":
25+
# Don't known what it is supposed to contain given the list is so far empty in the JSON
26+
raise NotImplementedError
27+
28+
29+
def parse_global_enum(spec: dict) -> EnumTypeSpec:
30+
assert spec.keys() == {"name", "values"}, spec.keys()
31+
return EnumTypeSpec(
32+
original_name=spec["name"],
33+
py_type=spec["name"],
34+
cy_type=spec["name"],
35+
is_bitfield=False,
36+
values={x["name"]: x["value"] for x in spec["values"]},
37+
)
38+
39+
40+
@dataclass
41+
class UtilityFunctionArgumentSpec:
42+
name: str
43+
original_name: str
44+
type: TypeInUse
45+
default_value: Optional[ValueInUse]
46+
47+
@classmethod
48+
def parse(cls, item: dict) -> "UtilityFunctionArgumentSpec":
49+
item.setdefault("original_name", item["name"])
50+
item.setdefault("default_value", None)
51+
assert_api_consistency(cls, item)
52+
arg_type = TypeInUse(item["type"])
53+
return cls(
54+
name=correct_name(item["name"]),
55+
original_name=item["original_name"],
56+
type=arg_type,
57+
default_value=ValueInUse.parse(arg_type, item["default_value"])
58+
if item["default_value"]
59+
else None,
60+
)
61+
62+
63+
@dataclass
64+
class UtilityFunctionSpec:
65+
original_name: str
66+
name: str
67+
return_type: TypeInUse
68+
category: str
69+
is_vararg: bool
70+
hash: int
71+
arguments: List[Tuple[str, TypeInUse]]
72+
73+
@classmethod
74+
def parse(cls, item: dict) -> "UtilityFunctionSpec":
75+
item.setdefault("original_name", item["name"])
76+
item.setdefault("arguments", [])
77+
item.setdefault("return_type", "Nil")
78+
assert_api_consistency(cls, item)
79+
return cls(
80+
name=correct_name(item["name"]),
81+
original_name=item["original_name"],
82+
return_type=TypeInUse(item["return_type"]),
83+
category=item["category"],
84+
is_vararg=item["is_vararg"],
85+
hash=item["hash"],
86+
arguments=[UtilityFunctionArgumentSpec.parse(x) for x in item["arguments"]],
87+
)
88+
89+
90+
@dataclass
91+
class SingletonSpec:
92+
original_name: str
93+
name: str
94+
type: TypeInUse
95+
96+
@classmethod
97+
def parse(cls, item: dict) -> "SingletonSpec":
98+
item.setdefault("original_name", item["name"])
99+
assert_api_consistency(cls, item)
100+
return cls(
101+
name=correct_name(item["name"]),
102+
original_name=item["original_name"],
103+
type=TypeInUse(item["type"]),
104+
)
105+
106+
107+
@dataclass
108+
class NativeStructureSpec:
109+
original_name: str
110+
name: str
111+
# Format is basically a dump of the C struct content, so don't try to be clever by parsing it
112+
format: str
113+
114+
@classmethod
115+
def parse(cls, item: dict) -> "NativeStructureSpec":
116+
item.setdefault("original_name", item["name"])
117+
assert_api_consistency(cls, item)
118+
return cls(
119+
name=correct_name(item["name"]),
120+
original_name=item["original_name"],
121+
format=item["format"],
122+
)
123+
124+
125+
@dataclass
126+
class ExtensionApi:
127+
version_major: int # e.g. 4
128+
version_minor: int # e.g. 0
129+
version_patch: int # e.g. 0
130+
version_status: str # e.g. "alpha13"
131+
version_build: str # e.g. "official"
132+
version_full_name: str # e.g. "Godot Engine v4.0.alpha13.official"
133+
134+
classes: List[ClassTypeSpec]
135+
builtins: List[BuiltinTypeSpec]
136+
global_constants: List[GlobalConstantSpec]
137+
global_enums: List[EnumTypeSpec]
138+
utility_functions: List[UtilityFunctionSpec]
139+
singletons: List[SingletonSpec]
140+
native_structures: List[NativeStructureSpec]
141+
142+
# Expose scalars, nil and variant
143+
144+
@property
145+
def variant_type(self):
146+
return TYPES_DB["Variant"]
147+
148+
@property
149+
def nil_type(self):
150+
return TYPES_DB["Nil"]
151+
152+
@property
153+
def bool_type(self):
154+
return TYPES_DB["bool"]
155+
156+
@property
157+
def int_type(self):
158+
return TYPES_DB["int"]
159+
160+
@property
161+
def float_type(self):
162+
return TYPES_DB["float"]
163+
164+
def get_class_meth_hash(self, classname: str, methname: str) -> int:
165+
klass = next(c for c in self.classes if c.original_name == classname)
166+
meth = next(m for m in klass.methods if m.original_name == methname)
167+
return meth.hash
168+
169+
170+
def merge_builtins_size_info(api_json: dict, build_config: BuildConfig) -> None:
171+
# Builtins size depend of the build config, hence it is stored separatly from
172+
# the rest of the built class definition.
173+
# Here we retreive the correct config and merge it back in the builtins classes
174+
# definition to simplify the rest of the parsing.
175+
176+
builtin_class_sizes = next(
177+
x["sizes"]
178+
for x in api_json["builtin_class_sizes"]
179+
if x["build_configuration"] == build_config.value
180+
)
181+
builtin_class_sizes = {x["name"]: x["size"] for x in builtin_class_sizes}
182+
builtin_class_member_offsets = next(
183+
x["classes"]
184+
for x in api_json["builtin_class_member_offsets"]
185+
if x["build_configuration"] == build_config.value
186+
)
187+
builtin_class_member_offsets = {x["name"]: x["members"] for x in builtin_class_member_offsets}
188+
189+
# TODO: remove me once https://github.com/godotengine/godot/pull/64690 is merged
190+
if "Projection" not in builtin_class_member_offsets:
191+
builtin_class_member_offsets["Projection"] = [
192+
{"member": "x", "offset": 0},
193+
{"member": "y", "offset": builtin_class_sizes["Vector4"]},
194+
{"member": "z", "offset": 2 * builtin_class_sizes["Vector4"]},
195+
{"member": "w", "offset": 3 * builtin_class_sizes["Vector4"]},
196+
]
197+
198+
for item in api_json["builtin_classes"]:
199+
name = item["name"]
200+
item["size"] = builtin_class_sizes[name]
201+
# TODO: correct me once https://github.com/godotengine/godot/pull/64365 is merged
202+
for member in builtin_class_member_offsets.get(name, ()):
203+
for item_member in item["members"]:
204+
if item_member["name"] == member["member"]:
205+
item_member["offset"] = member["offset"]
206+
# Float builtin in extension_api.json is always 64bits long,
207+
# however builtins made of floating point number can be made of
208+
# 32bits (C float) or 64bits (C double)
209+
# But Color is a special case: it is always made of 32bits floats !
210+
if name == "Color":
211+
item_member["type"] = "meta:float"
212+
elif item_member["type"] == "float":
213+
if build_config in (BuildConfig.FLOAT_32, BuildConfig.FLOAT_64):
214+
item_member["type"] = "meta:float"
215+
else:
216+
assert build_config in (BuildConfig.DOUBLE_32, BuildConfig.DOUBLE_64)
217+
item_member["type"] = "meta:double"
218+
elif item_member["type"] == "int":
219+
# Builtins containing int is always made of int32
220+
item_member["type"] = "meta:int32"
221+
break
222+
else:
223+
raise RuntimeError(f"Member `{member}` doesn't seem to be part of `{name}` !")
224+
225+
# Variant&Object are not present among the `builtin_classes`, only their size is provided.
226+
# So we have to create our own custom entry for them.
227+
api_json["variant_size"] = builtin_class_sizes["Variant"]
228+
api_json["object_size"] = builtin_class_sizes["Object"]
229+
230+
231+
def order_classes(classes: List[ClassTypeSpec]) -> List[ClassTypeSpec]:
232+
# Order classes by inheritance dependency needs
233+
ordered_classes = OrderedDict() # Makes it explicit we need ordering here !
234+
ordered_count = 0
235+
236+
while len(classes) != len(ordered_classes):
237+
for klass in classes:
238+
if klass.inherits is None or klass.inherits.type_name in ordered_classes:
239+
ordered_classes[klass.original_name] = klass
240+
241+
# Sanity check to avoid infinite loop in case of error in `extension_api.json`
242+
if ordered_count == len(ordered_classes):
243+
bad_class = next(
244+
klass
245+
for klass in classes
246+
if klass.inherits is not None and klass.inherits.type_name not in ordered_classes
247+
)
248+
raise RuntimeError(
249+
f"Class `{bad_class.original_name}` inherits of unknown class `{bad_class.inherits.type_name}`"
250+
)
251+
ordered_count = len(ordered_classes)
252+
253+
return list(ordered_classes.values())
254+
255+
256+
def parse_extension_api_json(
257+
path: Path, build_config: BuildConfig, skip_classes: bool = False
258+
) -> ExtensionApi:
259+
api_json = json.loads(path.read_text(encoding="utf8"))
260+
assert isinstance(api_json, dict)
261+
262+
merge_builtins_size_info(api_json, build_config)
263+
264+
# Not much info about variant
265+
variant_type = VariantTypeSpec(size=api_json["variant_size"])
266+
TYPES_DB_REGISTER_TYPE("Variant", variant_type)
267+
268+
# Unlike int type that is always 8 bytes long, float depends on config
269+
if build_config in (BuildConfig.DOUBLE_32, BuildConfig.DOUBLE_64):
270+
real_type = replace(TYPES_DB[f"meta:float"], original_name="float")
271+
else:
272+
real_type = replace(TYPES_DB[f"meta:double"], original_name="float")
273+
TYPES_DB_REGISTER_TYPE("float", real_type)
274+
275+
def _register_enums(enums, parent_id=None):
276+
for enum_type in enums:
277+
classifier = "bitfield" if enum_type.is_bitfield else "enum"
278+
if parent_id:
279+
type_id = f"{classifier}::{parent_id}.{enum_type.original_name}"
280+
else:
281+
type_id = f"{classifier}::{enum_type.original_name}"
282+
TYPES_DB_REGISTER_TYPE(type_id, enum_type)
283+
284+
builtins = parse_builtins_ignore_scalars_and_nil(api_json["builtin_classes"])
285+
for builtin_type in builtins:
286+
TYPES_DB_REGISTER_TYPE(builtin_type.original_name, builtin_type)
287+
_register_enums(builtin_type.enums, parent_id=builtin_type.original_name)
288+
289+
# Parsing classes takes ~75% of the time while not being needed to render builtins stuff
290+
if skip_classes:
291+
classes = []
292+
293+
else:
294+
classes = order_classes(
295+
[parse_class(x, object_size=api_json["object_size"]) for x in api_json["classes"]]
296+
)
297+
for class_type in classes:
298+
TYPES_DB_REGISTER_TYPE(class_type.original_name, class_type)
299+
_register_enums(class_type.enums, parent_id=class_type.original_name)
300+
301+
global_enums = [parse_global_enum(x) for x in api_json["global_enums"]]
302+
_register_enums(global_enums)
303+
304+
ensure_types_db_consistency()
305+
306+
api = ExtensionApi(
307+
version_major=api_json["header"]["version_major"],
308+
version_minor=api_json["header"]["version_minor"],
309+
version_patch=api_json["header"]["version_patch"],
310+
version_status=api_json["header"]["version_status"],
311+
version_build=api_json["header"]["version_build"],
312+
version_full_name=api_json["header"]["version_full_name"],
313+
classes=classes,
314+
builtins=builtins,
315+
global_constants=[GlobalConstantSpec.parse(x) for x in api_json["global_constants"]],
316+
global_enums=global_enums,
317+
utility_functions=[UtilityFunctionSpec.parse(x) for x in api_json["utility_functions"]],
318+
singletons=[SingletonSpec.parse(x) for x in api_json["singletons"]],
319+
native_structures=[NativeStructureSpec.parse(x) for x in api_json["native_structures"]],
320+
)
321+
322+
return api

0 commit comments

Comments
 (0)