123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547 |
- #!/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 _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())
|