gendynapi.py 20 KB

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