#!/usr/bin/env python3 # Simple DirectMedia Layer # Copyright (C) 1997-2023 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. # # OS-specific API: # After running the script, you have to manually add #ifdef __WIN32__ # or similar around the function in 'SDL_dynapi_procs.h' # import argparse import json import os import pathlib import pprint import re SDL_ROOT = pathlib.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" full_API = [] def main(): # Parse 'sdl_dynapi_procs_h' file to find existing functions existing_procs = find_existing_procs() # Get list of SDL headers sdl_list_includes = get_header_list() reg_externC = re.compile('.*extern[ "]*C[ "].*') reg_comment_remove_content = re.compile('\/\*.*\*/') reg_parsing_function = re.compile('(.*SDLCALL[^\(\)]*) ([a-zA-Z0-9_]+) *\((.*)\) *;.*') #eg: # void (SDLCALL *callback)(void*, int) # \1(\2)\3 reg_parsing_callback = re.compile('([^\(\)]*)\(([^\(\)]+)\)(.*)') for filename in sdl_list_includes: if args.debug: print("Parse header: %s" % filename) input = open(filename) parsing_function = False current_func = "" parsing_comment = False current_comment = "" for line in input: # Discard pre-processor directives ^#.* if line.startswith("#"): continue # Discard "extern C" line match = reg_externC.match(line) if match: continue # Remove one line comment /* ... */ # eg: extern DECLSPEC SDL_hid_device * SDLCALL SDL_hid_open_path(const char *path, int bExclusive /* = false */); line = reg_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: if args.debug: print(" Discard: " + func) continue if args.debug: print(" Raw data: " + 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_WPRINTF_VARARG_FUNC(3)", ""); func = func.replace(" SDL_SCANF_VARARG_FUNC(2)", ""); func = func.replace(" __attribute__((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(" SDL_ACQUIRE\(.*\)", "", func); func = re.sub(" SDL_ACQUIRE_SHARED\(.*\)", "", func); func = re.sub(" SDL_TRY_ACQUIRE\(.*\)", "", func); func = re.sub(" SDL_TRY_ACQUIRE_SHARED\(.*\)", "", func); func = re.sub(" SDL_RELEASE\(.*\)", "", func); func = re.sub(" SDL_RELEASE_SHARED\(.*\)", "", func); # Should be a valid function here match = reg_parsing_function.match(func) if not match: print("Cannot parse: "+ func) exit(-1) 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('DECLSPEC', ' ') # Remove trailing spaces in front of '*' tmp = "" while func_ret != tmp: tmp = func_ret func_ret = func_ret.replace(' ', ' ') 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 = reg_parsing_callback.match(t) if not match: print("cannot parse callback: " + t); exit(-1) 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 = {} # Return value type new_proc['retval'] = func_ret # List of parameters (type + anonymized param name 'REWRITE_NAME') new_proc['parameter'] = func_param_type # Real parameter name, or 'param_name_not_specified' new_proc['parameter_name'] = func_param_name # Function name new_proc['name'] = func_name # Header file new_proc['header'] = os.path.basename(filename) # Function comment new_proc['comment'] = comment full_API.append(new_proc) if args.debug: pprint.pprint(new_proc); print("\n") if func_name not in existing_procs: print("NEW " + func) add_dyn_api(new_proc) # For-End line in input input.close() # For-End parsing all files of sdl_list_includes # Dump API into a json file full_API_json() # Check comment formatting check_comment(); # Dump API into a json file def full_API_json(): if args.dump: filename = 'sdl.json' with open(filename, 'w') as f: json.dump(full_API, f, indent=4, sort_keys=True) print("dump API to '%s'" % filename); # Check public function comments are correct def check_comment_header(): if not check_comment_header.done: check_comment_header.done = True print("") print("Please fix following warning(s):") print("-------------------------------") def check_comment(): check_comment_header.done = False # Check \param for i in full_API: comment = i['comment'] name = i['name'] retval = i['retval'] header = i['header'] expected = len(i['parameter']) if expected == 1: if i['parameter'][0] == 'void': expected = 0; count = comment.count("\\param") if count != expected: # skip SDL_stdinc.h if header != 'SDL_stdinc.h': # Warning mismatch \param and function prototype check_comment_header() print(" In file %s: function %s() has %d '\\param' but expected %d" % (header, name, count, expected)); # Warning check \param uses the correct parameter name # skip SDL_stdinc.h if header != 'SDL_stdinc.h': parameter_name = i['parameter_name'] for n in parameter_name: if n != "" and "\\param " + n not in comment: check_comment_header() print(" In file %s: function %s() missing '\\param %s'" % (header, name, n)); # Check \returns for i in full_API: comment = i['comment'] name = i['name'] retval = i['retval'] header = i['header'] expected = 1 if retval == 'void': expected = 0; count = comment.count("\\returns") if count != expected: # skip SDL_stdinc.h if header != 'SDL_stdinc.h': # Warning mismatch \param and function prototype check_comment_header() print(" In file %s: function %s() has %d '\\returns' but expected %d" % (header, name, count, expected)); # Check \since for i in full_API: comment = i['comment'] name = i['name'] retval = i['retval'] header = i['header'] expected = 1 count = comment.count("\\since") if count != expected: # skip SDL_stdinc.h if header != 'SDL_stdinc.h': # Warning mismatch \param and function prototype check_comment_header() print(" In file %s: function %s() has %d '\\since' but expected %d" % (header, name, count, expected)); # Parse 'sdl_dynapi_procs_h' file to find existing functions def find_existing_procs(): reg = re.compile('SDL_DYNAPI_PROC\([^,]*,([^,]*),.*\)') ret = [] input = open(SDL_DYNAPI_PROCS_H) for line in input: match = reg.match(line) if not match: continue existing_func = match.group(1) ret.append(existing_func); # print(existing_func) input.close() return ret # Get list of SDL headers def get_header_list(): reg = re.compile('^.*\.h$') ret = [] tmp = os.listdir(SDL_INCLUDE_DIR) for f in tmp: # Only *.h files match = reg.match(f) if not match: if args.debug: print("Skip %s" % f) continue ret.append(SDL_INCLUDE_DIR / f) return ret # Write the new API in files: _procs.h _overrivides.h and .sym def add_dyn_api(proc): func_name = proc['name'] func_ret = proc['retval'] func_argtype = proc['parameter'] # File: SDL_dynapi_procs.h # # Add at last # SDL_DYNAPI_PROC(SDL_EGLConfig,SDL_EGL_GetCurrentEGLConfig,(void),(),return) f = open(SDL_DYNAPI_PROCS_H, "a") dyn_proc = "SDL_DYNAPI_PROC(" + func_ret + "," + func_name + ",(" i = ord('a') remove_last = False for argtype in func_argtype: # Special case, void has no parameter name if argtype == "void": dyn_proc += "void" continue # Var name: a, b, c, ... varname = chr(i) i += 1 tmp = argtype.replace("REWRITE_NAME", varname) dyn_proc += tmp + ", " remove_last = True # remove last 2 char ', ' if remove_last: dyn_proc = dyn_proc[:-1] dyn_proc = dyn_proc[:-1] dyn_proc += "),(" i = ord('a') remove_last = False for argtype in func_argtype: # Special case, void has no parameter name if argtype == "void": continue # Special case, '...' has no parameter name if argtype == "...": continue # Var name: a, b, c, ... varname = chr(i) i += 1 dyn_proc += varname + "," remove_last = True # remove last char ',' if remove_last: dyn_proc = dyn_proc[:-1] dyn_proc += ")," if func_ret != "void": dyn_proc += "return" dyn_proc += ")" f.write(dyn_proc + "\n") f.close() # File: SDL_dynapi_overrides.h # # Add at last # "#define SDL_DelayNS SDL_DelayNS_REAL f = open(SDL_DYNAPI_OVERRIDES_H, "a") f.write("#define " + func_name + " " + func_name + "_REAL\n") f.close() # File: SDL_dynapi.sym # # Add before "extra symbols go here" line input = open(SDL_DYNAPI_SYM) new_input = [] for line in input: if "extra symbols go here" in line: new_input.append(" " + func_name + ";\n") new_input.append(line) input.close() f = open(SDL_DYNAPI_SYM, 'w') for line in new_input: f.write(line) f.close() if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--dump', help='output all SDL API into a .json file', action='store_true') parser.add_argument('--debug', help='add debug traces', action='store_true') args = parser.parse_args() try: main() except Exception as e: print(e) exit(-1) print("done!") exit(0)