make_interface_header.py 12 KB

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