gendynapi.py 17 KB

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