make_interface_header.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import difflib
  2. import json
  3. from collections import OrderedDict
  4. BASE_TYPES = [
  5. "void",
  6. "int",
  7. "int8_t",
  8. "uint8_t",
  9. "int16_t",
  10. "uint16_t",
  11. "int32_t",
  12. "uint32_t",
  13. "int64_t",
  14. "uint64_t",
  15. "size_t",
  16. "char",
  17. "char16_t",
  18. "char32_t",
  19. "wchar_t",
  20. "float",
  21. "double",
  22. ]
  23. def _get_buffer(path):
  24. with open(path, "rb") as file:
  25. return file.read()
  26. def _generated_wrapper(path, header_lines):
  27. f = open(path, "wt", encoding="utf-8")
  28. for line in header_lines:
  29. f.write(line + "\n")
  30. f.write("#pragma once\n\n")
  31. return f
  32. def scons_run(target, source, env):
  33. target_path = str(target[0])
  34. source_path = str(source[0])
  35. generate_gdextension_interface_header(target_path, source_path)
  36. def generate_gdextension_interface_header(target, source, header_lines=[]):
  37. buffer = _get_buffer(source)
  38. data = json.loads(buffer, object_pairs_hook=OrderedDict)
  39. check_formatting(buffer.decode("utf-8"), data, source)
  40. check_allowed_keys(data, ["_copyright", "$schema", "format_version", "types", "interface"])
  41. valid_data_types = {}
  42. for type in BASE_TYPES:
  43. valid_data_types[type] = True
  44. with _generated_wrapper(target, header_lines) as file:
  45. file.write("""\
  46. #ifndef __cplusplus
  47. #include <stddef.h>
  48. #include <stdint.h>
  49. typedef uint32_t char32_t;
  50. typedef uint16_t char16_t;
  51. #else
  52. #include <cstddef>
  53. #include <cstdint>
  54. extern "C" {
  55. #endif
  56. """)
  57. handles = []
  58. type_replacements = []
  59. for type in data["types"]:
  60. kind = type["kind"]
  61. check_type(kind, type, valid_data_types)
  62. valid_data_types[type["name"]] = type
  63. if "deprecated" in type:
  64. check_allowed_keys(type["deprecated"], ["since"], ["message", "replace_with"])
  65. if "replace_with" in type["deprecated"]:
  66. type_replacements.append((type["name"], type["deprecated"]["replace_with"]))
  67. if "description" in type:
  68. write_doc(file, type["description"])
  69. if kind == "handle":
  70. check_allowed_keys(
  71. type, ["name", "kind"], ["is_const", "is_uninitialized", "parent", "description", "deprecated"]
  72. )
  73. if "parent" in type and type["parent"] not in handles:
  74. raise UnknownTypeError(type["parent"], type["name"])
  75. # @todo In the future, let's write these as `struct *` so the compiler can help us with type checking.
  76. type["type"] = "void*" if not type.get("is_const", False) else "const void*"
  77. write_simple_type(file, type)
  78. handles.append(type["name"])
  79. elif kind == "alias":
  80. check_allowed_keys(type, ["name", "kind", "type"], ["description", "deprecated"])
  81. write_simple_type(file, type)
  82. elif kind == "enum":
  83. check_allowed_keys(type, ["name", "kind", "values"], ["is_bitfield", "description", "deprecated"])
  84. write_enum_type(file, type)
  85. elif kind == "function":
  86. check_allowed_keys(type, ["name", "kind", "return_value", "arguments"], ["description", "deprecated"])
  87. write_function_type(file, type)
  88. elif kind == "struct":
  89. check_allowed_keys(type, ["name", "kind", "members"], ["description", "deprecated"])
  90. write_struct_type(file, type)
  91. else:
  92. raise Exception(f"Unknown kind of type: {kind}")
  93. for type_name, replace_with in type_replacements:
  94. if replace_with not in valid_data_types:
  95. raise Exception(f"Unknown type '{replace_with}' used as replacement for '{type_name}'")
  96. replacement = valid_data_types[replace_with]
  97. if isinstance(replacement, dict) and "deprecated" in replacement:
  98. raise Exception(
  99. f"Cannot use '{replace_with}' as replacement for '{type_name}' because it's deprecated too"
  100. )
  101. interface_replacements = []
  102. valid_interfaces = {}
  103. for interface in data["interface"]:
  104. check_type("function", interface, valid_data_types)
  105. check_allowed_keys(
  106. interface,
  107. ["name", "return_value", "arguments", "since", "description"],
  108. ["see", "legacy_type_name", "deprecated"],
  109. )
  110. valid_interfaces[interface["name"]] = interface
  111. if "deprecated" in interface:
  112. check_allowed_keys(interface["deprecated"], ["since"], ["message", "replace_with"])
  113. if "replace_with" in interface["deprecated"]:
  114. interface_replacements.append((interface["name"], interface["deprecated"]["replace_with"]))
  115. write_interface(file, interface)
  116. for function_name, replace_with in interface_replacements:
  117. if replace_with not in valid_interfaces:
  118. raise Exception(
  119. f"Unknown interface function '{replace_with}' used as replacement for '{function_name}'"
  120. )
  121. replacement = valid_interfaces[replace_with]
  122. if "deprecated" in replacement:
  123. raise Exception(
  124. f"Cannot use '{replace_with}' as replacement for '{function_name}' because it's deprecated too"
  125. )
  126. file.write("""\
  127. #ifdef __cplusplus
  128. }
  129. #endif
  130. """)
  131. # Serialize back into JSON in order to see if the formatting remains the same.
  132. def check_formatting(buffer, data, filename):
  133. buffer2 = json.dumps(data, indent=4)
  134. lines1 = buffer.splitlines()
  135. lines2 = buffer2.splitlines()
  136. diff = difflib.unified_diff(
  137. lines1,
  138. lines2,
  139. fromfile="a/" + filename,
  140. tofile="b/" + filename,
  141. lineterm="",
  142. )
  143. diff = list(diff)
  144. if len(diff) > 0:
  145. print(" *** Apply this patch to fix: ***\n")
  146. print("\n".join(diff))
  147. raise Exception(f"Formatting issues in {filename}")
  148. def check_allowed_keys(data, required, optional=[]):
  149. keys = data.keys()
  150. allowed = required + optional
  151. for k in keys:
  152. if k not in allowed:
  153. raise Exception(f"Found unknown key '{k}'")
  154. for r in required:
  155. if r not in keys:
  156. raise Exception(f"Missing required key '{r}'")
  157. class UnknownTypeError(Exception):
  158. def __init__(self, unknown, parent, item=None):
  159. self.unknown = unknown
  160. self.parent = parent
  161. if item:
  162. msg = f"Unknown type '{unknown}' for '{item}' used in '{parent}'"
  163. else:
  164. msg = f"Unknown type '{unknown}' used in '{parent}'"
  165. super().__init__(msg)
  166. def base_type_name(type_name):
  167. if type_name.startswith("const "):
  168. type_name = type_name[6:]
  169. if type_name.endswith("*"):
  170. type_name = type_name[:-1]
  171. return type_name
  172. def format_type_and_name(type, name=None):
  173. ret = type
  174. if ret[-1] == "*":
  175. ret = ret[:-1] + " *"
  176. if name:
  177. if ret[-1] == "*":
  178. ret = ret + name
  179. else:
  180. ret = ret + " " + name
  181. return ret
  182. def check_type(kind, type, valid_data_types):
  183. if kind == "alias":
  184. if base_type_name(type["type"]) not in valid_data_types:
  185. raise UnknownTypeError(type["type"], type["name"])
  186. elif kind == "struct":
  187. for member in type["members"]:
  188. if base_type_name(member["type"]) not in valid_data_types:
  189. raise UnknownTypeError(member["type"], type["name"], member["name"])
  190. elif kind == "function":
  191. for arg in type["arguments"]:
  192. if base_type_name(arg["type"]) not in valid_data_types:
  193. raise UnknownTypeError(arg["type"], type["name"], arg.get("name"))
  194. if "return_value" in type:
  195. if base_type_name(type["return_value"]["type"]) not in valid_data_types:
  196. raise UnknownTypeError(type["return_value"]["type"], type["name"])
  197. def write_doc(file, doc, indent=""):
  198. if len(doc) == 1:
  199. file.write(f"{indent}/* {doc[0]} */\n")
  200. return
  201. first = True
  202. for line in doc:
  203. if first:
  204. file.write(indent + "/*")
  205. first = False
  206. else:
  207. file.write(indent + " *")
  208. if line != "":
  209. file.write(" " + line)
  210. file.write("\n")
  211. file.write(indent + " */\n")
  212. def make_deprecated_message(data):
  213. parts = [
  214. f"Deprecated in Godot {data['since']}.",
  215. data["message"] if "message" in data else "",
  216. f"Use `{data['replace_with']}` instead." if "replace_with" in data else "",
  217. ]
  218. return " ".join([x for x in parts if x.strip() != ""])
  219. def make_deprecated_comment_for_type(type):
  220. if "deprecated" not in type:
  221. return ""
  222. message = make_deprecated_message(type["deprecated"])
  223. return f" /* {message} */"
  224. def write_simple_type(file, type):
  225. file.write(f"typedef {format_type_and_name(type['type'], type['name'])};{make_deprecated_comment_for_type(type)}\n")
  226. def write_enum_type(file, enum):
  227. file.write("typedef enum {\n")
  228. for value in enum["values"]:
  229. check_allowed_keys(value, ["name", "value"], ["description", "deprecated"])
  230. if "description" in value:
  231. write_doc(file, value["description"], "\t")
  232. file.write(f"\t{value['name']} = {value['value']},\n")
  233. file.write(f"}} {enum['name']};{make_deprecated_comment_for_type(enum)}\n\n")
  234. def make_args_text(args):
  235. combined = []
  236. for arg in args:
  237. check_allowed_keys(arg, ["type"], ["name", "description"])
  238. combined.append(format_type_and_name(arg["type"], arg.get("name")))
  239. return ", ".join(combined)
  240. def write_function_type(file, fn):
  241. args_text = make_args_text(fn["arguments"]) if ("arguments" in fn) else ""
  242. name_and_args = f"(*{fn['name']})({args_text})"
  243. file.write(
  244. f"typedef {format_type_and_name(fn['return_value']['type'], name_and_args)};{make_deprecated_comment_for_type(fn)}\n"
  245. )
  246. def write_struct_type(file, struct):
  247. file.write("typedef struct {\n")
  248. for member in struct["members"]:
  249. check_allowed_keys(member, ["name", "type"], ["description"])
  250. if "description" in member:
  251. write_doc(file, member["description"], "\t")
  252. file.write(f"\t{format_type_and_name(member['type'], member['name'])};\n")
  253. file.write(f"}} {struct['name']};{make_deprecated_comment_for_type(struct)}\n\n")
  254. def write_interface(file, interface):
  255. doc = [
  256. f"@name {interface['name']}",
  257. f"@since {interface['since']}",
  258. ]
  259. if "deprecated" in interface:
  260. doc.append(f"@deprecated {make_deprecated_message(interface['deprecated'])}")
  261. doc += [
  262. "",
  263. interface["description"][0],
  264. ]
  265. if len(interface["description"]) > 1:
  266. doc.append("")
  267. doc += interface["description"][1:]
  268. if "arguments" in interface:
  269. doc.append("")
  270. for arg in interface["arguments"]:
  271. if "description" not in arg:
  272. raise Exception(f"Interface function {interface['name']} is missing docs for {arg['name']} argument")
  273. arg_doc = " ".join(arg["description"])
  274. doc.append(f"@param {arg['name']} {arg_doc}")
  275. if "return_value" in interface and interface["return_value"]["type"] != "void":
  276. if "description" not in interface["return_value"]:
  277. raise Exception(f"Interface function {interface['name']} is missing docs for return value")
  278. ret_doc = " ".join(interface["return_value"]["description"])
  279. doc.append("")
  280. doc.append(f"@return {ret_doc}")
  281. if "see" in interface:
  282. doc.append("")
  283. for see in interface["see"]:
  284. doc.append(f"@see {see}")
  285. file.write("/**\n")
  286. for d in doc:
  287. if d != "":
  288. file.write(f" * {d}\n")
  289. else:
  290. file.write(" *\n")
  291. file.write(" */\n")
  292. fn = interface.copy()
  293. if "deprecated" in fn:
  294. del fn["deprecated"]
  295. fn["name"] = "GDExtensionInterface" + "".join(word.capitalize() for word in interface["name"].split("_"))
  296. write_function_type(file, fn)
  297. file.write("\n")