diff --git a/CMakeLists.txt b/CMakeLists.txt index 96090611d1..508789dc4b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -143,7 +143,7 @@ execute_process(COMMAND "${Python3_EXECUTABLE}" "-c" "import binding_generator; ) add_custom_command(OUTPUT ${GENERATED_FILES_LIST} - COMMAND "${Python3_EXECUTABLE}" "-c" "import binding_generator; binding_generator.generate_bindings(\"${GODOT_GDEXTENSION_API_FILE}\", \"${GENERATE_BINDING_PARAMETERS}\", \"${BITS}\", \"${FLOAT_PRECISION}\", \"${CMAKE_CURRENT_BINARY_DIR}\")" + COMMAND "${Python3_EXECUTABLE}" "-c" "import binding_generator; binding_generator.generate_bindings(\"${GODOT_GDEXTENSION_API_FILE}\", \"${GENERATE_BINDING_PARAMETERS}\", \"${BITS}\", \"${FLOAT_PRECISION}\", \"${CMAKE_CURRENT_BINARY_DIR}\", \"${GODOT_REPO}\")" VERBATIM WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} MAIN_DEPENDENCY ${GODOT_GDEXTENSION_API_FILE} diff --git a/README.md b/README.md index 005c8e7239..f923ceed8d 100644 --- a/README.md +++ b/README.md @@ -147,3 +147,64 @@ generic reusable template. Or checkout the code for the [Summator example](https://github.com/paddy-exe/GDExtensionSummator) as shown in the [official documentation](https://docs.godotengine.org/en/latest/tutorials/scripting/gdextension/gdextension_cpp_example.html). + +## Godot and Godot Cpp Compatibility + +If you intend to target both building as a GDExtension and as a module using godot repo, you can generate compatibility includes that will target either GDExtension or module, based on the GODOT_MODULE_COMPAT define. + +If you want such headers, when running the build command, `scons`, pass the `godot_repo` param with the path to the godot repository. Eg. if you have the godot repository cloned at path `../godot`, then do: + +```sh +scons godot_repo="../godot" +``` + +This will generate something like this: +``` +gen/include/godot_cpp/.. +gen/include/godot_compat/.. +``` + +Now, all you need to do is when writing your addon/module, replace includes like these: + +```cpp +#include +``` + +with + +```cpp +#include +``` + +Inside, this file will have code for both godot and godot-cpp: + +```cpp +#ifdef GODOT_MODULE_COMPAT +#include +#else +#include +#endif +``` + +### Manually generate mapping files + +The mappings can be manually generated by running the `compat_generator.py` script. + +Example of how to run `compat_generator.py`: + +```sh +git clone godotengine/godot +python compat_generator.py godot +``` + +The first argument of `compat_generator.py` is the folder where the repo is (can be godot or godot-cpp repo). If this folder is not given, the current directory is assumed. The output of this is either `output_header_mapping_godot.json` or `output_header_mapping_godot_cpp.json` + +### Manually match the mapping files + +If you want to manually match the godot mapping file with the godot-cpp one, you can do that by running: + +```sh +python header_matcher.py +``` + +This will generate the `header_matches.json` file with matches from godot and godot_cpp repo. diff --git a/binding_generator.py b/binding_generator.py index 75e56fde1c..c2e1013601 100644 --- a/binding_generator.py +++ b/binding_generator.py @@ -4,6 +4,8 @@ import re import shutil from pathlib import Path +from compat_generator import map_header_files +from header_matcher import match_headers def generate_mod_version(argcount, const=False, returns=False): @@ -378,11 +380,14 @@ def scons_generate_bindings(target, source, env): "32" if "32" in env["arch"] else "64", env["precision"], env["godot_cpp_gen_dir"], + env["godot_repo"], ) return None -def generate_bindings(api_filepath, use_template_get_node, bits="64", precision="single", output_dir="."): +def generate_bindings( + api_filepath, use_template_get_node, bits="64", precision="single", output_dir=".", godot_repo="" +): api = None target_dir = Path(output_dir) / "gen" @@ -402,6 +407,8 @@ def generate_bindings(api_filepath, use_template_get_node, bits="64", precision= generate_builtin_bindings(api, target_dir, real_t + "_" + bits) generate_engine_classes_bindings(api, target_dir, use_template_get_node) generate_utility_functions(api, target_dir) + if godot_repo != "": + generate_compat_includes(godot_repo, output_dir, target_dir) CLASS_ALIASES = { @@ -1545,6 +1552,54 @@ def generate_engine_classes_bindings(api, output_dir, use_template_get_node): header_file.write("\n".join(result)) +def generate_compat_includes(godot_repo: Path, output_dir: Path, target_dir: Path): + # Uncomment this to change include files + # target_dir = output_dir + file_types_mapping_godot_cpp_gen = map_header_files(target_dir) + file_types_mapping_godot = map_header_files(godot_repo) + # Match the headers + file_types_mapping = match_headers(file_types_mapping_godot_cpp_gen, file_types_mapping_godot) + for file_godot_cpp_name, file_godot_names in file_types_mapping.items(): + result = [] + with (Path(target_dir) / Path(file_godot_cpp_name)).open("r", encoding="utf-8") as header_file: + content = header_file.readlines() + # Find the last line (#endif guard) + last_endif_idx = len(content) - 1 + # Look for the marker for the header guard (usually #define) + marker_pos = next((i for i, line in enumerate(content) if line.startswith("#define")), -1) + + if marker_pos != -1: + # Add content before the last #endif + before_marker = content[: marker_pos + 1] # Include up to the marker line + after_marker = content[marker_pos + 1 : last_endif_idx] # Content excluding the final #endif + + result.extend(before_marker) # Append original content up to marker + result.append("\n") # Blank line for separation + + # Insert the #ifdef block + result.append("#ifdef GODOT_MODULE_COMPAT\n") + if len(file_godot_names) == 0: + print("No header found for", file_godot_cpp_name) + for file_godot_name in file_godot_names: + result.append(f"#include <{Path(file_godot_name).as_posix()}>\n") + result.append("#else\n") + + for line in after_marker: + if line.strip() not in {"namespace godot {", "} // namespace godot"}: + result.append(line) + + # Add the namespace and endif + result.append("#endif\n") + + # Finally, append the original final #endif line + result.append(content[last_endif_idx].strip()) + + else: + print(f"Marker not found in {file_godot_cpp_name}") + with (Path(target_dir) / Path(file_godot_cpp_name)).open("w+", encoding="utf-8") as header_file: + header_file.write("".join(result)) + + def generate_engine_class_header(class_api, used_classes, fully_used_classes, use_template_get_node): global singletons result = [] diff --git a/compat_generator.py b/compat_generator.py new file mode 100644 index 0000000000..ae94a95b2a --- /dev/null +++ b/compat_generator.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python + +import json +import os +import re +import sys + + +def parse_header_file(file_path): + types = {"classes": [], "structs": [], "defines": [], "enums": []} + + with open(file_path, "r", encoding="utf-8") as file: + content = file.read() + + # Regular expressions to match different types + struct_pattern = r"struct.*\s(\S+)\s*\{" + type_pattern = r"typedef.*\s(\S+)\s*\;" + enum_pattern = r"enum.*\s(\S+)\s*\{" + class_pattern = r"class\s+[\w\s]*?([a-zA-Z_]\w*)\s*[:{]" + define_pattern = r"#define\s+([a-zA-Z_]\w*)" + + # Extract classes + types["classes"] += re.findall(class_pattern, content) + + # Extract structs + struct_names = re.findall(struct_pattern, content) + types["structs"].extend(struct_names) + type_names = re.findall(type_pattern, content) + types["structs"].extend(type_names) + enum_names = re.findall(enum_pattern, content) + types["enums"].extend(enum_names) + + # Extract defines + define_matches = re.findall(define_pattern, content) + types["defines"] += define_matches + + # Debug the case where no classes or structs are found + # if len(types["classes"]) == 0 and len(types["structs"]) == 0 and len(types["defines"]) == 0: + # print(f"{file_path} missing things") + return types + + +def map_header_files(directory): + file_types_mapping = {} + for root, dirs, files in os.walk(directory): + if "thirdparty" in dirs: + dirs.remove("thirdparty") + if "tests" in dirs: + dirs.remove("tests") + if "test" in dirs: + dirs.remove("test") + if "misc" in dirs: + dirs.remove("misc") + if "gdextension" in dirs: + dirs.remove("gdextension") + for file in files: + if file.endswith(".h") or file.endswith(".hpp"): + relative_path = os.path.relpath(root, directory) + file_path = os.path.join(root, file) + file_types_mapping[f"{relative_path}/{file}"] = parse_header_file(file_path) + + return file_types_mapping + + +if __name__ == "__main__": + # Get current directory for godot-cpp + current_directory = os.path.join(os.getcwd(), "") + mapping_name = "" + if len(sys.argv) > 1: + mapping_name = "_godot" + current_directory = os.path.join(os.getcwd(), sys.argv[1]) + + file_types_mapping = map_header_files(current_directory) + with open(f"output_header_mapping{mapping_name}.json", "w") as json_file: + json.dump(file_types_mapping, json_file, indent=4) diff --git a/header_matcher.py b/header_matcher.py new file mode 100644 index 0000000000..8dbdd5f530 --- /dev/null +++ b/header_matcher.py @@ -0,0 +1,33 @@ +import json +import os + +from compat_generator import map_header_files + + +def match_headers(mapping1, mapping2): + matches = {} + for header_file, data1 in mapping1.items(): + for header_file2, data2 in mapping2.items(): + # Check if classes/defines/structs in header_file1 are present in header_file2 + if header_file not in matches: + matches[header_file] = [] + if ( + any(class_name in data2["classes"] for class_name in data1["classes"]) + or any(define_name in data2["defines"] for define_name in data1["defines"]) + or any(define_name in data2["structs"] for define_name in data1["structs"]) + ): # or + # any(define_name in data2["enums"] for define_name in data1["enums"])): + matches[header_file].append(header_file2) + return matches + + +if __name__ == "__main__": + # Load the two header mappings + with open("output_header_mapping_godot.json", "r") as file: + mapping_godot = json.load(file) + file_types_mapping_godot_cpp_gen = map_header_files(os.path.join(os.getcwd(), "gen", "include")) + matches = match_headers(file_types_mapping_godot_cpp_gen, mapping_godot) + + # Optionally, you can save the matches to a file + with open("header_matches.json", "w") as outfile: + json.dump(matches, outfile, indent=4) diff --git a/tools/godotcpp.py b/tools/godotcpp.py index 9ceac02b12..a43fd92a8f 100644 --- a/tools/godotcpp.py +++ b/tools/godotcpp.py @@ -228,6 +228,14 @@ def options(opts, env): validator=validate_file, ) ) + opts.Add( + PathVariable( + key="godot_repo", + help="Path to a custom directory containing Godot repository. Used to generate godot_compat bindings.", + default=env.get("godot_repo", ""), + validator=validate_dir, + ) + ) opts.Add( BoolVariable( key="generate_bindings",