gendynapi.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. #!/usr/bin/env python3
  2. # Simple DirectMedia Layer
  3. # Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
  4. #
  5. # This software is provided 'as-is', without any express or implied
  6. # warranty. In no event will the authors be held liable for any damages
  7. # arising from the use of this software.
  8. #
  9. # Permission is granted to anyone to use this software for any purpose,
  10. # including commercial applications, and to alter it and redistribute it
  11. # freely, subject to the following restrictions:
  12. #
  13. # 1. The origin of this software must not be misrepresented; you must not
  14. # claim that you wrote the original software. If you use this software
  15. # in a product, an acknowledgment in the product documentation would be
  16. # appreciated but is not required.
  17. # 2. Altered source versions must be plainly marked as such, and must not be
  18. # misrepresented as being the original software.
  19. # 3. This notice may not be removed or altered from any source distribution.
  20. # WHAT IS THIS?
  21. # When you add a public API to SDL, please run this script, make sure the
  22. # output looks sane (git diff, it adds to existing files), and commit it.
  23. # It keeps the dynamic API jump table operating correctly.
  24. #
  25. # Platform-specific API:
  26. # After running the script, you have to manually add #ifdef SDL_PLATFORM_WIN32
  27. # or similar around the function in 'SDL_dynapi_procs.h'.
  28. #
  29. import argparse
  30. import dataclasses
  31. import json
  32. import logging
  33. import os
  34. from pathlib import Path
  35. import pprint
  36. import re
  37. SDL_ROOT = Path(__file__).resolve().parents[2]
  38. SDL_INCLUDE_DIR = SDL_ROOT / "include/SDL3"
  39. SDL_DYNAPI_PROCS_H = SDL_ROOT / "src/dynapi/SDL_dynapi_procs.h"
  40. SDL_DYNAPI_OVERRIDES_H = SDL_ROOT / "src/dynapi/SDL_dynapi_overrides.h"
  41. SDL_DYNAPI_SYM = SDL_ROOT / "src/dynapi/SDL_dynapi.sym"
  42. RE_EXTERN_C = re.compile(r'.*extern[ "]*C[ "].*')
  43. RE_COMMENT_REMOVE_CONTENT = re.compile(r'\/\*.*\*/')
  44. RE_PARSING_FUNCTION = re.compile(r'(.*SDLCALL[^\(\)]*) ([a-zA-Z0-9_]+) *\((.*)\) *;.*')
  45. #eg:
  46. # void (SDLCALL *callback)(void*, int)
  47. # \1(\2)\3
  48. RE_PARSING_CALLBACK = re.compile(r'([^\(\)]*)\(([^\(\)]+)\)(.*)')
  49. logger = logging.getLogger(__name__)
  50. @dataclasses.dataclass(frozen=True)
  51. class SdlProcedure:
  52. retval: str
  53. name: str
  54. parameter: list[str]
  55. parameter_name: list[str]
  56. header: str
  57. comment: str
  58. @property
  59. def variadic(self) -> bool:
  60. return "..." in self.parameter
  61. def parse_header(header_path: Path) -> list[SdlProcedure]:
  62. logger.debug("Parse header: %s", header_path)
  63. header_procedures = []
  64. parsing_function = False
  65. current_func = ""
  66. parsing_comment = False
  67. current_comment = ""
  68. ignore_wiki_documentation = False
  69. with header_path.open() as f:
  70. for line in f:
  71. # Skip lines if we're in a wiki documentation block.
  72. if ignore_wiki_documentation:
  73. if line.startswith("#endif"):
  74. ignore_wiki_documentation = False
  75. continue
  76. # Discard wiki documentations blocks.
  77. if line.startswith("#ifdef SDL_WIKI_DOCUMENTATION_SECTION"):
  78. ignore_wiki_documentation = True
  79. continue
  80. # Discard pre-processor directives ^#.*
  81. if line.startswith("#"):
  82. continue
  83. # Discard "extern C" line
  84. match = RE_EXTERN_C.match(line)
  85. if match:
  86. continue
  87. # Remove one line comment // ...
  88. # eg: extern SDL_DECLSPEC SDL_hid_device * SDLCALL SDL_hid_open_path(const char *path, int bExclusive /* = false */)
  89. line = RE_COMMENT_REMOVE_CONTENT.sub('', line)
  90. # Get the comment block /* ... */ across several lines
  91. match_start = "/*" in line
  92. match_end = "*/" in line
  93. if match_start and match_end:
  94. continue
  95. if match_start:
  96. parsing_comment = True
  97. current_comment = line
  98. continue
  99. if match_end:
  100. parsing_comment = False
  101. current_comment += line
  102. continue
  103. if parsing_comment:
  104. current_comment += line
  105. continue
  106. # Get the function prototype across several lines
  107. if parsing_function:
  108. # Append to the current function
  109. current_func += " "
  110. current_func += line.strip()
  111. else:
  112. # if is contains "extern", start grabbing
  113. if "extern" not in line:
  114. continue
  115. # Start grabbing the new function
  116. current_func = line.strip()
  117. parsing_function = True
  118. # If it contains ';', then the function is complete
  119. if ";" not in current_func:
  120. continue
  121. # Got function/comment, reset vars
  122. parsing_function = False
  123. func = current_func
  124. comment = current_comment
  125. current_func = ""
  126. current_comment = ""
  127. # Discard if it doesn't contain 'SDLCALL'
  128. if "SDLCALL" not in func:
  129. logger.debug(" Discard, doesn't have SDLCALL: %r", func)
  130. continue
  131. # Discard if it contains 'SDLMAIN_DECLSPEC' (these are not SDL symbols).
  132. if "SDLMAIN_DECLSPEC" in func:
  133. logger.debug(" Discard, has SDLMAIN_DECLSPEC: %r", func)
  134. continue
  135. logger.debug("Raw data: %r", func)
  136. # Replace unusual stuff...
  137. func = func.replace(" SDL_PRINTF_VARARG_FUNC(1)", "")
  138. func = func.replace(" SDL_PRINTF_VARARG_FUNC(2)", "")
  139. func = func.replace(" SDL_PRINTF_VARARG_FUNC(3)", "")
  140. func = func.replace(" SDL_PRINTF_VARARG_FUNC(4)", "")
  141. func = func.replace(" SDL_PRINTF_VARARG_FUNCV(1)", "")
  142. func = func.replace(" SDL_PRINTF_VARARG_FUNCV(2)", "")
  143. func = func.replace(" SDL_PRINTF_VARARG_FUNCV(3)", "")
  144. func = func.replace(" SDL_PRINTF_VARARG_FUNCV(4)", "")
  145. func = func.replace(" SDL_WPRINTF_VARARG_FUNC(3)", "")
  146. func = func.replace(" SDL_WPRINTF_VARARG_FUNCV(3)", "")
  147. func = func.replace(" SDL_SCANF_VARARG_FUNC(2)", "")
  148. func = func.replace(" SDL_SCANF_VARARG_FUNCV(2)", "")
  149. func = func.replace(" SDL_ANALYZER_NORETURN", "")
  150. func = func.replace(" SDL_MALLOC", "")
  151. func = func.replace(" SDL_ALLOC_SIZE2(1, 2)", "")
  152. func = func.replace(" SDL_ALLOC_SIZE(2)", "")
  153. func = re.sub(r" SDL_ACQUIRE\(.*\)", "", func)
  154. func = re.sub(r" SDL_ACQUIRE_SHARED\(.*\)", "", func)
  155. func = re.sub(r" SDL_TRY_ACQUIRE\(.*\)", "", func)
  156. func = re.sub(r" SDL_TRY_ACQUIRE_SHARED\(.*\)", "", func)
  157. func = re.sub(r" SDL_RELEASE\(.*\)", "", func)
  158. func = re.sub(r" SDL_RELEASE_SHARED\(.*\)", "", func)
  159. func = re.sub(r" SDL_RELEASE_GENERIC\(.*\)", "", func)
  160. func = re.sub(r"([ (),])(SDL_IN_BYTECAP\([^)]*\))", r"\1", func)
  161. func = re.sub(r"([ (),])(SDL_OUT_BYTECAP\([^)]*\))", r"\1", func)
  162. func = re.sub(r"([ (),])(SDL_INOUT_Z_CAP\([^)]*\))", r"\1", func)
  163. func = re.sub(r"([ (),])(SDL_OUT_Z_CAP\([^)]*\))", r"\1", func)
  164. # Should be a valid function here
  165. match = RE_PARSING_FUNCTION.match(func)
  166. if not match:
  167. logger.error("Cannot parse: %s", func)
  168. raise ValueError(func)
  169. func_ret = match.group(1)
  170. func_name = match.group(2)
  171. func_params = match.group(3)
  172. #
  173. # Parse return value
  174. #
  175. func_ret = func_ret.replace('extern', ' ')
  176. func_ret = func_ret.replace('SDLCALL', ' ')
  177. func_ret = func_ret.replace('SDL_DECLSPEC', ' ')
  178. func_ret, _ = re.subn('([ ]{2,})', ' ', func_ret)
  179. # Remove trailing spaces in front of '*'
  180. func_ret = func_ret.replace(' *', '*')
  181. func_ret = func_ret.strip()
  182. #
  183. # Parse parameters
  184. #
  185. func_params = func_params.strip()
  186. if func_params == "":
  187. func_params = "void"
  188. # Identify each function parameters with type and name
  189. # (eventually there are callbacks of several parameters)
  190. tmp = func_params.split(',')
  191. tmp2 = []
  192. param = ""
  193. for t in tmp:
  194. if param == "":
  195. param = t
  196. else:
  197. param = param + "," + t
  198. # Identify a callback or parameter when there is same count of '(' and ')'
  199. if param.count('(') == param.count(')'):
  200. tmp2.append(param.strip())
  201. param = ""
  202. # Process each parameters, separation name and type
  203. func_param_type = []
  204. func_param_name = []
  205. for t in tmp2:
  206. if t == "void":
  207. func_param_type.append(t)
  208. func_param_name.append("")
  209. continue
  210. if t == "...":
  211. func_param_type.append(t)
  212. func_param_name.append("")
  213. continue
  214. param_name = ""
  215. # parameter is a callback
  216. if '(' in t:
  217. match = RE_PARSING_CALLBACK.match(t)
  218. if not match:
  219. logger.error("cannot parse callback: %s", t)
  220. raise ValueError(t)
  221. a = match.group(1).strip()
  222. b = match.group(2).strip()
  223. c = match.group(3).strip()
  224. try:
  225. (param_type, param_name) = b.rsplit('*', 1)
  226. except:
  227. param_type = t
  228. param_name = "param_name_not_specified"
  229. # bug rsplit ??
  230. if param_name == "":
  231. param_name = "param_name_not_specified"
  232. # reconstruct a callback name for future parsing
  233. func_param_type.append(a + " (" + param_type.strip() + " *REWRITE_NAME)" + c)
  234. func_param_name.append(param_name.strip())
  235. continue
  236. # array like "char *buf[]"
  237. has_array = False
  238. if t.endswith("[]"):
  239. t = t.replace("[]", "")
  240. has_array = True
  241. # pointer
  242. if '*' in t:
  243. try:
  244. (param_type, param_name) = t.rsplit('*', 1)
  245. except:
  246. param_type = t
  247. param_name = "param_name_not_specified"
  248. # bug rsplit ??
  249. if param_name == "":
  250. param_name = "param_name_not_specified"
  251. val = param_type.strip() + "*REWRITE_NAME"
  252. # Remove trailing spaces in front of '*'
  253. tmp = ""
  254. while val != tmp:
  255. tmp = val
  256. val = val.replace(' ', ' ')
  257. val = val.replace(' *', '*')
  258. # first occurrence
  259. val = val.replace('*', ' *', 1)
  260. val = val.strip()
  261. else: # non pointer
  262. # cut-off last word on
  263. try:
  264. (param_type, param_name) = t.rsplit(' ', 1)
  265. except:
  266. param_type = t
  267. param_name = "param_name_not_specified"
  268. val = param_type.strip() + " REWRITE_NAME"
  269. # set back array
  270. if has_array:
  271. val += "[]"
  272. func_param_type.append(val)
  273. func_param_name.append(param_name.strip())
  274. new_proc = SdlProcedure(
  275. retval=func_ret, # Return value type
  276. name=func_name, # Function name
  277. comment=comment, # Function comment
  278. header=header_path.name, # Header file
  279. parameter=func_param_type, # List of parameters (type + anonymized param name 'REWRITE_NAME')
  280. parameter_name=func_param_name, # Real parameter name, or 'param_name_not_specified'
  281. )
  282. header_procedures.append(new_proc)
  283. if logger.getEffectiveLevel() <= logging.DEBUG:
  284. logger.debug("%s", pprint.pformat(new_proc))
  285. return header_procedures
  286. # Dump API into a json file
  287. def full_API_json(path: Path, procedures: list[SdlProcedure]):
  288. with path.open('w', newline='') as f:
  289. json.dump([dataclasses.asdict(proc) for proc in procedures], f, indent=4, sort_keys=True)
  290. logger.info("dump API to '%s'", path)
  291. class CallOnce:
  292. def __init__(self, cb):
  293. self._cb = cb
  294. self._called = False
  295. def __call__(self, *args, **kwargs):
  296. if self._called:
  297. return
  298. self._called = True
  299. self._cb(*args, **kwargs)
  300. # Check public function comments are correct
  301. def print_check_comment_header():
  302. logger.warning("")
  303. logger.warning("Please fix following warning(s):")
  304. logger.warning("--------------------------------")
  305. def check_documentations(procedures: list[SdlProcedure]) -> None:
  306. check_comment_header = CallOnce(print_check_comment_header)
  307. warning_header_printed = False
  308. # Check \param
  309. for proc in procedures:
  310. expected = len(proc.parameter)
  311. if expected == 1:
  312. if proc.parameter[0] == 'void':
  313. expected = 0
  314. count = proc.comment.count("\\param")
  315. if count != expected:
  316. # skip SDL_stdinc.h
  317. if proc.header != 'SDL_stdinc.h':
  318. # Warning mismatch \param and function prototype
  319. check_comment_header()
  320. logger.warning(" In file %s: function %s() has %d '\\param' but expected %d", proc.header, proc.name, count, expected)
  321. # Warning check \param uses the correct parameter name
  322. # skip SDL_stdinc.h
  323. if proc.header != 'SDL_stdinc.h':
  324. for n in proc.parameter_name:
  325. if n != "" and "\\param " + n not in proc.comment and "\\param[out] " + n not in proc.comment:
  326. check_comment_header()
  327. logger.warning(" In file %s: function %s() missing '\\param %s'", proc.header, proc.name, n)
  328. # Check \returns
  329. for proc in procedures:
  330. expected = 1
  331. if proc.retval == 'void':
  332. expected = 0
  333. count = proc.comment.count("\\returns")
  334. if count != expected:
  335. # skip SDL_stdinc.h
  336. if proc.header != 'SDL_stdinc.h':
  337. # Warning mismatch \param and function prototype
  338. check_comment_header()
  339. logger.warning(" In file %s: function %s() has %d '\\returns' but expected %d" % (proc.header, proc.name, count, expected))
  340. # Check \since
  341. for proc in procedures:
  342. expected = 1
  343. count = proc.comment.count("\\since")
  344. if count != expected:
  345. # skip SDL_stdinc.h
  346. if proc.header != 'SDL_stdinc.h':
  347. # Warning mismatch \param and function prototype
  348. check_comment_header()
  349. logger.warning(" In file %s: function %s() has %d '\\since' but expected %d" % (proc.header, proc.name, count, expected))
  350. # Parse 'sdl_dynapi_procs_h' file to find existing functions
  351. def find_existing_proc_names() -> list[str]:
  352. reg = re.compile(r'SDL_DYNAPI_PROC\([^,]*,([^,]*),.*\)')
  353. ret = []
  354. with SDL_DYNAPI_PROCS_H.open() as f:
  355. for line in f:
  356. match = reg.match(line)
  357. if not match:
  358. continue
  359. existing_func = match.group(1)
  360. ret.append(existing_func)
  361. return ret
  362. # Get list of SDL headers
  363. def get_header_list() -> list[Path]:
  364. ret = []
  365. for f in SDL_INCLUDE_DIR.iterdir():
  366. # Only *.h files
  367. if f.is_file() and f.suffix == ".h":
  368. ret.append(f)
  369. else:
  370. logger.debug("Skip %s", f)
  371. # Order headers for reproducible behavior
  372. ret.sort()
  373. return ret
  374. # Write the new API in files: _procs.h _overrivides.h and .sym
  375. def add_dyn_api(proc: SdlProcedure) -> None:
  376. decl_args: list[str] = []
  377. call_args = []
  378. for i, argtype in enumerate(proc.parameter):
  379. # Special case, void has no parameter name
  380. if argtype == "void":
  381. assert len(decl_args) == 0
  382. assert len(proc.parameter) == 1
  383. decl_args.append("void")
  384. continue
  385. # Var name: a, b, c, ...
  386. varname = chr(ord('a') + i)
  387. decl_args.append(argtype.replace("REWRITE_NAME", varname))
  388. if argtype != "...":
  389. call_args.append(varname)
  390. macro_args = (
  391. proc.retval,
  392. proc.name,
  393. "({})".format(",".join(decl_args)),
  394. "({})".format(",".join(call_args)),
  395. "" if proc.retval == "void" else "return",
  396. )
  397. # File: SDL_dynapi_procs.h
  398. #
  399. # Add at last
  400. # SDL_DYNAPI_PROC(SDL_EGLConfig,SDL_EGL_GetCurrentConfig,(void),(),return)
  401. with SDL_DYNAPI_PROCS_H.open("a", newline="") as f:
  402. if proc.variadic:
  403. f.write("#ifndef SDL_DYNAPI_PROC_NO_VARARGS\n")
  404. f.write(f"SDL_DYNAPI_PROC({','.join(macro_args)})\n")
  405. if proc.variadic:
  406. f.write("#endif\n")
  407. # File: SDL_dynapi_overrides.h
  408. #
  409. # Add at last
  410. # "#define SDL_DelayNS SDL_DelayNS_REAL
  411. f = open(SDL_DYNAPI_OVERRIDES_H, "a", newline="")
  412. f.write(f"#define {proc.name} {proc.name}_REAL\n")
  413. f.close()
  414. # File: SDL_dynapi.sym
  415. #
  416. # Add before "extra symbols go here" line
  417. with SDL_DYNAPI_SYM.open() as f:
  418. new_input = []
  419. for line in f:
  420. if "extra symbols go here" in line:
  421. new_input.append(f" {proc.name};\n")
  422. new_input.append(line)
  423. with SDL_DYNAPI_SYM.open('w', newline='') as f:
  424. for line in new_input:
  425. f.write(line)
  426. def main():
  427. parser = argparse.ArgumentParser()
  428. parser.set_defaults(loglevel=logging.INFO)
  429. parser.add_argument('--dump', nargs='?', default=None, const="sdl.json", metavar="JSON", help='output all SDL API into a .json file')
  430. parser.add_argument('--debug', action='store_const', const=logging.DEBUG, dest="loglevel", help='add debug traces')
  431. args = parser.parse_args()
  432. logging.basicConfig(level=args.loglevel, format='[%(levelname)s] %(message)s')
  433. # Get list of SDL headers
  434. sdl_list_includes = get_header_list()
  435. procedures = []
  436. for filename in sdl_list_includes:
  437. header_procedures = parse_header(filename)
  438. procedures.extend(header_procedures)
  439. # Parse 'sdl_dynapi_procs_h' file to find existing functions
  440. existing_proc_names = find_existing_proc_names()
  441. for procedure in procedures:
  442. if procedure.name not in existing_proc_names:
  443. logger.info("NEW %s", procedure.name)
  444. add_dyn_api(procedure)
  445. if args.dump:
  446. # Dump API into a json file
  447. full_API_json(path=Path(args.dump), procedures=procedures)
  448. # Check comment formatting
  449. check_documentations(procedures)
  450. if __name__ == '__main__':
  451. raise SystemExit(main())