SDL2/src/dynapi/gendynapi.py

541 lines
17 KiB
Python
Executable file

#!/usr/bin/env python3
# Simple DirectMedia Layer
# Copyright (C) 1997-2023 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.
#
# 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_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_TRY_ACQUIRE\(.*\)", "", func);
func = re.sub(" SDL_RELEASE\(.*\)", "", 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 trailling 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"
# recontruct 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 trailling spaces in front of '*'
tmp = ""
while val != tmp:
tmp = val
val = val.replace(' ', ' ')
val = val.replace(' *', '*')
# first occurence
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 commment formating
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);
# Dump API into a json file
def check_comment():
if args.check_comment:
print("check comment formating");
# 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 missmatch \param and function prototype
print("%s: %s() %d '\\param'' but expected %d" % (header, name, count, expected));
# 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 missmatch \param and function prototype
print("%s: %s() %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 missmatch \param and function prototype
print("%s: %s() %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('--check-comment', help='check comment formating', 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)