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