diff --git a/README.md b/README.md index 4a0dcea0..c36e8925 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,15 @@ > **Warning** > -> This repository's `master` branch is only usable with the latest version of -> Godot's ([GDExtension](https://godotengine.org/article/introducing-gd-extensions)) -> API (Godot 4.1 and later). +> This repository's `master` branch is only usable with +> [GDExtension](https://godotengine.org/article/introducing-gd-extensions) +> from Godot's `master` branch. > -> For users of Godot 4.0.x, switch to the [`4.0`](https://github.com/godotengine/godot-cpp/tree/4.0) branch. +> For users of stable branches, switch to the branch matching your target Godot version: +> - [`4.0`](https://github.com/godotengine/godot-cpp/tree/4.0) +> - [`4.1`](https://github.com/godotengine/godot-cpp/tree/4.1) +> +> Or check out the Git tag matching your Godot version (e.g. `godot-4.1.1-stable`). > > For GDNative users (Godot 3.x), switch to the [`3.x`](https://github.com/godotengine/godot-cpp/tree/3.x) > or the [`3.5`](https://github.com/godotengine/godot-cpp/tree/3.5) branch. @@ -52,9 +56,10 @@ first-party `godot-cpp` extension. Some compatibility breakage is to be expected as GDExtension and `godot-cpp` get more used, documented, and critical issues get resolved. See the -[issue tracker](https://github.com/godotengine/godot/issues) for a list of known -issues, and be sure to provide feedback on issues and PRs which affect your use -of this extension. +[Godot issue tracker](https://github.com/godotengine/godot/issues?q=is%3Aissue+is%3Aopen+label%3Atopic%3Agdextension) +and the [godot-cpp issue tracker](https://github.com/godotengine/godot/issues) +for a list of known issues, and be sure to provide feedback on issues and PRs +which affect your use of this extension. ## Contributing @@ -76,22 +81,22 @@ just like before. To use the shared lib in your Godot project you'll need a `.gdextension` file, which replaces what was the `.gdnlib` before. -Follow [the example](test/demo/example.gdextension): +See [example.gdextension](test/project/example.gdextension) used in the test project: ```ini [configuration] entry_symbol = "example_library_init" -compatibility_minimum = 4.1 +compatibility_minimum = "4.1" [libraries] -macos.debug = "bin/libgdexample.macos.debug.framework" -macos.release = "bin/libgdexample.macos.release.framework" -windows.debug.x86_64 = "bin/libgdexample.windows.debug.x86_64.dll" -windows.release.x86_64 = "bin/libgdexample.windows.release.x86_64.dll" -linux.debug.x86_64 = "bin/libgdexample.linux.debug.x86_64.so" -linux.release.x86_64 = "bin/libgdexample.linux.release.x86_64.so" +macos.debug = "res://bin/libgdexample.macos.debug.framework" +macos.release = "res://bin/libgdexample.macos.release.framework" +windows.debug.x86_64 = "res://bin/libgdexample.windows.debug.x86_64.dll" +windows.release.x86_64 = "res://bin/libgdexample.windows.release.x86_64.dll" +linux.debug.x86_64 = "res://bin/libgdexample.linux.debug.x86_64.so" +linux.release.x86_64 = "res://bin/libgdexample.linux.release.x86_64.so" # Repeat for other architectures to support arm64, rv64, etc. ``` @@ -129,6 +134,10 @@ void initialize_example_module(ModuleInitializationLevel p_level) { Any node and resource you register will be available in the corresponding `Create...` dialog. Any class will be available to scripting as well. -## Included example +## Examples and templates -Check the project in the `test` folder for an example on how to use and register different things. +See the [godot-cpp-template](https://github.com/godotengine/godot-cpp-template) project for a +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). diff --git a/SConstruct b/SConstruct index b028676f..7c652f8f 100644 --- a/SConstruct +++ b/SConstruct @@ -7,55 +7,10 @@ import subprocess from binding_generator import scons_generate_bindings, scons_emit_files from SCons.Errors import UserError + EnsureSConsVersion(4, 0) -def add_sources(sources, dir, extension): - for f in os.listdir(dir): - if f.endswith("." + extension): - sources.append(dir + "/" + f) - - -def normalize_path(val): - return val if os.path.isabs(val) else os.path.join(env.Dir("#").abspath, val) - - -def validate_file(key, val, env): - if not os.path.isfile(normalize_path(val)): - raise UserError("'%s' is not a file: %s" % (key, val)) - - -def validate_dir(key, val, env): - if not os.path.isdir(normalize_path(val)): - raise UserError("'%s' is not a directory: %s" % (key, val)) - - -def validate_parent_dir(key, val, env): - if not os.path.isdir(normalize_path(os.path.dirname(val))): - raise UserError("'%s' is not a directory: %s" % (key, os.path.dirname(val))) - - -def get_gdextension_dir(env): - return normalize_path(env.get("gdextension_dir", env.Dir("gdextension").abspath)) - - -def get_api_file(env): - return normalize_path(env.get("custom_api_file", os.path.join(get_gdextension_dir(env), "extension_api.json"))) - - -# Try to detect the host platform automatically. -# This is used if no `platform` argument is passed -if sys.platform.startswith("linux"): - default_platform = "linux" -elif sys.platform == "darwin": - default_platform = "macos" -elif sys.platform == "win32" or sys.platform == "msys": - default_platform = "windows" -elif ARGUMENTS.get("platform", ""): - default_platform = ARGUMENTS.get("platform") -else: - raise ValueError("Could not detect platform automatically, please specify with platform=") - try: Import("env") except: @@ -65,24 +20,6 @@ except: env.PrependENVPath("PATH", os.getenv("PATH")) -# Default num_jobs to local cpu count if not user specified. -# SCons has a peculiarity where user-specified options won't be overridden -# by SetOption, so we can rely on this to know if we should use our default. -initial_num_jobs = env.GetOption("num_jobs") -altered_num_jobs = initial_num_jobs + 1 -env.SetOption("num_jobs", altered_num_jobs) -if env.GetOption("num_jobs") == altered_num_jobs: - cpu_count = os.cpu_count() - if cpu_count is None: - print("Couldn't auto-detect CPU count to configure build parallelism. Specify it with the -j argument.") - else: - safer_cpu_count = cpu_count if cpu_count <= 4 else cpu_count - 1 - print( - "Auto-detected %d CPU cores available for build parallelism. Using %d cores by default. You can override it with the -j argument." - % (cpu_count, safer_cpu_count) - ) - env.SetOption("num_jobs", safer_cpu_count) - # Custom options and profile flags. customs = ["custom.py"] profile = ARGUMENTS.get("profile", "") @@ -92,160 +29,12 @@ if profile: elif os.path.isfile(profile + ".py"): customs.append(profile + ".py") opts = Variables(customs, ARGUMENTS) - -platforms = ("linux", "macos", "windows", "android", "ios", "javascript") -opts.Add( - EnumVariable( - key="platform", - help="Target platform", - default=env.get("platform", default_platform), - allowed_values=platforms, - ignorecase=2, - ) -) - -# Editor and template_debug are compatible (i.e. you can use the same binary for Godot editor builds and Godot debug templates). -# Godot release templates are only compatible with "template_release" builds. -# For this reason, we default to template_debug builds, unlike Godot which defaults to editor builds. -opts.Add( - EnumVariable( - key="target", - help="Compilation target", - default=env.get("target", "template_debug"), - allowed_values=("editor", "template_release", "template_debug"), - ) -) -opts.Add( - PathVariable( - key="gdextension_dir", - help="Path to a custom directory containing GDExtension interface header and API JSON file", - default=env.get("gdextension_dir", None), - validator=validate_dir, - ) -) -opts.Add( - PathVariable( - key="custom_api_file", - help="Path to a custom GDExtension API JSON file (takes precedence over `gdextension_dir`)", - default=env.get("custom_api_file", None), - validator=validate_file, - ) -) -opts.Add( - BoolVariable( - key="generate_bindings", - help="Force GDExtension API bindings generation. Auto-detected by default.", - default=env.get("generate_bindings", False), - ) -) -opts.Add( - BoolVariable( - key="generate_template_get_node", - help="Generate a template version of the Node class's get_node.", - default=env.get("generate_template_get_node", True), - ) -) - -opts.Add(BoolVariable(key="build_library", help="Build the godot-cpp library.", default=env.get("build_library", True))) -opts.Add( - EnumVariable( - key="precision", - help="Set the floating-point precision level", - default=env.get("precision", "single"), - allowed_values=("single", "double"), - ) -) - -# compiledb -opts.Add( - BoolVariable( - key="compiledb", - help="Generate compilation DB (`compile_commands.json`) for external tools", - default=env.get("compiledb", False), - ) -) -opts.Add( - PathVariable( - key="compiledb_file", - help="Path to a custom `compile_commands.json` file", - default=env.get("compiledb_file", "compile_commands.json"), - validator=validate_parent_dir, - ) -) - -# Add platform options -tools = {} -for pl in platforms: - tool = Tool(pl, toolpath=["tools"]) - if hasattr(tool, "options"): - tool.options(opts) - tools[pl] = tool - -# CPU architecture options. -architecture_array = ["", "universal", "x86_32", "x86_64", "arm32", "arm64", "rv64", "ppc32", "ppc64", "wasm32"] -architecture_aliases = { - "x64": "x86_64", - "amd64": "x86_64", - "armv7": "arm32", - "armv8": "arm64", - "arm64v8": "arm64", - "aarch64": "arm64", - "rv": "rv64", - "riscv": "rv64", - "riscv64": "rv64", - "ppcle": "ppc32", - "ppc": "ppc32", - "ppc64le": "ppc64", -} -opts.Add( - EnumVariable( - key="arch", - help="CPU architecture", - default=env.get("arch", ""), - allowed_values=architecture_array, - map=architecture_aliases, - ) -) - -# Targets flags tool (optimizations, debug symbols) -target_tool = Tool("targets", toolpath=["tools"]) -target_tool.options(opts) - +cpp_tool = Tool("godotcpp", toolpath=["tools"]) +cpp_tool.options(opts, env) opts.Update(env) + Help(opts.GenerateHelpText(env)) -# Process CPU architecture argument. -if env["arch"] == "": - # No architecture specified. Default to arm64 if building for Android, - # universal if building for macOS or iOS, wasm32 if building for web, - # otherwise default to the host architecture. - if env["platform"] in ["macos", "ios"]: - env["arch"] = "universal" - elif env["platform"] == "android": - env["arch"] = "arm64" - elif env["platform"] == "javascript": - env["arch"] = "wasm32" - else: - host_machine = platform.machine().lower() - if host_machine in architecture_array: - env["arch"] = host_machine - elif host_machine in architecture_aliases.keys(): - env["arch"] = architecture_aliases[host_machine] - elif "86" in host_machine: - # Catches x86, i386, i486, i586, i686, etc. - env["arch"] = "x86_32" - else: - print("Unsupported CPU architecture: " + host_machine) - Exit() - -tool = Tool(env["platform"], toolpath=["tools"]) - -if tool is None or not tool.exists(env): - raise ValueError("Required toolchain not found for platform " + env["platform"]) - -tool.generate(env) -target_tool.generate(env) - # Detect and print a warning listing unknown SCons variables to ease troubleshooting. unknown = opts.UnknownVariables() if unknown: @@ -253,71 +42,12 @@ if unknown: for item in unknown.items(): print(" " + item[0] + "=" + item[1]) -print("Building for architecture " + env["arch"] + " on platform " + env["platform"]) - -# Require C++17 -if env.get("is_msvc", False): - env.Append(CXXFLAGS=["/std:c++17"]) -else: - env.Append(CXXFLAGS=["-std=c++17"]) - -if env["precision"] == "double": - env.Append(CPPDEFINES=["REAL_T_IS_DOUBLE"]) - -# compile_commands.json -if env.get("compiledb", False): - env.Tool("compilation_db") - env.Alias("compiledb", env.CompilationDatabase(normalize_path(env["compiledb_file"]))) - -# Generate bindings -env.Append(BUILDERS={"GenerateBindings": Builder(action=scons_generate_bindings, emitter=scons_emit_files)}) - -bindings = env.GenerateBindings( - env.Dir("."), - [get_api_file(env), os.path.join(get_gdextension_dir(env), "gdextension_interface.h"), "binding_generator.py"], -) - scons_cache_path = os.environ.get("SCONS_CACHE") if scons_cache_path is not None: CacheDir(scons_cache_path) Decider("MD5") -# Forces bindings regeneration. -if env["generate_bindings"]: - AlwaysBuild(bindings) - NoCache(bindings) +cpp_tool.generate(env) +library = env.GodotCPP() -# Includes -env.Append(CPPPATH=[[env.Dir(d) for d in [get_gdextension_dir(env), "include", os.path.join("gen", "include")]]]) - -# Sources to compile -sources = [] -add_sources(sources, "src", "cpp") -add_sources(sources, "src/classes", "cpp") -add_sources(sources, "src/core", "cpp") -add_sources(sources, "src/variant", "cpp") -sources.extend([f for f in bindings if str(f).endswith(".cpp")]) - -suffix = ".{}.{}".format(env["platform"], env["target"]) -if env.dev_build: - suffix += ".dev" -if env["precision"] == "double": - suffix += ".double" -suffix += "." + env["arch"] -if env["ios_simulator"]: - suffix += ".simulator" - -# Expose it when included from another project -env["suffix"] = suffix - -library = None -env["OBJSUFFIX"] = suffix + env["OBJSUFFIX"] -library_name = "libgodot-cpp{}{}".format(suffix, env["LIBSUFFIX"]) - -if env["build_library"]: - library = env.StaticLibrary(target=env.File("bin/%s" % library_name), source=sources) - Default(library) - -env.Append(LIBPATH=[env.Dir("bin")]) -env.Append(LIBS=library_name) Return("env") diff --git a/binding_generator.py b/binding_generator.py index 6e47e7bc..49664d14 100644 --- a/binding_generator.py +++ b/binding_generator.py @@ -123,6 +123,7 @@ def get_file_list(api_filepath, output_dir, headers=False, sources=False): include_gen_folder / "variant" / "variant_size.hpp", include_gen_folder / "classes" / "global_constants.hpp", include_gen_folder / "classes" / "global_constants_binds.hpp", + include_gen_folder / "core" / "version.hpp", ]: files.append(str(path.as_posix())) if sources: @@ -173,6 +174,7 @@ def generate_bindings(api_filepath, use_template_get_node, bits="64", precision= print("Built-in type config: " + real_t + "_" + bits) generate_global_constants(api, target_dir) + generate_version_header(api, target_dir) generate_global_constant_binds(api, target_dir) generate_builtin_bindings(api, target_dir, real_t + "_" + bits) generate_engine_classes_bindings(api, target_dir, use_template_get_node) @@ -1666,6 +1668,35 @@ def generate_global_constants(api, output_dir): header_file.write("\n".join(header)) +def generate_version_header(api, output_dir): + header = [] + header_filename = "version.hpp" + add_header(header_filename, header) + + include_gen_folder = Path(output_dir) / "include" / "godot_cpp" / "core" + include_gen_folder.mkdir(parents=True, exist_ok=True) + + header_file_path = include_gen_folder / header_filename + + header_guard = "GODOT_CPP_VERSION_HPP" + header.append(f"#ifndef {header_guard}") + header.append(f"#define {header_guard}") + header.append("") + + header.append(f"#define GODOT_VERSION_MAJOR {api['header']['version_major']}") + header.append(f"#define GODOT_VERSION_MINOR {api['header']['version_minor']}") + header.append(f"#define GODOT_VERSION_PATCH {api['header']['version_patch']}") + header.append(f"#define GODOT_VERSION_STATUS \"{api['header']['version_status']}\"") + header.append(f"#define GODOT_VERSION_BUILD \"{api['header']['version_build']}\"") + + header.append("") + header.append(f"#endif // {header_guard}") + header.append("") + + with header_file_path.open("w+", encoding="utf-8") as header_file: + header_file.write("\n".join(header)) + + def generate_global_constant_binds(api, output_dir): include_gen_folder = Path(output_dir) / "include" / "godot_cpp" / "classes" source_gen_folder = Path(output_dir) / "src" / "classes" diff --git a/include/godot_cpp/templates/cowdata.hpp b/include/godot_cpp/templates/cowdata.hpp index 18320d80..d3ea982b 100644 --- a/include/godot_cpp/templates/cowdata.hpp +++ b/include/godot_cpp/templates/cowdata.hpp @@ -39,6 +39,7 @@ #include #include +#include namespace godot { @@ -210,9 +211,9 @@ void CowData::_unref(void *p_data) { if (refc->decrement() > 0) { return; // still in use } - // clean up - if (!__has_trivial_destructor(T)) { + // clean up + if (!std::is_trivially_destructible::value) { uint32_t *count = _get_size(); T *data = (T *)(count + 1); @@ -247,7 +248,7 @@ uint32_t CowData::_copy_on_write() { T *_data = (T *)(mem_new); // initialize new elements - if (__has_trivial_copy(T)) { + if (std::is_trivially_copyable::value) { memcpy(mem_new, _ptr, current_size * sizeof(T)); } else { @@ -310,7 +311,7 @@ Error CowData::resize(int p_size) { // construct the newly created elements - if (!__has_trivial_constructor(T)) { + if (!std::is_trivially_constructible::value) { T *elems = _get_data(); for (int i = *_get_size(); i < p_size; i++) { @@ -321,7 +322,7 @@ Error CowData::resize(int p_size) { *_get_size() = p_size; } else if (p_size < current_size) { - if (!__has_trivial_destructor(T)) { + if (!std::is_trivially_destructible::value) { // deinitialize no longer needed elements for (uint32_t i = p_size; i < *_get_size(); i++) { T *t = &_get_data()[i]; diff --git a/include/godot_cpp/variant/vector3.hpp b/include/godot_cpp/variant/vector3.hpp index 2ccd6cf3..a8d96ed0 100644 --- a/include/godot_cpp/variant/vector3.hpp +++ b/include/godot_cpp/variant/vector3.hpp @@ -78,6 +78,14 @@ struct _NO_DISCARD_ Vector3 { return x < y ? (y < z ? Vector3::AXIS_Z : Vector3::AXIS_Y) : (x < z ? Vector3::AXIS_Z : Vector3::AXIS_X); } + Vector3 min(const Vector3 &p_vector3) const { + return Vector3(MIN(x, p_vector3.x), MIN(y, p_vector3.y), MIN(z, p_vector3.z)); + } + + Vector3 max(const Vector3 &p_vector3) const { + return Vector3(MAX(x, p_vector3.x), MAX(y, p_vector3.y), MAX(z, p_vector3.z)); + } + _FORCE_INLINE_ real_t length() const; _FORCE_INLINE_ real_t length_squared() const; diff --git a/include/godot_cpp/variant/vector3i.hpp b/include/godot_cpp/variant/vector3i.hpp index c8f1fe5b..c13bd089 100644 --- a/include/godot_cpp/variant/vector3i.hpp +++ b/include/godot_cpp/variant/vector3i.hpp @@ -71,6 +71,14 @@ struct _NO_DISCARD_ Vector3i { Vector3i::Axis min_axis_index() const; Vector3i::Axis max_axis_index() const; + Vector3i min(const Vector3i &p_vector3i) const { + return Vector3i(MIN(x, p_vector3i.x), MIN(y, p_vector3i.y), MIN(z, p_vector3i.z)); + } + + Vector3i max(const Vector3i &p_vector3i) const { + return Vector3i(MAX(x, p_vector3i.x), MAX(y, p_vector3i.y), MAX(z, p_vector3i.z)); + } + _FORCE_INLINE_ int64_t length_squared() const; _FORCE_INLINE_ double length() const; diff --git a/include/godot_cpp/variant/vector4.hpp b/include/godot_cpp/variant/vector4.hpp index ba3da686..26c57c35 100644 --- a/include/godot_cpp/variant/vector4.hpp +++ b/include/godot_cpp/variant/vector4.hpp @@ -70,6 +70,14 @@ struct _NO_DISCARD_ Vector4 { Vector4::Axis min_axis_index() const; Vector4::Axis max_axis_index() const; + Vector4 min(const Vector4 &p_vector4) const { + return Vector4(MIN(x, p_vector4.x), MIN(y, p_vector4.y), MIN(z, p_vector4.z), MIN(w, p_vector4.w)); + } + + Vector4 max(const Vector4 &p_vector4) const { + return Vector4(MAX(x, p_vector4.x), MAX(y, p_vector4.y), MAX(z, p_vector4.z), MAX(w, p_vector4.w)); + } + _FORCE_INLINE_ real_t length_squared() const; bool is_equal_approx(const Vector4 &p_vec4) const; bool is_zero_approx() const; diff --git a/include/godot_cpp/variant/vector4i.hpp b/include/godot_cpp/variant/vector4i.hpp index 9d70fb78..ccfcf6da 100644 --- a/include/godot_cpp/variant/vector4i.hpp +++ b/include/godot_cpp/variant/vector4i.hpp @@ -73,6 +73,14 @@ struct _NO_DISCARD_ Vector4i { Vector4i::Axis min_axis_index() const; Vector4i::Axis max_axis_index() const; + Vector4i min(const Vector4i &p_vector4i) const { + return Vector4i(MIN(x, p_vector4i.x), MIN(y, p_vector4i.y), MIN(z, p_vector4i.z), MIN(w, p_vector4i.w)); + } + + Vector4i max(const Vector4i &p_vector4i) const { + return Vector4i(MAX(x, p_vector4i.x), MAX(y, p_vector4i.y), MAX(z, p_vector4i.z), MAX(w, p_vector4i.w)); + } + _FORCE_INLINE_ int64_t length_squared() const; _FORCE_INLINE_ double length() const; diff --git a/test/project/example.gdextension b/test/project/example.gdextension index 90fd53a0..99c1746d 100644 --- a/test/project/example.gdextension +++ b/test/project/example.gdextension @@ -1,7 +1,7 @@ [configuration] entry_symbol = "example_library_init" -compatibility_minimum = 4.1 +compatibility_minimum = "4.1" [libraries] diff --git a/test/project/main.gd b/test/project/main.gd index cedd5124..12f535f1 100644 --- a/test/project/main.gd +++ b/test/project/main.gd @@ -94,6 +94,49 @@ func _ready(): example.group_subgroup_custom_position = Vector2(50, 50) assert_equal(example.group_subgroup_custom_position, Vector2(50, 50)) + # Test Object::cast_to<>() and that correct wrappers are being used. + var control = Control.new() + var sprite = Sprite2D.new() + var example_ref = ExampleRef.new() + + assert_equal(example.test_object_cast_to_node(control), true) + assert_equal(example.test_object_cast_to_control(control), true) + assert_equal(example.test_object_cast_to_example(control), false) + + assert_equal(example.test_object_cast_to_node(example), true) + assert_equal(example.test_object_cast_to_control(example), true) + assert_equal(example.test_object_cast_to_example(example), true) + + assert_equal(example.test_object_cast_to_node(sprite), true) + assert_equal(example.test_object_cast_to_control(sprite), false) + assert_equal(example.test_object_cast_to_example(sprite), false) + + assert_equal(example.test_object_cast_to_node(example_ref), false) + assert_equal(example.test_object_cast_to_control(example_ref), false) + assert_equal(example.test_object_cast_to_example(example_ref), false) + + control.queue_free() + sprite.queue_free() + + # Test conversions to and from Variant. + assert_equal(example.test_variant_vector2i_conversion(Vector2i(1, 1)), Vector2i(1, 1)) + assert_equal(example.test_variant_vector2i_conversion(Vector2(1.0, 1.0)), Vector2i(1, 1)) + assert_equal(example.test_variant_int_conversion(10), 10) + assert_equal(example.test_variant_int_conversion(10.0), 10) + assert_equal(example.test_variant_float_conversion(10.0), 10.0) + assert_equal(example.test_variant_float_conversion(10), 10.0) + + # Test that ptrcalls from GDExtension to the engine are correctly encoding Object and RefCounted. + var new_node = Node.new() + example.test_add_child(new_node) + assert_equal(new_node.get_parent(), example) + + var new_tileset = TileSet.new() + var new_tilemap = TileMap.new() + example.test_set_tileset(new_tilemap, new_tileset) + assert_equal(new_tilemap.tile_set, new_tileset) + new_tilemap.queue_free() + # Constants. assert_equal(Example.FIRST, 0) assert_equal(Example.ANSWER_TO_EVERYTHING, 42) diff --git a/test/project/project.godot b/test/project/project.godot index eafcad30..3ed679b4 100644 --- a/test/project/project.godot +++ b/test/project/project.godot @@ -12,7 +12,7 @@ config_version=5 config/name="GDExtension Test Project" run/main_scene="res://main.tscn" -config/features=PackedStringArray("4.1") +config/features=PackedStringArray("4.2") config/icon="res://icon.png" [native_extensions] diff --git a/test/src/example.cpp b/test/src/example.cpp index fb47dd8d..599b34bd 100644 --- a/test/src/example.cpp +++ b/test/src/example.cpp @@ -141,6 +141,17 @@ void Example::_bind_methods() { ClassDB::bind_method(D_METHOD("test_string_is_fourty_two"), &Example::test_string_is_fourty_two); ClassDB::bind_method(D_METHOD("test_vector_ops"), &Example::test_vector_ops); + ClassDB::bind_method(D_METHOD("test_object_cast_to_node", "object"), &Example::test_object_cast_to_node); + ClassDB::bind_method(D_METHOD("test_object_cast_to_control", "object"), &Example::test_object_cast_to_control); + ClassDB::bind_method(D_METHOD("test_object_cast_to_example", "object"), &Example::test_object_cast_to_example); + + ClassDB::bind_method(D_METHOD("test_variant_vector2i_conversion", "variant"), &Example::test_variant_vector2i_conversion); + ClassDB::bind_method(D_METHOD("test_variant_int_conversion", "variant"), &Example::test_variant_int_conversion); + ClassDB::bind_method(D_METHOD("test_variant_float_conversion", "variant"), &Example::test_variant_float_conversion); + + ClassDB::bind_method(D_METHOD("test_add_child", "node"), &Example::test_add_child); + ClassDB::bind_method(D_METHOD("test_set_tileset", "tilemap", "tileset"), &Example::test_set_tileset); + ClassDB::bind_method(D_METHOD("test_bitfield", "flags"), &Example::test_bitfield); ClassDB::bind_method(D_METHOD("test_rpc", "value"), &Example::test_rpc); @@ -348,6 +359,38 @@ Example *Example::test_node_argument(Example *p_node) const { return p_node; } +bool Example::test_object_cast_to_node(Object *p_object) const { + return Object::cast_to(p_object) != nullptr; +} + +bool Example::test_object_cast_to_control(Object *p_object) const { + return Object::cast_to(p_object) != nullptr; +} + +bool Example::test_object_cast_to_example(Object *p_object) const { + return Object::cast_to(p_object) != nullptr; +} + +Vector2i Example::test_variant_vector2i_conversion(const Variant &p_variant) const { + return p_variant; +} + +int Example::test_variant_int_conversion(const Variant &p_variant) const { + return p_variant; +} + +float Example::test_variant_float_conversion(const Variant &p_variant) const { + return p_variant; +} + +void Example::test_add_child(Node *p_node) { + add_child(p_node); +} + +void Example::test_set_tileset(TileMap *p_tilemap, const Ref &p_tileset) const { + p_tilemap->set_tileset(p_tileset); +} + BitField Example::test_bitfield(BitField flags) { return flags; } diff --git a/test/src/example.h b/test/src/example.h index a84efedc..6223a14e 100644 --- a/test/src/example.h +++ b/test/src/example.h @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include #include @@ -120,6 +122,17 @@ public: bool test_string_is_fourty_two(const String &p_str) const; int test_vector_ops() const; + bool test_object_cast_to_node(Object *p_object) const; + bool test_object_cast_to_control(Object *p_object) const; + bool test_object_cast_to_example(Object *p_object) const; + + Vector2i test_variant_vector2i_conversion(const Variant &p_variant) const; + int test_variant_int_conversion(const Variant &p_variant) const; + float test_variant_float_conversion(const Variant &p_variant) const; + + void test_add_child(Node *p_node); + void test_set_tileset(TileMap *p_tilemap, const Ref &p_tileset) const; + BitField test_bitfield(BitField flags); // RPC diff --git a/tools/godotcpp.py b/tools/godotcpp.py new file mode 100644 index 00000000..60cd34b4 --- /dev/null +++ b/tools/godotcpp.py @@ -0,0 +1,309 @@ +import os, sys, platform + +from SCons.Variables import EnumVariable, PathVariable, BoolVariable +from SCons.Tool import Tool +from SCons.Builder import Builder + +from binding_generator import scons_generate_bindings, scons_emit_files + + +def add_sources(sources, dir, extension): + for f in os.listdir(dir): + if f.endswith("." + extension): + sources.append(dir + "/" + f) + + +def normalize_path(val, env): + return val if os.path.isabs(val) else os.path.join(env.Dir("#").abspath, val) + + +def validate_file(key, val, env): + if not os.path.isfile(normalize_path(val, env)): + raise UserError("'%s' is not a file: %s" % (key, val)) + + +def validate_dir(key, val, env): + if not os.path.isdir(normalize_path(val, env)): + raise UserError("'%s' is not a directory: %s" % (key, val)) + + +def validate_parent_dir(key, val, env): + if not os.path.isdir(normalize_path(os.path.dirname(val), env)): + raise UserError("'%s' is not a directory: %s" % (key, os.path.dirname(val))) + + +platforms = ("linux", "macos", "windows", "android", "ios", "javascript") + +# CPU architecture options. +architecture_array = [ + "", + "universal", + "x86_32", + "x86_64", + "arm32", + "arm64", + "rv64", + "ppc32", + "ppc64", + "wasm32", +] +architecture_aliases = { + "x64": "x86_64", + "amd64": "x86_64", + "armv7": "arm32", + "armv8": "arm64", + "arm64v8": "arm64", + "aarch64": "arm64", + "rv": "rv64", + "riscv": "rv64", + "riscv64": "rv64", + "ppcle": "ppc32", + "ppc": "ppc32", + "ppc64le": "ppc64", +} + + +def exists(env): + return True + + +def options(opts, env): + # Try to detect the host platform automatically. + # This is used if no `platform` argument is passed + if sys.platform.startswith("linux"): + default_platform = "linux" + elif sys.platform == "darwin": + default_platform = "macos" + elif sys.platform == "win32" or sys.platform == "msys": + default_platform = "windows" + elif ARGUMENTS.get("platform", ""): + default_platform = ARGUMENTS.get("platform") + else: + raise ValueError("Could not detect platform automatically, please specify with platform=") + + opts.Add( + EnumVariable( + key="platform", + help="Target platform", + default=env.get("platform", default_platform), + allowed_values=platforms, + ignorecase=2, + ) + ) + + # Editor and template_debug are compatible (i.e. you can use the same binary for Godot editor builds and Godot debug templates). + # Godot release templates are only compatible with "template_release" builds. + # For this reason, we default to template_debug builds, unlike Godot which defaults to editor builds. + opts.Add( + EnumVariable( + key="target", + help="Compilation target", + default=env.get("target", "template_debug"), + allowed_values=("editor", "template_release", "template_debug"), + ) + ) + opts.Add( + PathVariable( + key="gdextension_dir", + help="Path to a custom directory containing GDExtension interface header and API JSON file", + default=env.get("gdextension_dir", None), + validator=validate_dir, + ) + ) + opts.Add( + PathVariable( + key="custom_api_file", + help="Path to a custom GDExtension API JSON file (takes precedence over `gdextension_dir`)", + default=env.get("custom_api_file", None), + validator=validate_file, + ) + ) + opts.Add( + BoolVariable( + key="generate_bindings", + help="Force GDExtension API bindings generation. Auto-detected by default.", + default=env.get("generate_bindings", False), + ) + ) + opts.Add( + BoolVariable( + key="generate_template_get_node", + help="Generate a template version of the Node class's get_node.", + default=env.get("generate_template_get_node", True), + ) + ) + opts.Add( + BoolVariable( + key="build_library", + help="Build the godot-cpp library.", + default=env.get("build_library", True), + ) + ) + opts.Add( + EnumVariable( + key="precision", + help="Set the floating-point precision level", + default=env.get("precision", "single"), + allowed_values=("single", "double"), + ) + ) + opts.Add( + EnumVariable( + key="arch", + help="CPU architecture", + default=env.get("arch", ""), + allowed_values=architecture_array, + map=architecture_aliases, + ) + ) + + # compiledb + opts.Add( + BoolVariable( + key="compiledb", + help="Generate compilation DB (`compile_commands.json`) for external tools", + default=env.get("compiledb", False), + ) + ) + opts.Add( + PathVariable( + key="compiledb_file", + help="Path to a custom `compile_commands.json` file", + default=env.get("compiledb_file", "compile_commands.json"), + validator=validate_parent_dir, + ) + ) + + # Add platform options + for pl in platforms: + tool = Tool(pl, toolpath=["tools"]) + if hasattr(tool, "options"): + tool.options(opts) + + # Targets flags tool (optimizations, debug symbols) + target_tool = Tool("targets", toolpath=["tools"]) + target_tool.options(opts) + + +def generate(env): + # Default num_jobs to local cpu count if not user specified. + # SCons has a peculiarity where user-specified options won't be overridden + # by SetOption, so we can rely on this to know if we should use our default. + initial_num_jobs = env.GetOption("num_jobs") + altered_num_jobs = initial_num_jobs + 1 + env.SetOption("num_jobs", altered_num_jobs) + if env.GetOption("num_jobs") == altered_num_jobs: + cpu_count = os.cpu_count() + if cpu_count is None: + print("Couldn't auto-detect CPU count to configure build parallelism. Specify it with the -j argument.") + else: + safer_cpu_count = cpu_count if cpu_count <= 4 else cpu_count - 1 + print( + "Auto-detected %d CPU cores available for build parallelism. Using %d cores by default. You can override it with the -j argument." + % (cpu_count, safer_cpu_count) + ) + env.SetOption("num_jobs", safer_cpu_count) + + # Process CPU architecture argument. + if env["arch"] == "": + # No architecture specified. Default to arm64 if building for Android, + # universal if building for macOS or iOS, wasm32 if building for web, + # otherwise default to the host architecture. + if env["platform"] in ["macos", "ios"]: + env["arch"] = "universal" + elif env["platform"] == "android": + env["arch"] = "arm64" + elif env["platform"] == "javascript": + env["arch"] = "wasm32" + else: + host_machine = platform.machine().lower() + if host_machine in architecture_array: + env["arch"] = host_machine + elif host_machine in architecture_aliases.keys(): + env["arch"] = architecture_aliases[host_machine] + elif "86" in host_machine: + # Catches x86, i386, i486, i586, i686, etc. + env["arch"] = "x86_32" + else: + print("Unsupported CPU architecture: " + host_machine) + Exit() + + print("Building for architecture " + env["arch"] + " on platform " + env["platform"]) + + tool = Tool(env["platform"], toolpath=["tools"]) + + if tool is None or not tool.exists(env): + raise ValueError("Required toolchain not found for platform " + env["platform"]) + + tool.generate(env) + target_tool = Tool("targets", toolpath=["tools"]) + target_tool.generate(env) + + # Require C++17 + if env.get("is_msvc", False): + env.Append(CXXFLAGS=["/std:c++17"]) + else: + env.Append(CXXFLAGS=["-std=c++17"]) + + if env["precision"] == "double": + env.Append(CPPDEFINES=["REAL_T_IS_DOUBLE"]) + + # Suffix + suffix = ".{}.{}".format(env["platform"], env["target"]) + if env.dev_build: + suffix += ".dev" + if env["precision"] == "double": + suffix += ".double" + suffix += "." + env["arch"] + if env["ios_simulator"]: + suffix += ".simulator" + + env["suffix"] = suffix # Exposed when included from another project + env["OBJSUFFIX"] = suffix + env["OBJSUFFIX"] + + # compile_commands.json + if env.get("compiledb", False): + env.Tool("compilation_db") + env.Alias("compiledb", env.CompilationDatabase(normalize_path(env["compiledb_file"], env))) + + # Builders + env.Append(BUILDERS={"GodotCPPBindings": Builder(action=scons_generate_bindings, emitter=scons_emit_files)}) + env.AddMethod(_godot_cpp, "GodotCPP") + + +def _godot_cpp(env): + api_file = normalize_path(env.get("custom_api_file", env.File("gdextension/extension_api.json").abspath), env) + extension_dir = normalize_path(env.get("gdextension_dir", env.Dir("gdextension").abspath), env) + bindings = env.GodotCPPBindings( + env.Dir("."), + [ + api_file, + os.path.join(extension_dir, "gdextension_interface.h"), + "binding_generator.py", + ], + ) + # Forces bindings regeneration. + if env["generate_bindings"]: + AlwaysBuild(bindings) + NoCache(bindings) + + # Sources to compile + sources = [] + add_sources(sources, "src", "cpp") + add_sources(sources, "src/classes", "cpp") + add_sources(sources, "src/core", "cpp") + add_sources(sources, "src/variant", "cpp") + sources.extend([f for f in bindings if str(f).endswith(".cpp")]) + + # Includes + env.AppendUnique(CPPPATH=[env.Dir(d) for d in [extension_dir, "include", "gen/include"]]) + + library = None + library_name = "libgodot-cpp" + env["suffix"] + env["LIBSUFFIX"] + + if env["build_library"]: + library = env.StaticLibrary(target=env.File("bin/%s" % library_name), source=sources) + env.Default(library) + + env.AppendUnique(LIBS=[env.File("bin/%s" % library_name)]) + return library diff --git a/tools/targets.py b/tools/targets.py index 5f8e7688..21611349 100644 --- a/tools/targets.py +++ b/tools/targets.py @@ -1,10 +1,14 @@ import os +import subprocess import sys from SCons.Script import ARGUMENTS from SCons.Variables import * from SCons.Variables.BoolVariable import _text2bool +# Helper methods + + def get_cmdline_bool(option, default): """We use `ARGUMENTS.get()` to check if options were manually overridden on the command line, and SCons' _text2bool helper to convert them to booleans, otherwise they're handled as strings. @@ -16,6 +20,24 @@ def get_cmdline_bool(option, default): return default +def using_clang(env): + return "clang" in os.path.basename(env["CC"]) + + +def is_vanilla_clang(env): + if not using_clang(env): + return False + try: + version = subprocess.check_output([env.subst(env["CXX"]), "--version"]).strip().decode("utf-8") + except (subprocess.CalledProcessError, OSError): + print("Couldn't parse CXX environment variable to infer compiler version.") + return False + return not version.startswith("Apple") + + +# Main tool definition + + def options(opts): opts.Add( EnumVariable( @@ -34,19 +56,21 @@ def exists(env): def generate(env): + # Configuration of build targets: + # - Editor or template + # - Debug features (DEBUG_ENABLED code) + # - Dev only code (DEV_ENABLED code) + # - Optimization level + # - Debug symbols for crash traces / debuggers + + # Keep this configuration in sync with SConstruct in upstream Godot. + + env.editor_build = env["target"] == "editor" env.dev_build = env["dev_build"] env.debug_features = env["target"] in ["editor", "template_debug"] - env.editor_build = env["target"] == "editor" - - if env.editor_build: - env.AppendUnique(CPPDEFINES=["TOOLS_ENABLED"]) - - if env.debug_features: - env.AppendUnique(CPPDEFINES=["DEBUG_ENABLED", "DEBUG_METHODS_ENABLED"]) if env.dev_build: opt_level = "none" - env.AppendUnique(CPPDEFINES=["DEV_ENABLED"]) elif env.debug_features: opt_level = "speed_trace" else: # Release @@ -55,6 +79,26 @@ def generate(env): env["optimize"] = ARGUMENTS.get("optimize", opt_level) env["debug_symbols"] = get_cmdline_bool("debug_symbols", env.dev_build) + if env.editor_build: + env.Append(CPPDEFINES=["TOOLS_ENABLED"]) + + if env.debug_features: + # DEBUG_ENABLED enables debugging *features* and debug-only code, which is intended + # to give *users* extra debugging information for their game development. + env.Append(CPPDEFINES=["DEBUG_ENABLED"]) + # In upstream Godot this is added in typedefs.h when DEBUG_ENABLED is set. + env.Append(CPPDEFINES=["DEBUG_METHODS_ENABLED"]) + + if env.dev_build: + # DEV_ENABLED enables *engine developer* code which should only be compiled for those + # working on the engine itself. + env.Append(CPPDEFINES=["DEV_ENABLED"]) + else: + # Disable assert() for production targets (only used in thirdparty code). + env.Append(CPPDEFINES=["NDEBUG"]) + + # Set optimize and debug_symbols flags. + # "custom" means do nothing and let users set their own optimization flags. if env.get("is_msvc", False): if env["debug_symbols"]: env.Append(CCFLAGS=["/Zi", "/FS"]) @@ -71,13 +115,21 @@ def generate(env): env.Append(LINKFLAGS=["/OPT:REF"]) elif env["optimize"] == "debug" or env["optimize"] == "none": env.Append(CCFLAGS=["/Od"]) - else: if env["debug_symbols"]: + # Adding dwarf-4 explicitly makes stacktraces work with clang builds, + # otherwise addr2line doesn't understand them. + env.Append(CCFLAGS=["-gdwarf-4"]) if env.dev_build: env.Append(CCFLAGS=["-g3"]) else: env.Append(CCFLAGS=["-g2"]) + else: + if using_clang(env) and not is_vanilla_clang(env): + # Apple Clang, its linker doesn't like -s. + env.Append(LINKFLAGS=["-Wl,-S", "-Wl,-x", "-Wl,-dead_strip"]) + else: + env.Append(LINKFLAGS=["-s"]) if env["optimize"] == "speed": env.Append(CCFLAGS=["-O3"])