gendynapi.py 19 KB

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