| #!/usr/bin/env python3 |
| |
| # Simple DirectMedia Layer |
| # Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org> |
| # |
| # 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 _overrides.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()) |