#!/usr/bin/env python3 # Simple DirectMedia Layer # Copyright (C) 1997-2025 Sam Lantinga # # This software is provided 'as-is', without any express or implied # warranty. In no event will the authors be held liable for any damages # arising from the use of this software. # # Permission is granted to anyone to use this software for any purpose, # including commercial applications, and to alter it and redistribute it # freely, subject to the following restrictions: # # 1. The origin of this software must not be misrepresented; you must not # claim that you wrote the original software. If you use this software # in a product, an acknowledgment in the product documentation would be # appreciated but is not required. # 2. Altered source versions must be plainly marked as such, and must not be # misrepresented as being the original software. # 3. This notice may not be removed or altered from any source distribution. # WHAT IS THIS? # When you add a public API to SDL, please run this script, make sure the # output looks sane (git diff, it adds to existing files), and commit it. # It keeps the dynamic API jump table operating correctly. # # Platform-specific API: # After running the script, you have to manually add #ifdef SDL_PLATFORM_WIN32 # or similar around the function in 'SDL_dynapi_procs.h'. # import argparse import dataclasses import json import logging import os from pathlib import Path import pprint import re SDL_ROOT = Path(__file__).resolve().parents[2] SDL_INCLUDE_DIR = SDL_ROOT / "include/SDL3" SDL_DYNAPI_PROCS_H = SDL_ROOT / "src/dynapi/SDL_dynapi_procs.h" SDL_DYNAPI_OVERRIDES_H = SDL_ROOT / "src/dynapi/SDL_dynapi_overrides.h" SDL_DYNAPI_SYM = SDL_ROOT / "src/dynapi/SDL_dynapi.sym" RE_EXTERN_C = re.compile(r'.*extern[ "]*C[ "].*') RE_COMMENT_REMOVE_CONTENT = re.compile(r'\/\*.*\*/') RE_PARSING_FUNCTION = re.compile(r'(.*SDLCALL[^\(\)]*) ([a-zA-Z0-9_]+) *\((.*)\) *;.*') #eg: # void (SDLCALL *callback)(void*, int) # \1(\2)\3 RE_PARSING_CALLBACK = re.compile(r'([^\(\)]*)\(([^\(\)]+)\)(.*)') logger = logging.getLogger(__name__) @dataclasses.dataclass(frozen=True) class SdlProcedure: retval: str name: str parameter: list[str] parameter_name: list[str] header: str comment: str @property def variadic(self) -> bool: return "..." in self.parameter def parse_header(header_path: Path) -> list[SdlProcedure]: logger.debug("Parse header: %s", header_path) header_procedures = [] parsing_function = False current_func = "" parsing_comment = False current_comment = "" ignore_wiki_documentation = False with header_path.open() as f: for line in f: # Skip lines if we're in a wiki documentation block. if ignore_wiki_documentation: if line.startswith("#endif"): ignore_wiki_documentation = False continue # Discard wiki documentations blocks. if line.startswith("#ifdef SDL_WIKI_DOCUMENTATION_SECTION"): ignore_wiki_documentation = True continue # Discard pre-processor directives ^#.* if line.startswith("#"): continue # Discard "extern C" line match = RE_EXTERN_C.match(line) if match: continue # Remove one line comment // ... # eg: extern SDL_DECLSPEC SDL_hid_device * SDLCALL SDL_hid_open_path(const char *path, int bExclusive /* = false */) line = RE_COMMENT_REMOVE_CONTENT.sub('', line) # Get the comment block /* ... */ across several lines match_start = "/*" in line match_end = "*/" in line if match_start and match_end: continue if match_start: parsing_comment = True current_comment = line continue if match_end: parsing_comment = False current_comment += line continue if parsing_comment: current_comment += line continue # Get the function prototype across several lines if parsing_function: # Append to the current function current_func += " " current_func += line.strip() else: # if is contains "extern", start grabbing if "extern" not in line: continue # Start grabbing the new function current_func = line.strip() parsing_function = True # If it contains ';', then the function is complete if ";" not in current_func: continue # Got function/comment, reset vars parsing_function = False func = current_func comment = current_comment current_func = "" current_comment = "" # Discard if it doesn't contain 'SDLCALL' if "SDLCALL" not in func: logger.debug(" Discard, doesn't have SDLCALL: %r", func) continue # Discard if it contains 'SDLMAIN_DECLSPEC' (these are not SDL symbols). if "SDLMAIN_DECLSPEC" in func: logger.debug(" Discard, has SDLMAIN_DECLSPEC: %r", func) continue logger.debug("Raw data: %r", func) # Replace unusual stuff... func = func.replace(" SDL_PRINTF_VARARG_FUNC(1)", "") func = func.replace(" SDL_PRINTF_VARARG_FUNC(2)", "") func = func.replace(" SDL_PRINTF_VARARG_FUNC(3)", "") func = func.replace(" SDL_PRINTF_VARARG_FUNC(4)", "") func = func.replace(" SDL_PRINTF_VARARG_FUNCV(1)", "") func = func.replace(" SDL_PRINTF_VARARG_FUNCV(2)", "") func = func.replace(" SDL_PRINTF_VARARG_FUNCV(3)", "") func = func.replace(" SDL_PRINTF_VARARG_FUNCV(4)", "") func = func.replace(" SDL_WPRINTF_VARARG_FUNC(3)", "") func = func.replace(" SDL_WPRINTF_VARARG_FUNCV(3)", "") func = func.replace(" SDL_SCANF_VARARG_FUNC(2)", "") func = func.replace(" SDL_SCANF_VARARG_FUNCV(2)", "") func = func.replace(" SDL_ANALYZER_NORETURN", "") func = func.replace(" SDL_MALLOC", "") func = func.replace(" SDL_ALLOC_SIZE2(1, 2)", "") func = func.replace(" SDL_ALLOC_SIZE(2)", "") func = re.sub(r" SDL_ACQUIRE\(.*\)", "", func) func = re.sub(r" SDL_ACQUIRE_SHARED\(.*\)", "", func) func = re.sub(r" SDL_TRY_ACQUIRE\(.*\)", "", func) func = re.sub(r" SDL_TRY_ACQUIRE_SHARED\(.*\)", "", func) func = re.sub(r" SDL_RELEASE\(.*\)", "", func) func = re.sub(r" SDL_RELEASE_SHARED\(.*\)", "", func) func = re.sub(r" SDL_RELEASE_GENERIC\(.*\)", "", func) func = re.sub(r"([ (),])(SDL_IN_BYTECAP\([^)]*\))", r"\1", func) func = re.sub(r"([ (),])(SDL_OUT_BYTECAP\([^)]*\))", r"\1", func) func = re.sub(r"([ (),])(SDL_INOUT_Z_CAP\([^)]*\))", r"\1", func) func = re.sub(r"([ (),])(SDL_OUT_Z_CAP\([^)]*\))", r"\1", func) # Should be a valid function here match = RE_PARSING_FUNCTION.match(func) if not match: logger.error("Cannot parse: %s", func) raise ValueError(func) func_ret = match.group(1) func_name = match.group(2) func_params = match.group(3) # # Parse return value # func_ret = func_ret.replace('extern', ' ') func_ret = func_ret.replace('SDLCALL', ' ') func_ret = func_ret.replace('SDL_DECLSPEC', ' ') func_ret, _ = re.subn('([ ]{2,})', ' ', func_ret) # Remove trailing spaces in front of '*' func_ret = func_ret.replace(' *', '*') func_ret = func_ret.strip() # # Parse parameters # func_params = func_params.strip() if func_params == "": func_params = "void" # Identify each function parameters with type and name # (eventually there are callbacks of several parameters) tmp = func_params.split(',') tmp2 = [] param = "" for t in tmp: if param == "": param = t else: param = param + "," + t # Identify a callback or parameter when there is same count of '(' and ')' if param.count('(') == param.count(')'): tmp2.append(param.strip()) param = "" # Process each parameters, separation name and type func_param_type = [] func_param_name = [] for t in tmp2: if t == "void": func_param_type.append(t) func_param_name.append("") continue if t == "...": func_param_type.append(t) func_param_name.append("") continue param_name = "" # parameter is a callback if '(' in t: match = RE_PARSING_CALLBACK.match(t) if not match: logger.error("cannot parse callback: %s", t) raise ValueError(t) a = match.group(1).strip() b = match.group(2).strip() c = match.group(3).strip() try: (param_type, param_name) = b.rsplit('*', 1) except: param_type = t param_name = "param_name_not_specified" # bug rsplit ?? if param_name == "": param_name = "param_name_not_specified" # reconstruct a callback name for future parsing func_param_type.append(a + " (" + param_type.strip() + " *REWRITE_NAME)" + c) func_param_name.append(param_name.strip()) continue # array like "char *buf[]" has_array = False if t.endswith("[]"): t = t.replace("[]", "") has_array = True # pointer if '*' in t: try: (param_type, param_name) = t.rsplit('*', 1) except: param_type = t param_name = "param_name_not_specified" # bug rsplit ?? if param_name == "": param_name = "param_name_not_specified" val = param_type.strip() + "*REWRITE_NAME" # Remove trailing spaces in front of '*' tmp = "" while val != tmp: tmp = val val = val.replace(' ', ' ') val = val.replace(' *', '*') # first occurrence val = val.replace('*', ' *', 1) val = val.strip() else: # non pointer # cut-off last word on try: (param_type, param_name) = t.rsplit(' ', 1) except: param_type = t param_name = "param_name_not_specified" val = param_type.strip() + " REWRITE_NAME" # set back array if has_array: val += "[]" func_param_type.append(val) func_param_name.append(param_name.strip()) new_proc = SdlProcedure( retval=func_ret, # Return value type name=func_name, # Function name comment=comment, # Function comment header=header_path.name, # Header file parameter=func_param_type, # List of parameters (type + anonymized param name 'REWRITE_NAME') parameter_name=func_param_name, # Real parameter name, or 'param_name_not_specified' ) header_procedures.append(new_proc) if logger.getEffectiveLevel() <= logging.DEBUG: logger.debug("%s", pprint.pformat(new_proc)) return header_procedures # Dump API into a json file def full_API_json(path: Path, procedures: list[SdlProcedure]): with path.open('w', newline='') as f: json.dump([dataclasses.asdict(proc) for proc in procedures], f, indent=4, sort_keys=True) logger.info("dump API to '%s'", path) class CallOnce: def __init__(self, cb): self._cb = cb self._called = False def __call__(self, *args, **kwargs): if self._called: return self._called = True self._cb(*args, **kwargs) # Check public function comments are correct def print_check_comment_header(): logger.warning("") logger.warning("Please fix following warning(s):") logger.warning("--------------------------------") def check_documentations(procedures: list[SdlProcedure]) -> None: check_comment_header = CallOnce(print_check_comment_header) warning_header_printed = False # Check \param for proc in procedures: expected = len(proc.parameter) if expected == 1: if proc.parameter[0] == 'void': expected = 0 count = proc.comment.count("\\param") if count != expected: # skip SDL_stdinc.h if proc.header != 'SDL_stdinc.h': # Warning mismatch \param and function prototype check_comment_header() logger.warning(" In file %s: function %s() has %d '\\param' but expected %d", proc.header, proc.name, count, expected) # Warning check \param uses the correct parameter name # skip SDL_stdinc.h if proc.header != 'SDL_stdinc.h': for n in proc.parameter_name: if n != "" and "\\param " + n not in proc.comment and "\\param[out] " + n not in proc.comment: check_comment_header() logger.warning(" In file %s: function %s() missing '\\param %s'", proc.header, proc.name, n) # Check \returns for proc in procedures: expected = 1 if proc.retval == 'void': expected = 0 count = proc.comment.count("\\returns") if count != expected: # skip SDL_stdinc.h if proc.header != 'SDL_stdinc.h': # Warning mismatch \param and function prototype check_comment_header() logger.warning(" In file %s: function %s() has %d '\\returns' but expected %d" % (proc.header, proc.name, count, expected)) # Check \since for proc in procedures: expected = 1 count = proc.comment.count("\\since") if count != expected: # skip SDL_stdinc.h if proc.header != 'SDL_stdinc.h': # Warning mismatch \param and function prototype check_comment_header() logger.warning(" In file %s: function %s() has %d '\\since' but expected %d" % (proc.header, proc.name, count, expected)) # Parse 'sdl_dynapi_procs_h' file to find existing functions def find_existing_proc_names() -> list[str]: reg = re.compile(r'SDL_DYNAPI_PROC\([^,]*,([^,]*),.*\)') ret = [] with SDL_DYNAPI_PROCS_H.open() as f: for line in f: match = reg.match(line) if not match: continue existing_func = match.group(1) ret.append(existing_func) return ret # Get list of SDL headers def get_header_list() -> list[Path]: ret = [] for f in SDL_INCLUDE_DIR.iterdir(): # Only *.h files if f.is_file() and f.suffix == ".h": ret.append(f) else: logger.debug("Skip %s", f) # Order headers for reproducible behavior ret.sort() return ret # Write the new API in files: _procs.h _overrivides.h and .sym def add_dyn_api(proc: SdlProcedure) -> None: decl_args: list[str] = [] call_args = [] for i, argtype in enumerate(proc.parameter): # Special case, void has no parameter name if argtype == "void": assert len(decl_args) == 0 assert len(proc.parameter) == 1 decl_args.append("void") continue # Var name: a, b, c, ... varname = chr(ord('a') + i) decl_args.append(argtype.replace("REWRITE_NAME", varname)) if argtype != "...": call_args.append(varname) macro_args = ( proc.retval, proc.name, "({})".format(",".join(decl_args)), "({})".format(",".join(call_args)), "" if proc.retval == "void" else "return", ) # File: SDL_dynapi_procs.h # # Add at last # SDL_DYNAPI_PROC(SDL_EGLConfig,SDL_EGL_GetCurrentConfig,(void),(),return) with SDL_DYNAPI_PROCS_H.open("a", newline="") as f: if proc.variadic: f.write("#ifndef SDL_DYNAPI_PROC_NO_VARARGS\n") f.write(f"SDL_DYNAPI_PROC({','.join(macro_args)})\n") if proc.variadic: f.write("#endif\n") # File: SDL_dynapi_overrides.h # # Add at last # "#define SDL_DelayNS SDL_DelayNS_REAL f = open(SDL_DYNAPI_OVERRIDES_H, "a", newline="") f.write(f"#define {proc.name} {proc.name}_REAL\n") f.close() # File: SDL_dynapi.sym # # Add before "extra symbols go here" line with SDL_DYNAPI_SYM.open() as f: new_input = [] for line in f: if "extra symbols go here" in line: new_input.append(f" {proc.name};\n") new_input.append(line) with SDL_DYNAPI_SYM.open('w', newline='') as f: for line in new_input: f.write(line) def main(): parser = argparse.ArgumentParser() parser.set_defaults(loglevel=logging.INFO) parser.add_argument('--dump', nargs='?', default=None, const="sdl.json", metavar="JSON", help='output all SDL API into a .json file') parser.add_argument('--debug', action='store_const', const=logging.DEBUG, dest="loglevel", help='add debug traces') args = parser.parse_args() logging.basicConfig(level=args.loglevel, format='[%(levelname)s] %(message)s') # Get list of SDL headers sdl_list_includes = get_header_list() procedures = [] for filename in sdl_list_includes: header_procedures = parse_header(filename) procedures.extend(header_procedures) # Parse 'sdl_dynapi_procs_h' file to find existing functions existing_proc_names = find_existing_proc_names() for procedure in procedures: if procedure.name not in existing_proc_names: logger.info("NEW %s", procedure.name) add_dyn_api(procedure) if args.dump: # Dump API into a json file full_API_json(path=Path(args.dump), procedures=procedures) # Check comment formatting check_documentations(procedures) if __name__ == '__main__': raise SystemExit(main())