From b3e17ea9d48bf2f0e159cc74fe6ca68aa53391c4 Mon Sep 17 00:00:00 2001 From: Vadim Levin Date: Fri, 16 May 2025 12:26:10 +0300 Subject: [PATCH] feat: add conditional inclusion support to header parser --- ...penCVBindingsPreprocessorDefinitions.cmake | 63 ++++++ modules/java/generator/CMakeLists.txt | 9 + modules/java/generator/gen_java.py | 12 +- modules/js/generator/CMakeLists.txt | 46 ++++- modules/js/generator/embindgen.py | 76 ++++--- modules/objc/generator/CMakeLists.txt | 12 +- modules/objc/generator/gen_objc.py | 12 +- modules/python/bindings/CMakeLists.txt | 44 +++- modules/python/src2/gen2.py | 62 +++++- modules/python/src2/hdr_parser.py | 194 ++++++++++++++++-- 10 files changed, 466 insertions(+), 64 deletions(-) create mode 100644 cmake/OpenCVBindingsPreprocessorDefinitions.cmake diff --git a/cmake/OpenCVBindingsPreprocessorDefinitions.cmake b/cmake/OpenCVBindingsPreprocessorDefinitions.cmake new file mode 100644 index 0000000000..2828e638a7 --- /dev/null +++ b/cmake/OpenCVBindingsPreprocessorDefinitions.cmake @@ -0,0 +1,63 @@ +function(ocv_bindings_generator_populate_preprocessor_definitions + opencv_modules + output_variable) + set(defs "\"CV_VERSION_MAJOR\": ${OPENCV_VERSION_MAJOR}") + + macro(ocv_add_definition name value) + set(defs "${defs},\n\"${name}\": ${value}") + endmacro() + + ocv_add_definition(CV_VERSION_MINOR ${OPENCV_VERSION_MINOR}) + ocv_add_definition(CV_VERSION_PATCH ${OPENCV_VERSION_PATCH}) + ocv_add_definition(OPENCV_ABI_COMPATIBILITY "${OPENCV_VERSION_MAJOR}00") + + foreach(module IN LISTS ${opencv_modules}) + if(HAVE_${module}) + string(TOUPPER "${module}" module) + ocv_add_definition("HAVE_${module}" 1) + endif() + endforeach() + if(HAVE_EIGEN) + ocv_add_definition(HAVE_EIGEN 1) + ocv_add_definition(EIGEN_WORLD_VERSION ${EIGEN_WORLD_VERSION}) + ocv_add_definition(EIGEN_MAJOR_VERSION ${EIGEN_MAJOR_VERSION}) + ocv_add_definition(EIGEN_MINOR_VERSION ${EIGEN_MINOR_VERSION}) + else() + # Some checks in parsed headers might not be protected with HAVE_EIGEN check + ocv_add_definition(EIGEN_WORLD_VERSION 0) + ocv_add_definition(EIGEN_MAJOR_VERSION 0) + ocv_add_definition(EIGEN_MINOR_VERSION 0) + endif() + if(HAVE_LAPACK) + ocv_add_definition(HAVE_LAPACK 1) + endif() + + if(OPENCV_DISABLE_FILESYSTEM_SUPPORT) + ocv_add_definition(OPENCV_HAVE_FILESYSTEM_SUPPORT 0) + else() + ocv_add_definition(OPENCV_HAVE_FILESYSTEM_SUPPORT 1) + endif() + + ocv_add_definition(OPENCV_BINDINGS_PARSER 1) + + # Implementation details definitions, having no impact on how bindings are + # generated, so their real values can be safely ignored + ocv_add_definition(CV_ENABLE_UNROLLED 0) + ocv_add_definition(CV__EXCEPTION_PTR 0) + ocv_add_definition(CV_NEON 0) + ocv_add_definition(TBB_INTERFACE_VERSION 0) + ocv_add_definition(CV_SSE2 0) + ocv_add_definition(CV_VSX 0) + ocv_add_definition(OPENCV_SUPPORTS_FP_DENORMALS_HINT 0) + ocv_add_definition(CV_LOG_STRIP_LEVEL 0) + ocv_add_definition(CV_LOG_LEVEL_SILENT 0) + ocv_add_definition(CV_LOG_LEVEL_FATAL 1) + ocv_add_definition(CV_LOG_LEVEL_ERROR 2) + ocv_add_definition(CV_LOG_LEVEL_WARN 3) + ocv_add_definition(CV_LOG_LEVEL_INFO 4) + ocv_add_definition(CV_LOG_LEVEL_DEBUG 5) + ocv_add_definition(CV_LOG_LEVEL_VERBOSE 6) + ocv_add_definition(CERES_FOUND 0) + + set(${output_variable} ${defs} PARENT_SCOPE) +endfunction() diff --git a/modules/java/generator/CMakeLists.txt b/modules/java/generator/CMakeLists.txt index b8ae34023b..130e6d5fec 100644 --- a/modules/java/generator/CMakeLists.txt +++ b/modules/java/generator/CMakeLists.txt @@ -56,6 +56,12 @@ foreach(m ${OPENCV_JAVA_MODULES}) ocv_remap_files(misc_files) endforeach(m) +include("${OpenCV_SOURCE_DIR}/cmake/OpenCVBindingsPreprocessorDefinitions.cmake") +ocv_bindings_generator_populate_preprocessor_definitions( + OPENCV_MODULES_BUILD + opencv_preprocessor_defs +) + set(CONFIG_FILE "${CMAKE_CURRENT_BINARY_DIR}/gen_java.json") set(__config_str "{ @@ -63,6 +69,9 @@ set(__config_str \"modules\": [ ${__modules_config} ], + \"preprocessor_definitions\": { +${opencv_preprocessor_defs} + }, \"files_remap\": [ ${__remap_config} ] diff --git a/modules/java/generator/gen_java.py b/modules/java/generator/gen_java.py index a77ca199f3..0ffa5bd6ae 100755 --- a/modules/java/generator/gen_java.py +++ b/modules/java/generator/gen_java.py @@ -591,12 +591,16 @@ class JavaWrapperGenerator(object): f.write(buf) updated_files += 1 - def gen(self, srcfiles, module, output_path, output_jni_path, output_java_path, common_headers): + def gen(self, srcfiles, module, output_path, output_jni_path, output_java_path, common_headers, + preprocessor_definitions=None): self.clear() self.module = module self.Module = module.capitalize() # TODO: support UMat versions of declarations (implement UMat-wrapper for Java) - parser = hdr_parser.CppHeaderParser(generate_umat_decls=False) + parser = hdr_parser.CppHeaderParser( + generate_umat_decls=False, + preprocessor_definitions=preprocessor_definitions + ) self.add_class( ['class cv.' + self.Module, '', [], []] ) # [ 'class/struct cname', ':bases', [modlist] [props] ] @@ -1444,6 +1448,7 @@ if __name__ == "__main__": gen_dict_files = [] print("JAVA: Processing OpenCV modules: %d" % len(config['modules'])) + preprocessor_definitions = config.get('preprocessor_definitions', None) for e in config['modules']: (module, module_location) = (e['name'], os.path.join(ROOT_DIR, e['location'])) logging.info("\n=== MODULE: %s (%s) ===\n" % (module, module_location)) @@ -1508,7 +1513,8 @@ if __name__ == "__main__": copy_java_files(java_test_files_dir, java_test_base_path, 'org/opencv/test/' + module) if len(srcfiles) > 0: - generator.gen(srcfiles, module, dstdir, jni_path, java_path, common_headers) + generator.gen(srcfiles, module, dstdir, jni_path, java_path, common_headers, + preprocessor_definitions) else: logging.info("No generated code for module: %s", module) generator.finalize(jni_path) diff --git a/modules/js/generator/CMakeLists.txt b/modules/js/generator/CMakeLists.txt index c66608e917..d135b151eb 100644 --- a/modules/js/generator/CMakeLists.txt +++ b/modules/js/generator/CMakeLists.txt @@ -30,7 +30,14 @@ ocv_list_filterout(opencv_hdrs "modules/core/include/opencv2/core/utils/*.privat ocv_list_filterout(opencv_hdrs "modules/core/include/opencv2/core/utils/instrumentation.hpp") ocv_list_filterout(opencv_hdrs "modules/core/include/opencv2/core/utils/trace*") -ocv_update_file("${CMAKE_CURRENT_BINARY_DIR}/headers.txt" "${opencv_hdrs}") +set(config_json_headers_list "") +foreach(header IN LISTS opencv_hdrs) + if(NOT config_json_headers_list STREQUAL "") + set(config_json_headers_list "${config_json_headers_list},\n\"${header}\"") + else() + set(config_json_headers_list "\"${header}\"") + endif() +endforeach() set(bindings_cpp "${OPENCV_JS_BINDINGS_DIR}/gen/bindings.cpp") @@ -55,16 +62,42 @@ else() message(STATUS "Use autogenerated whitelist ${OPENCV_JS_WHITELIST_FILE}") endif() +include("${OpenCV_SOURCE_DIR}/cmake/OpenCVBindingsPreprocessorDefinitions.cmake") +ocv_bindings_generator_populate_preprocessor_definitions( + OPENCV_MODULES_BUILD + opencv_preprocessor_defs +) + +set(__config_str +"{ + \"headers\": [ +${config_json_headers_list} + ], + \"preprocessor_definitions\": { +${opencv_preprocessor_defs} + }, + \"core_bindings_file_path\": \"${JS_SOURCE_DIR}/src/core_bindings.cpp\" +}") +set(JSON_CONFIG_FILE_PATH "${CMAKE_CURRENT_BINARY_DIR}/gen_js_config.json") +if(EXISTS "${JSON_CONFIG_FILE_PATH}") + file(READ "${JSON_CONFIG_FILE_PATH}" __content) +else() + set(__content "") +endif() +if(NOT "${__content}" STREQUAL "${__config_str}") + file(WRITE "${JSON_CONFIG_FILE_PATH}" "${__config_str}") +endif() +unset(__config_str) + add_custom_command( OUTPUT ${bindings_cpp} "${OPENCV_DEPHELPER}/gen_opencv_js_source" COMMAND ${PYTHON_DEFAULT_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/embindgen.py" - "${scripts_hdr_parser}" - "${bindings_cpp}" - "${CMAKE_CURRENT_BINARY_DIR}/headers.txt" - "${JS_SOURCE_DIR}/src/core_bindings.cpp" - "${OPENCV_JS_WHITELIST_FILE}" + --parser "${scripts_hdr_parser}" + --output_file "${bindings_cpp}" + --config "${JSON_CONFIG_FILE_PATH}" + --whitelist "${OPENCV_JS_WHITELIST_FILE}" COMMAND ${CMAKE_COMMAND} -E touch "${OPENCV_DEPHELPER}/gen_opencv_js_source" WORKING_DIRECTORY @@ -73,6 +106,7 @@ add_custom_command( ${JS_SOURCE_DIR}/src/core_bindings.cpp ${CMAKE_CURRENT_SOURCE_DIR}/embindgen.py ${CMAKE_CURRENT_SOURCE_DIR}/templates.py + ${JSON_CONFIG_FILE_PATH} "${OPENCV_JS_WHITELIST_FILE}" ${scripts_hdr_parser} #(not needed - generated by CMake) ${CMAKE_CURRENT_BINARY_DIR}/headers.txt diff --git a/modules/js/generator/embindgen.py b/modules/js/generator/embindgen.py index 8352893133..d5d600e83f 100644 --- a/modules/js/generator/embindgen.py +++ b/modules/js/generator/embindgen.py @@ -319,7 +319,7 @@ class Namespace(object): class JSWrapperGenerator(object): - def __init__(self): + def __init__(self, preprocessor_definitions=None): self.bindings = [] self.wrapper_funcs = [] @@ -328,7 +328,9 @@ class JSWrapperGenerator(object): self.namespaces = {} self.enums = {} # FIXIT 'enums' should belong to 'namespaces' - self.parser = hdr_parser.CppHeaderParser() + self.parser = hdr_parser.CppHeaderParser( + preprocessor_definitions=preprocessor_definitions + ) self.class_idx = 0 def add_class(self, stype, name, decl): @@ -962,41 +964,69 @@ class JSWrapperGenerator(object): if __name__ == "__main__": - if len(sys.argv) < 5: - print("Usage:\n", \ - os.path.basename(sys.argv[0]), \ - " ") - print("Current args are: ", ", ".join(["'"+a+"'" for a in sys.argv])) - exit(1) + import argparse - dstdir = "." - hdr_parser_path = os.path.abspath(sys.argv[1]) + arg_parser = argparse.ArgumentParser( + description="OpenCV JavaScript bindings generator" + ) + arg_parser.add_argument( + "-p", "--parser", + required=True, + help="Full path to OpenCV header parser `hdr_parser.py`" + ) + arg_parser.add_argument( + "-o", "--output_file", + dest="output_file_path", + required=True, + help="Path to output file containing js bindings" + ) + arg_parser.add_argument( + "-c", "--config", + dest="config_json_path", + required=True, + help="Path to generator configuration file in .json format" + ) + arg_parser.add_argument( + "--whitelist", + dest="whitelist_file_path", + required=True, + help="Path to whitelist.js or opencv_js.config.py" + ) + args = arg_parser.parse_args() + + # import header parser + hdr_parser_path = os.path.abspath(args.parser) if hdr_parser_path.endswith(".py"): hdr_parser_path = os.path.dirname(hdr_parser_path) sys.path.append(hdr_parser_path) import hdr_parser - bindingsCpp = sys.argv[2] - headers = open(sys.argv[3], 'r').read().split(';') - coreBindings = sys.argv[4] - whiteListFile = sys.argv[5] + with open(args.config_json_path, "r") as fh: + config_json = json.load(fh) + headers = config_json.get("headers", ()) - if whiteListFile.endswith(".json") or whiteListFile.endswith(".JSON"): - with open(whiteListFile) as f: + bindings_cpp = args.output_file_path + core_bindings_path = config_json["core_bindings_file_path"] + whitelist_file_path = args.whitelist_file_path + + if whitelist_file_path.endswith(".json") or whitelist_file_path.endswith(".JSON"): + with open(whitelist_file_path) as f: gen_dict = json.load(f) - f.close() white_list = makeWhiteListJson(gen_dict) namespace_prefix_override = makeNamespacePrefixOverride(gen_dict) - elif whiteListFile.endswith(".py") or whiteListFile.endswith(".PY"): - exec(open(whiteListFile).read()) - assert(white_list) + elif whitelist_file_path.endswith(".py") or whitelist_file_path.endswith(".PY"): + with open(whitelist_file_path) as fh: + exec(fh.read()) + assert white_list namespace_prefix_override = { 'dnn' : '', 'aruco' : '', } else: - print("Unexpected format of OpenCV config file", whiteListFile) + print("Unexpected format of OpenCV config file", whitelist_file_path) exit(1) - generator = JSWrapperGenerator() - generator.gen(bindingsCpp, headers, coreBindings) + generator = JSWrapperGenerator( + preprocessor_definitions=config_json.get("preprocessor_definitions", None) + ) + generator.gen(bindings_cpp, headers, core_bindings_path) diff --git a/modules/objc/generator/CMakeLists.txt b/modules/objc/generator/CMakeLists.txt index bd8f8325b3..1d7580f412 100644 --- a/modules/objc/generator/CMakeLists.txt +++ b/modules/objc/generator/CMakeLists.txt @@ -38,6 +38,13 @@ if(HAVE_opencv_objc) set(__objc_build_dir "\"objc_build_dir\": \"${CMAKE_CURRENT_BINARY_DIR}/../objc\",") endif() +include("${OpenCV_SOURCE_DIR}/cmake/OpenCVBindingsPreprocessorDefinitions.cmake") + +ocv_bindings_generator_populate_preprocessor_definitions( + OPENCV_MODULES_BUILD + opencv_preprocessor_defs +) + set(CONFIG_FILE "${CMAKE_CURRENT_BINARY_DIR}/gen_objc.json") set(__config_str "{ @@ -45,7 +52,10 @@ set(__config_str ${__objc_build_dir} \"modules\": [ ${__modules_config} - ] + ], + \"preprocessor_definitions\": { +${opencv_preprocessor_defs} + } } ") #TODO: ocv_update_file("${CONFIG_FILE}" "${__config_str}" ON_CHANGE_REMOVE "${OPENCV_DEPHELPER}/gen_opencv_objc_source") diff --git a/modules/objc/generator/gen_objc.py b/modules/objc/generator/gen_objc.py index 74c370e8bf..2f6f974a02 100755 --- a/modules/objc/generator/gen_objc.py +++ b/modules/objc/generator/gen_objc.py @@ -894,7 +894,8 @@ class ObjectiveCWrapperGenerator(object): namespace = self.classes[cname].namespace if cname in self.classes else "cv" return namespace.replace(".", "::") + "::" - def gen(self, srcfiles, module, output_path, output_objc_path, common_headers, manual_classes): + def gen(self, srcfiles, module, output_path, output_objc_path, + common_headers, manual_classes, preprocessor_definitions=None): self.clear() self.module = module self.objcmodule = make_objcmodule(module) @@ -903,7 +904,10 @@ class ObjectiveCWrapperGenerator(object): extension_signatures = [] # TODO: support UMat versions of declarations (implement UMat-wrapper for Java) - parser = hdr_parser.CppHeaderParser(generate_umat_decls=False) + parser = hdr_parser.CppHeaderParser( + generate_umat_decls=False, + preprocessor_definitions=preprocessor_definitions + ) module_ci = self.add_class( ['class ' + self.Module, '', [], []]) # [ 'class/struct cname', ':bases', [modlist] [props] ] module_ci.header_import = module + '.hpp' @@ -1715,7 +1719,9 @@ if __name__ == "__main__": manual_classes = [x for x in [x[x.rfind('/')+1:-2] for x in [x for x in copied_files if x.endswith('.h')]] if x in type_dict] if len(srcfiles) > 0: - generator.gen(srcfiles, module, dstdir, objc_base_path, common_headers, manual_classes) + generator.gen(srcfiles, module, dstdir, objc_base_path, + common_headers, manual_classes, + config.get("preprocessor_definitions")) else: logging.info("No generated code for module: %s", module) generator.finalize(args.target, objc_base_path, objc_build_dir) diff --git a/modules/python/bindings/CMakeLists.txt b/modules/python/bindings/CMakeLists.txt index 918411864c..c6db97bf28 100644 --- a/modules/python/bindings/CMakeLists.txt +++ b/modules/python/bindings/CMakeLists.txt @@ -74,12 +74,50 @@ set(cv2_generated_files "${OPENCV_PYTHON_SIGNATURES_FILE}" ) -string(REPLACE ";" "\n" opencv_hdrs_ "${opencv_hdrs}") -file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/headers.txt" "${opencv_hdrs_}") + +set(config_json_headers_list "") +foreach(header IN LISTS opencv_hdrs) + if(NOT config_json_headers_list STREQUAL "") + set(config_json_headers_list "${config_json_headers_list},\n\"${header}\"") + else() + set(config_json_headers_list "\"${header}\"") + endif() +endforeach() + +include("${OpenCV_SOURCE_DIR}/cmake/OpenCVBindingsPreprocessorDefinitions.cmake") + +ocv_bindings_generator_populate_preprocessor_definitions( + OPENCV_MODULES_BUILD + opencv_preprocessor_defs +) + +set(__config_str +"{ + \"headers\": [ +${config_json_headers_list} + ], + \"preprocessor_definitions\": { +${opencv_preprocessor_defs} + } +}") + +set(JSON_CONFIG_FILE_PATH "${CMAKE_CURRENT_BINARY_DIR}/gen_python_config.json") +if(EXISTS "${JSON_CONFIG_FILE_PATH}") + file(READ "${JSON_CONFIG_FILE_PATH}" __content) +else() + set(__content "") +endif() +if(NOT "${__content}" STREQUAL "${__config_str}") + file(WRITE "${JSON_CONFIG_FILE_PATH}" "${__config_str}") +endif() +unset(__config_str) + file(GLOB_RECURSE typing_stubs_generation_files "${PYTHON_SOURCE_DIR}/src2/typing_stubs_generation/*.py") add_custom_command( OUTPUT ${cv2_generated_files} - COMMAND "${PYTHON_DEFAULT_EXECUTABLE}" "${PYTHON_SOURCE_DIR}/src2/gen2.py" "${CMAKE_CURRENT_BINARY_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/headers.txt" + COMMAND "${PYTHON_DEFAULT_EXECUTABLE}" "${PYTHON_SOURCE_DIR}/src2/gen2.py" + "--config" "${JSON_CONFIG_FILE_PATH}" + "--output_dir" "${CMAKE_CURRENT_BINARY_DIR}" DEPENDS "${PYTHON_SOURCE_DIR}/src2/gen2.py" "${PYTHON_SOURCE_DIR}/src2/hdr_parser.py" "${typing_stubs_generation_files}" diff --git a/modules/python/src2/gen2.py b/modules/python/src2/gen2.py index af187e5d3f..7d9f75063c 100755 --- a/modules/python/src2/gen2.py +++ b/modules/python/src2/gen2.py @@ -2,6 +2,7 @@ from __future__ import print_function import hdr_parser, sys, re +import json from string import Template from collections import namedtuple from itertools import chain @@ -1108,6 +1109,18 @@ class Namespace(object): class PythonWrapperGenerator(object): + class Config: + def __init__(self, headers, preprocessor_definitions = None): + self.headers = headers + if preprocessor_definitions is None: + preprocessor_definitions = {} + elif not isinstance(preprocessor_definitions, dict): + raise TypeError( + "preprocessor_definitions should rather dictionary or None. " + "Got: {}".format(type(preprocessor_definitions).__name__) + ) + self.preprocessor_definitions = preprocessor_definitions + def __init__(self): self.clear() @@ -1324,13 +1337,16 @@ class PythonWrapperGenerator(object): f.write(buf.getvalue()) def save_json(self, path, name, value): - import json with open(path + "/" + name, "wt") as f: json.dump(value, f) - def gen(self, srcfiles, output_path): + def gen(self, srcfiles, output_path, preprocessor_definitions = None): self.clear() - self.parser = hdr_parser.CppHeaderParser(generate_umat_decls=True, generate_gpumat_decls=True) + self.parser = hdr_parser.CppHeaderParser( + generate_umat_decls=True, + generate_gpumat_decls=True, + preprocessor_definitions=preprocessor_definitions + ) # step 1: scan the headers and build more descriptive maps of classes, consts, functions @@ -1502,12 +1518,36 @@ class PythonWrapperGenerator(object): if __name__ == "__main__": - srcfiles = hdr_parser.opencv_hdr_list - dstdir = "/Users/vp/tmp" - if len(sys.argv) > 1: - dstdir = sys.argv[1] - if len(sys.argv) > 2: - with open(sys.argv[2], 'r') as f: - srcfiles = [l.strip() for l in f.readlines()] + import argparse + import tempfile + + arg_parser = argparse.ArgumentParser( + description="OpenCV Python bindings generator" + ) + arg_parser.add_argument( + "-c", "--config", + dest="config_json_path", + required=False, + help="Generator configuration file in .json format" + "Refer to PythonWrapperGenerator.Config for available " + "configuration keys" + ) + arg_parser.add_argument( + "-o", "--output_dir", + dest="output_dir", + default=tempfile.gettempdir(), + help="Generated bindings output directory" + ) + args = arg_parser.parse_args() + if args.config_json_path is not None: + with open(args.config_json_path, "r") as fh: + config_json = json.load(fh) + config = PythonWrapperGenerator.Config(**config_json) + else: + config = PythonWrapperGenerator.Config( + headers=hdr_parser.opencv_hdr_list + ) + generator = PythonWrapperGenerator() - generator.gen(srcfiles, dstdir) + + generator.gen(config.headers, args.output_dir, config.preprocessor_definitions) diff --git a/modules/python/src2/hdr_parser.py b/modules/python/src2/hdr_parser.py index afcc390dee..b5faa61289 100755 --- a/modules/python/src2/hdr_parser.py +++ b/modules/python/src2/hdr_parser.py @@ -31,11 +31,160 @@ where the list of modifiers is yet another nested list of strings original_return_type is None if the original_return_type is the same as return_value_type """ +def evaluate_conditional_inclusion_directive(directive, preprocessor_definitions): + """Evaluates C++ conditional inclusion directive. + Reference: https://en.cppreference.com/w/cpp/preprocessor/conditional + + Args: + directive(str): input C++ conditional directive. + preprocessor_definitions(dict[str, int]): defined preprocessor identifiers. + + Returns: + bool: True, if directive is evaluated to 1, False otherwise. + + >>> evaluate_conditional_inclusion_directive("#ifdef A", {"A": 0}) + True + + >>> evaluate_conditional_inclusion_directive("#ifdef A", {"B": 0}) + False + + >>> evaluate_conditional_inclusion_directive("#ifndef A", {}) + True + + >>> evaluate_conditional_inclusion_directive("#ifndef A", {"A": 1}) + False + + >>> evaluate_conditional_inclusion_directive("#if 0", {}) + False + + >>> evaluate_conditional_inclusion_directive("#if 1", {}) + True + + >>> evaluate_conditional_inclusion_directive("#if VAR", {"VAR": 0}) + False + + >>> evaluate_conditional_inclusion_directive("#if VAR ", {"VAR": 1}) + True + + >>> evaluate_conditional_inclusion_directive("#if defined(VAR)", {"VAR": 0}) + True + + >>> evaluate_conditional_inclusion_directive("#if !defined(VAR)", {"VAR": 0}) + False + + >>> evaluate_conditional_inclusion_directive("#if defined(VAR_1)", {"VAR_2": 0}) + False + + >>> evaluate_conditional_inclusion_directive( + ... "#if defined(VAR) && VAR", {"VAR": 0} + ... ) + False + + >>> evaluate_conditional_inclusion_directive( + ... "#if VAR_1 || VAR_2", {"VAR_1": 1, "VAR_2": 0} + ... ) + True + + >>> evaluate_conditional_inclusion_directive( + ... "#if defined VAR && defined (VAR)", {"VAR": 1} + ... ) + True + + >>> evaluate_conditional_inclusion_directive( + ... "#if strangedefinedvar", {} + ... ) + Traceback (most recent call last): + ... + ValueError: Failed to evaluate '#if strangedefinedvar' directive, stripped down to 'strangedefinedvar' + """ + OPERATORS = { "!": "not ", "&&": "and", "&": "and", "||": "or", "|": "or" } + + input_directive = directive + + # Ignore all directives if they contain __cplusplus check + if "__cplusplus" in directive: + return True + + directive = directive.strip() + if directive.startswith("#ifdef "): + var = directive[len("#ifdef "):].strip() + return var in preprocessor_definitions + if directive.startswith("#ifndef "): + var = directive[len("#ifndef "):].strip() + return var not in preprocessor_definitions + + if directive.startswith("#if "): + directive = directive[len("#if "):].strip() + elif directive.startswith("#elif "): + directive = directive[len("#elif "):].strip() + else: + raise ValueError("{} is not known conditional directive".format(directive)) + + if directive.isdigit(): + return int(directive) != 0 + + if directive in preprocessor_definitions: + return bool(preprocessor_definitions[directive]) + + # Converting all `defined` directives to their boolean representations + # they have 2 forms: `defined identifier` and `defined(identifier)` + directive = re.sub( + r"\bdefined\s*(\w+|\(\w+\))", + lambda m: "True" if m.group(1).strip("() ") in preprocessor_definitions else "False", + directive + ) + + for src_op, dst_op in OPERATORS.items(): + directive = directive.replace(src_op, dst_op) + + try: + if sys.version_info >= (3, 13): + eval_directive = eval(directive, + globals={"__builtins__": {}}, + locals=preprocessor_definitions) + else: + eval_directive = eval(directive, + {"__builtins__": {}}, + preprocessor_definitions) + except Exception as e: + raise ValueError( + "Failed to evaluate '{}' directive, stripped down to '{}'".format( + input_directive, directive + ) + ) from e + + if not isinstance(eval_directive, (bool, int)): + raise TypeError( + "'{}' directive is evaluated to unexpected type: {}".format( + input_directive, type(eval_directive).__name__ + ) + ) + if isinstance(eval_directive, bool): + return eval_directive + + return eval_directive != 0 + + class CppHeaderParser(object): - def __init__(self, generate_umat_decls=False, generate_gpumat_decls=False): + def __init__(self, generate_umat_decls = False, generate_gpumat_decls = False, + preprocessor_definitions = None): self._generate_umat_decls = generate_umat_decls self._generate_gpumat_decls = generate_gpumat_decls + if preprocessor_definitions is None: + preprocessor_definitions = {} + elif not isinstance(preprocessor_definitions, dict): + raise TypeError( + "preprocessor_definitions should rather dictionary or None. " + "Got: {}".format(type(preprocessor_definitions).__name__) + ) + self.preprocessor_definitions = preprocessor_definitions + if "__OPENCV_BUILD" not in self.preprocessor_definitions: + self.preprocessor_definitions["__OPENCV_BUILD"] = 0 + if "OPENCV_BINDING_PARSER" not in self.preprocessor_definitions: + self.preprocessor_definitions["OPENCV_BINDING_PARSER"] = 1 + if "OPENCV_BINDINGS_PARSER" not in self.preprocessor_definitions: + self.preprocessor_definitions["OPENCV_BINDINGS_PARSER"] = 1 self.BLOCK_TYPE = 0 self.BLOCK_NAME = 1 @@ -839,9 +988,8 @@ class CppHeaderParser(object): """ self.hname = hname decls = [] - f = io.open(hname, 'rt', encoding='utf-8') - linelist = list(f.readlines()) - f.close() + with io.open(hname, 'rt', encoding='utf-8') as f: + linelist = list(f.readlines()) # states: SCAN = 0 # outside of a comment or preprocessor directive @@ -859,7 +1007,6 @@ class CppHeaderParser(object): self.wrap_mode = wmode depth_if_0 = 0 - for l0 in linelist: self.lineno += 1 #print(state, self.lineno, l0) @@ -886,22 +1033,38 @@ class CppHeaderParser(object): continue state = SCAN l = re.sub(r'//(.+)?', '', l).strip() # drop // comment - if l in [ - '#if 0', - '#if defined(__OPENCV_BUILD)', '#ifdef __OPENCV_BUILD', - '#if !defined(OPENCV_BINDING_PARSER)', '#ifndef OPENCV_BINDING_PARSER', - ]: + if l.startswith("#if") or l.startswith("#elif"): + if not evaluate_conditional_inclusion_directive( + l, self.preprocessor_definitions + ): + # Condition evaluated to false + state = DIRECTIVE_IF_0 + depth_if_0 = 1 + elif l.startswith("#else"): + # else in state == DIRECTIVE may occur only if previous + # conditional inclusion directive was evaluated to True state = DIRECTIVE_IF_0 depth_if_0 = 1 continue if state == DIRECTIVE_IF_0: - if l.startswith('#'): - l = l[1:].strip() - if l.startswith("if"): + if l.startswith("#"): + if l.startswith("#if"): depth_if_0 += 1 continue - if l.startswith("endif"): + elif l.startswith("#else") and depth_if_0 == 1: + depth_if_0 = 0 + state = SCAN + elif l.startswith("#elif") and depth_if_0 == 1: + if evaluate_conditional_inclusion_directive( + l, self.preprocessor_definitions + ): + depth_if_0 = 0 + state = SCAN + else: + depth_if_0 += 1 + continue + elif l.startswith("#endif"): depth_if_0 -= 1 if depth_if_0 == 0: state = SCAN @@ -1075,6 +1238,9 @@ class CppHeaderParser(object): print() if __name__ == '__main__': + import doctest + doctest.testmod() + parser = CppHeaderParser(generate_umat_decls=True, generate_gpumat_decls=True) decls = [] for hname in opencv_hdr_list: