Atlas - gendynapi.py

Home / ext / SDL / src / dynapi Lines: 6 | Size: 19660 bytes [Download] [Show on GitHub] [Search similar files] [Raw] [Raw (proxy)]
[FILE BEGIN]
1#!/usr/bin/env python3 2 3# Simple DirectMedia Layer 4# Copyright (C) 1997-2025 Sam Lantinga <[email protected]> 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 _overrides.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()) 548
[FILE END]
(C) 2025 0x4248 (C) 2025 4248 Media and 4248 Systems, All part of 0x4248 See LICENCE files for more information. Not all files are by 0x4248 always check Licencing.