From 2c41abc24bbe0c151e03952cd434598f28c2bb24 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Thu, 18 May 2023 23:11:59 +0200 Subject: [PATCH] # This is a combination of 20 commits. # This is the 1st commit message: Parse gdextension_interface.h declarations using regex # This is the commit message #2: AsUninit trait to convert FFI pointers to their uninitialized versions # This is the commit message #3: GodotFfi::from_sys_init() now uses uninitialized pointer types # This is the commit message #4: Introduce GDExtensionUninitialized*Ptr, without changing semantics # This is the commit message #5: Adjust init code to new get_proc_address mechanism # This is the commit message #6: Make `trace` feature available in godot-ffi, fix interface access before initialization # This is the commit message #7: Compatibility layer between Godot 4.0 and 4.1 (different GDExtension APIs) # This is the commit message #8: Add GdextBuild to access build/runtime metadata # This is the commit message #9: Detect 4.0 <-> 4.1 mismatches in both directions + missing `compatibility_minimum = 4.1` # This is the commit message #10: Detect legacy/modern version of C header (also without `custom-godot` feature) # This is the commit message #11: CI: add jobs that use patched 4.0.x versions # This is the commit message #12: Remove several memory leaks by constructing into uninitialized pointers # This is the commit message #13: CI: memcheck jobs for both 4.0.3 and nightly # This is the commit message #14: Remove ToVariant, FromVariant, and VariantMetadata impls for pointers This commit splits SignatureTuple into two separate traits: PtrcallSignatureTuple and VarcallSignatureTuple. The latter is a child of the former. PtrcallSignatureTuple is used for ptrcall and only demands GodotFuncMarshall of its arguments. VarcallSignatureTuple is used for varcall and additionally demands ToVariant, FromVariant, and VariantMetadata of its arguments, so pointers cannot benefit from the optimizations provided by varcall over ptrcall. # This is the commit message #15: Adds FromVariant and ToVariant proc macros # This is the commit message #16: godot-core: builtin: reimplement Plane functions/methods # This is the commit message #17: impl GodotFfi for Option when T is pointer sized and nullable #240 Additionally FromVariant and ToVariant are also implemented for Option> to satisfy all the requirements for ffi and godot_api. # This is the commit message #18: Fix UB in virtual method calls that take objects Fix incorrect incrementing of refcount when calling in to godot Fix refcount not being incremented when we receive a refcounted object in virtual methods # This is the commit message #19: fix UB caused by preload weirdness # This is the commit message #20: Implements swizzle and converts from/to tuples --- .github/composite/godot-itest/action.yml | 51 +- .github/workflows/full-ci.yml | 68 ++- .../godot/DodgeTheCreeps.gdextension | 1 + godot-bindings/Cargo.toml | 2 +- godot-bindings/res/tweak.patch | 41 +- godot-bindings/src/godot_exe.rs | 100 +++- godot-codegen/Cargo.toml | 4 + godot-codegen/src/api_parser.rs | 13 + godot-codegen/src/central_generator.rs | 77 ++- godot-codegen/src/interface_generator.rs | 215 ++++++++ godot-codegen/src/lib.rs | 12 +- godot-codegen/src/util.rs | 5 + godot-core/Cargo.toml | 2 +- godot-core/src/builtin/array.rs | 5 +- godot-core/src/builtin/callable.rs | 2 +- godot-core/src/builtin/dictionary.rs | 48 +- godot-core/src/builtin/macros.rs | 18 +- godot-core/src/builtin/meta/signature.rs | 73 ++- godot-core/src/builtin/mod.rs | 21 +- godot-core/src/builtin/plane.rs | 512 +++++++++++++++++- godot-core/src/builtin/quaternion.rs | 4 +- godot-core/src/builtin/string/godot_string.rs | 6 +- godot-core/src/builtin/string/node_path.rs | 2 +- godot-core/src/builtin/string/string_name.rs | 3 +- godot-core/src/builtin/variant/impls.rs | 76 +-- godot-core/src/builtin/variant/mod.rs | 43 +- .../src/builtin/variant/variant_traits.rs | 9 +- godot-core/src/builtin/vectors/mod.rs | 73 +++ .../src/builtin/{ => vectors}/vector2.rs | 34 +- .../src/builtin/{ => vectors}/vector2i.rs | 28 +- .../src/builtin/{ => vectors}/vector3.rs | 34 +- .../src/builtin/{ => vectors}/vector3i.rs | 31 +- .../src/builtin/{ => vectors}/vector4.rs | 33 +- .../src/builtin/{ => vectors}/vector4i.rs | 35 +- .../builtin/{ => vectors}/vector_macros.rs | 39 ++ .../src/builtin/vectors/vector_utils.rs | 91 ++++ godot-core/src/init/mod.rs | 4 +- godot-core/src/lib.rs | 35 +- godot-core/src/log.rs | 64 +-- godot-core/src/macros.rs | 8 +- godot-core/src/obj/as_arg.rs | 7 +- godot-core/src/obj/gd.rs | 69 ++- godot-core/src/obj/instance_id.rs | 2 +- godot-ffi/Cargo.toml | 1 + godot-ffi/build.rs | 2 +- godot-ffi/src/compat/compat_4_0.rs | 54 ++ godot-ffi/src/compat/compat_4_1.rs | 118 ++++ godot-ffi/src/compat/mod.rs | 38 ++ godot-ffi/src/godot_ffi.rs | 85 ++- godot-ffi/src/lib.rs | 179 +++++- godot-macros/src/derive_from_variant.rs | 200 +++++++ godot-macros/src/derive_to_variant.rs | 230 ++++++++ godot-macros/src/gdextension.rs | 4 +- godot-macros/src/lib.rs | 12 + godot-macros/src/util.rs | 36 +- godot/src/lib.rs | 4 +- .../.godot/global_script_class_cache.cfg | 12 + itest/godot/ManualFfiTests.gd | 86 ++- itest/godot/SpecialTests.gd | 49 ++ itest/godot/TestRunner.gd | 49 +- itest/godot/TestSuite.gd | 18 +- itest/godot/TestSuiteSpecial.gd | 34 ++ itest/godot/itest.gdextension | 1 + itest/rust/src/derive_variant.rs | 155 ++++++ itest/rust/src/lib.rs | 2 + itest/rust/src/native_structures_test.rs | 2 +- itest/rust/src/object_test.rs | 6 +- itest/rust/src/option_ffi_test.rs | 87 +++ itest/rust/src/runner.rs | 32 +- itest/rust/src/variant_test.rs | 15 +- itest/rust/src/virtual_methods_test.rs | 85 ++- 71 files changed, 3086 insertions(+), 510 deletions(-) create mode 100644 godot-codegen/src/interface_generator.rs create mode 100644 godot-core/src/builtin/vectors/mod.rs rename godot-core/src/builtin/{ => vectors}/vector2.rs (94%) rename godot-core/src/builtin/{ => vectors}/vector2i.rs (87%) rename godot-core/src/builtin/{ => vectors}/vector3.rs (94%) rename godot-core/src/builtin/{ => vectors}/vector3i.rs (87%) rename godot-core/src/builtin/{ => vectors}/vector4.rs (87%) rename godot-core/src/builtin/{ => vectors}/vector4i.rs (86%) rename godot-core/src/builtin/{ => vectors}/vector_macros.rs (91%) create mode 100644 godot-core/src/builtin/vectors/vector_utils.rs create mode 100644 godot-ffi/src/compat/compat_4_0.rs create mode 100644 godot-ffi/src/compat/compat_4_1.rs create mode 100644 godot-ffi/src/compat/mod.rs create mode 100644 godot-macros/src/derive_from_variant.rs create mode 100644 godot-macros/src/derive_to_variant.rs create mode 100644 itest/godot/SpecialTests.gd create mode 100644 itest/godot/TestSuiteSpecial.gd create mode 100644 itest/rust/src/derive_variant.rs create mode 100644 itest/rust/src/option_ffi_test.rs diff --git a/.github/composite/godot-itest/action.yml b/.github/composite/godot-itest/action.yml index fd2cd4ff4..f10db9584 100644 --- a/.github/composite/godot-itest/action.yml +++ b/.github/composite/godot-itest/action.yml @@ -24,6 +24,11 @@ inputs: default: 'false' description: "Should the job check against latest gdextension_interface.h, and warn on difference" + godot-prebuilt-patch: + required: false + default: '' + description: "If specified, sets the branch name of the godot4-prebuilt crate to this value" + rust-toolchain: required: false default: 'stable' @@ -71,6 +76,46 @@ runs: rust: ${{ inputs.rust-toolchain }} with-llvm: ${{ inputs.with-llvm }} + - name: "Patch prebuilt version ({{ inputs.godot-prebuilt-patch }})" + if: inputs.godot-prebuilt-patch != '' + env: + VERSION: ${{ inputs.godot-prebuilt-patch }} + # sed -i'' needed for macOS compatibility, see https://stackoverflow.com/q/4247068 + run: | + echo "Patch prebuilt version to $VERSION..." + + # For newer versions, update the compatibility_minimum in .gdextension files to 4.1 + # Once a 4.1.0 is released, we can invert this and set compatibility_minimum to 4.0 for older versions. + if [[ "$VERSION" == "4.1" ]]; then + echo "Update compatibility_minimum in .gdextension files..." + dirs=("itest" "examples") + for dir in "${dirs[@]}"; do + find "$dir" -type f -name "*.gdextension" -exec sed -i'.bak' 's/compatibility_minimum = 4\.0/compatibility_minimum = 4.1/' {} + + done + + # Versions 4.0.x + else + # Patch only needed if version is not already set + if grep -E 'godot4-prebuilt = { .+ branch = "$VERSION" }' godot-bindings/Cargo.toml; then + echo "Already has version $version; no need for patch." + else + cat << HEREDOC >> Cargo.toml + [patch."https://github.com/godot-rust/godot4-prebuilt"] + godot4-prebuilt = { git = "https://github.com//godot-rust/godot4-prebuilt", branch = "$VERSION" } + HEREDOC + echo "Patched Cargo.toml for version $version." + fi + fi + + shell: bash + + # else + - name: "No patch selected" + if: inputs.godot-prebuilt-patch == '' + run: | + echo "No patch selected; use default godot4-prebuilt version." + shell: bash + - name: "Build gdext (itest)" run: | cargo build -p itest ${{ inputs.rust-extra-args }} @@ -78,7 +123,9 @@ runs: env: RUSTFLAGS: ${{ inputs.rust-env-rustflags }} - # Note: no longer fails, as we expect header to be forward-compatible; instead issues a warning + # This step no longer fails if there's a diff, as we expect header to be forward-compatible; instead issues a warning + # However, still fails if patch cannot be applied (conflict). + # Step is only run in latest, not for compat 4.0.1 etc. -> no need to take into account different header versions. - name: "Copy and compare GDExtension header" if: inputs.godot-check-header == 'true' run: | @@ -96,7 +143,7 @@ runs: echo "\`\`\`diff" >> $GITHUB_STEP_SUMMARY git diff --no-index gdextension_interface_prebuilt.h gdextension_interface.h >> $GITHUB_STEP_SUMMARY || true echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "After manually updating file, run: \`git diff -R > tweak.patch\`." >> $GITHUB_STEP_SUMMARY + echo "After manually updating file, run: \`git diff -R > tweak2.patch && mv tweak2.patch tweak.patch\`." >> $GITHUB_STEP_SUMMARY # Undo modifications mv gdextension_interface_prebuilt.h gdextension_interface.h diff --git a/.github/workflows/full-ci.yml b/.github/workflows/full-ci.yml index bc0f88613..6e212c685 100644 --- a/.github/workflows/full-ci.yml +++ b/.github/workflows/full-ci.yml @@ -127,9 +127,13 @@ jobs: # Additionally, the 'linux (msrv *)' special case will then be listed next to the other 'linux' jobs. # Note: Windows uses '--target x86_64-pc-windows-msvc' by default as Cargo argument. include: - - name: macos + # macOS + + - name: macos-4.0.3 os: macos-12 + artifact-name: macos godot-binary: godot.macos.editor.dev.x86_64 + godot-prebuilt-patch: '4.0.3' - name: macos-double os: macos-12 @@ -142,10 +146,15 @@ jobs: godot-binary: godot.macos.editor.dev.x86_64 rust-extra-args: --features godot/custom-godot with-llvm: true + godot-prebuilt-patch: '4.1' - - name: windows + # Windows + + - name: windows-4.0.3 os: windows-latest + artifact-name: windows godot-binary: godot.windows.editor.dev.x86_64.exe + godot-prebuilt-patch: '4.0.3' - name: windows-double os: windows-latest @@ -157,12 +166,35 @@ jobs: artifact-name: windows godot-binary: godot.windows.editor.dev.x86_64.exe rust-extra-args: --features godot/custom-godot + godot-prebuilt-patch: '4.1' + + # Linux # Don't use latest Ubuntu (22.04) as it breaks lots of ecosystem compatibility. # If ever moving to ubuntu-latest, need to manually install libtinfo5 for LLVM. - - name: linux + - name: linux-4.0.3 + os: ubuntu-20.04 + artifact-name: linux + godot-binary: godot.linuxbsd.editor.dev.x86_64 + godot-check-header: false # disabled for now + + - name: linux-4.0.2 os: ubuntu-20.04 + artifact-name: linux godot-binary: godot.linuxbsd.editor.dev.x86_64 + godot-prebuilt-patch: '4.0.2' + + - name: linux-4.0.1 + os: ubuntu-20.04 + artifact-name: linux + godot-binary: godot.linuxbsd.editor.dev.x86_64 + godot-prebuilt-patch: '4.0.1' + + - name: linux-4.0 + os: ubuntu-20.04 + artifact-name: linux + godot-binary: godot.linuxbsd.editor.dev.x86_64 + godot-prebuilt-patch: '4.0' - name: linux-double os: ubuntu-20.04 @@ -180,6 +212,7 @@ jobs: artifact-name: linux godot-binary: godot.linuxbsd.editor.dev.x86_64 rust-extra-args: --features godot/custom-godot + godot-prebuilt-patch: '4.1' # Special Godot binaries compiled with AddressSanitizer/LeakSanitizer to detect UB/leaks. # Additionally, the Godot source is patched to make dlclose() a no-op, as unloading dynamic libraries loses stacktrace and @@ -187,20 +220,42 @@ jobs: # The gcc version can possibly be removed later, as it is slower and needs a larger artifact than the clang one. # --disallow-focus: fail if #[itest(focus)] is encountered, to prevent running only a few tests for full CI - - name: linux-memcheck-gcc + - name: linux-memcheck-gcc-4.0.3 os: ubuntu-20.04 + artifact-name: linux-memcheck-gcc godot-binary: godot.linuxbsd.editor.dev.x86_64.san godot-args: -- --disallow-focus rust-toolchain: nightly rust-env-rustflags: -Zrandomize-layout - - name: linux-memcheck-clang + - name: linux-memcheck-clang-4.0.3 os: ubuntu-20.04 + artifact-name: linux-memcheck-clang godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san godot-args: -- --disallow-focus rust-toolchain: nightly rust-env-rustflags: -Zrandomize-layout + - name: linux-memcheck-gcc-nightly + os: ubuntu-20.04 + artifact-name: linux-memcheck-gcc + godot-binary: godot.linuxbsd.editor.dev.x86_64.san + godot-args: -- --disallow-focus + rust-toolchain: nightly + rust-env-rustflags: -Zrandomize-layout + rust-extra-args: --features godot/custom-godot + godot-prebuilt-patch: '4.1' + + - name: linux-memcheck-clang-nightly + os: ubuntu-20.04 + artifact-name: linux-memcheck-clang + godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san + godot-args: -- --disallow-focus + rust-toolchain: nightly + rust-env-rustflags: -Zrandomize-layout + rust-extra-args: --features godot/custom-godot + godot-prebuilt-patch: '4.1' + steps: - uses: actions/checkout@v3 @@ -211,11 +266,12 @@ jobs: artifact-name: godot-${{ matrix.artifact-name || matrix.name }} godot-binary: ${{ matrix.godot-binary }} godot-args: ${{ matrix.godot-args }} + godot-prebuilt-patch: ${{ matrix.godot-prebuilt-patch }} rust-extra-args: ${{ matrix.rust-extra-args }} rust-toolchain: ${{ matrix.rust-toolchain || 'stable' }} rust-env-rustflags: ${{ matrix.rust-env-rustflags }} with-llvm: ${{ matrix.with-llvm }} - godot-check-header: ${{ matrix.name == 'linux' }} + godot-check-header: ${{ matrix.godot-check-header }} license-guard: diff --git a/examples/dodge-the-creeps/godot/DodgeTheCreeps.gdextension b/examples/dodge-the-creeps/godot/DodgeTheCreeps.gdextension index 6f5badabd..ef1464641 100644 --- a/examples/dodge-the-creeps/godot/DodgeTheCreeps.gdextension +++ b/examples/dodge-the-creeps/godot/DodgeTheCreeps.gdextension @@ -1,5 +1,6 @@ [configuration] entry_symbol = "gdext_rust_init" +compatibility_minimum = 4.0 [libraries] linux.debug.x86_64 = "res://../../../target/debug/libdodge_the_creeps.so" diff --git a/godot-bindings/Cargo.toml b/godot-bindings/Cargo.toml index 965b0e39a..4752cf75a 100644 --- a/godot-bindings/Cargo.toml +++ b/godot-bindings/Cargo.toml @@ -18,7 +18,7 @@ custom-godot = ["dep:bindgen", "dep:regex", "dep:which"] custom-godot-extheader = [] [dependencies] -godot4-prebuilt = { optional = true, git = "https://github.com/godot-rust/godot4-prebuilt", branch = "4.0.1" } +godot4-prebuilt = { optional = true, git = "https://github.com/godot-rust/godot4-prebuilt", branch = "4.0.3" } # Version >= 1.5.5 for security: https://blog.rust-lang.org/2022/03/08/cve-2022-24713.html # 'unicode-gencat' needed for \d, see: https://docs.rs/regex/1.5.5/regex/#unicode-features diff --git a/godot-bindings/res/tweak.patch b/godot-bindings/res/tweak.patch index 9aad26029..e8f46e553 100644 --- a/godot-bindings/res/tweak.patch +++ b/godot-bindings/res/tweak.patch @@ -1,32 +1,42 @@ diff --git b/godot-ffi/src/gen/gdextension_interface.h a/godot-ffi/src/gen/gdextension_interface.h -index 0b7615f..6db266e 100644 +index 4e4f300..e1cd5fb 100644 --- b/godot-ffi/src/gen/gdextension_interface.h +++ a/godot-ffi/src/gen/gdextension_interface.h -@@ -139,22 +139,22 @@ typedef enum { - - } GDExtensionVariantOperator; +@@ -155,27 +155,27 @@ typedef enum { + // - Some types have no destructor (see `extension_api.json`'s `has_destructor` field), for + // them it is always safe to skip the constructor for the return value if you are in a hurry ;-) -typedef void *GDExtensionVariantPtr; -typedef const void *GDExtensionConstVariantPtr; +-typedef void *GDExtensionUninitializedVariantPtr; -typedef void *GDExtensionStringNamePtr; -typedef const void *GDExtensionConstStringNamePtr; +-typedef void *GDExtensionUninitializedStringNamePtr; -typedef void *GDExtensionStringPtr; -typedef const void *GDExtensionConstStringPtr; +-typedef void *GDExtensionUninitializedStringPtr; -typedef void *GDExtensionObjectPtr; -typedef const void *GDExtensionConstObjectPtr; +-typedef void *GDExtensionUninitializedObjectPtr; -typedef void *GDExtensionTypePtr; -typedef const void *GDExtensionConstTypePtr; +-typedef void *GDExtensionUninitializedTypePtr; -typedef const void *GDExtensionMethodBindPtr; +typedef struct __GdextVariant *GDExtensionVariantPtr; +typedef const struct __GdextVariant *GDExtensionConstVariantPtr; ++typedef struct __GdextUninitializedVariant *GDExtensionUninitializedVariantPtr; +typedef struct __GdextStringName *GDExtensionStringNamePtr; +typedef const struct __GdextStringName *GDExtensionConstStringNamePtr; ++typedef struct __GdextUninitializedStringName *GDExtensionUninitializedStringNamePtr; +typedef struct __GdextString *GDExtensionStringPtr; +typedef const struct __GdextString *GDExtensionConstStringPtr; ++typedef struct __GdextUninitializedString *GDExtensionUninitializedStringPtr; +typedef struct __GdextObject *GDExtensionObjectPtr; +typedef const struct __GdextObject *GDExtensionConstObjectPtr; ++typedef struct __GdextUninitializedObject *GDExtensionUninitializedObjectPtr; +typedef struct __GdextType *GDExtensionTypePtr; +typedef const struct __GdextType *GDExtensionConstTypePtr; ++typedef struct __GdextUninitializedType *GDExtensionUninitializedTypePtr; +typedef const struct __GdextMethodBind *GDExtensionMethodBindPtr; typedef int64_t GDExtensionInt; typedef uint8_t GDExtensionBool; @@ -38,7 +48,22 @@ index 0b7615f..6db266e 100644 /* VARIANT DATA I/O */ -@@ -203,7 +203,7 @@ typedef struct { +@@ -195,11 +195,11 @@ typedef struct { + int32_t expected; + } GDExtensionCallError; + +-typedef void (*GDExtensionVariantFromTypeConstructorFunc)(GDExtensionVariantPtr, GDExtensionTypePtr); +-typedef void (*GDExtensionTypeFromVariantConstructorFunc)(GDExtensionTypePtr, GDExtensionVariantPtr); ++typedef void (*GDExtensionVariantFromTypeConstructorFunc)(GDExtensionUninitializedVariantPtr, GDExtensionTypePtr); ++typedef void (*GDExtensionTypeFromVariantConstructorFunc)(GDExtensionUninitializedTypePtr, GDExtensionVariantPtr); + typedef void (*GDExtensionPtrOperatorEvaluator)(GDExtensionConstTypePtr p_left, GDExtensionConstTypePtr p_right, GDExtensionTypePtr r_result); + typedef void (*GDExtensionPtrBuiltInMethod)(GDExtensionTypePtr p_base, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_return, int p_argument_count); +-typedef void (*GDExtensionPtrConstructor)(GDExtensionTypePtr p_base, const GDExtensionConstTypePtr *p_args); ++typedef void (*GDExtensionPtrConstructor)(GDExtensionUninitializedTypePtr p_base, const GDExtensionConstTypePtr *p_args); + typedef void (*GDExtensionPtrDestructor)(GDExtensionTypePtr p_base); + typedef void (*GDExtensionPtrSetter)(GDExtensionTypePtr p_base, GDExtensionConstTypePtr p_value); + typedef void (*GDExtensionPtrGetter)(GDExtensionConstTypePtr p_base, GDExtensionTypePtr r_value); +@@ -224,7 +224,7 @@ typedef struct { /* EXTENSION CLASSES */ @@ -47,7 +72,7 @@ index 0b7615f..6db266e 100644 typedef GDExtensionBool (*GDExtensionClassSet)(GDExtensionClassInstancePtr p_instance, GDExtensionConstStringNamePtr p_name, GDExtensionConstVariantPtr p_value); typedef GDExtensionBool (*GDExtensionClassGet)(GDExtensionClassInstancePtr p_instance, GDExtensionConstStringNamePtr p_name, GDExtensionVariantPtr r_ret); -@@ -266,7 +266,7 @@ typedef struct { +@@ -287,7 +287,7 @@ typedef struct { void *class_userdata; // Per-class user data, later accessible in instance bindings. } GDExtensionClassCreationInfo; @@ -56,7 +81,7 @@ index 0b7615f..6db266e 100644 /* Method */ -@@ -323,7 +323,7 @@ typedef struct { +@@ -345,7 +345,7 @@ typedef struct { /* SCRIPT INSTANCE EXTENSION */ @@ -65,7 +90,7 @@ index 0b7615f..6db266e 100644 typedef GDExtensionBool (*GDExtensionScriptInstanceSet)(GDExtensionScriptInstanceDataPtr p_instance, GDExtensionConstStringNamePtr p_name, GDExtensionConstVariantPtr p_value); typedef GDExtensionBool (*GDExtensionScriptInstanceGet)(GDExtensionScriptInstanceDataPtr p_instance, GDExtensionConstStringNamePtr p_name, GDExtensionVariantPtr r_ret); -@@ -353,13 +353,13 @@ typedef GDExtensionBool (*GDExtensionScriptInstanceRefCountDecremented)(GDExtens +@@ -375,13 +375,13 @@ typedef GDExtensionBool (*GDExtensionScriptInstanceRefCountDecremented)(GDExtens typedef GDExtensionObjectPtr (*GDExtensionScriptInstanceGetScript)(GDExtensionScriptInstanceDataPtr p_instance); typedef GDExtensionBool (*GDExtensionScriptInstanceIsPlaceholder)(GDExtensionScriptInstanceDataPtr p_instance); diff --git a/godot-bindings/src/godot_exe.rs b/godot-bindings/src/godot_exe.rs index 916104b00..6b2814c6d 100644 --- a/godot-bindings/src/godot_exe.rs +++ b/godot-bindings/src/godot_exe.rs @@ -6,6 +6,7 @@ //! Commands related to Godot executable +use crate::custom::godot_version::GodotVersion; use crate::godot_version::parse_godot_version; use crate::header_gen::generate_rust_binding; use crate::watch::StopWatch; @@ -50,14 +51,19 @@ pub fn write_gdextension_headers( is_h_provided: bool, watch: &mut StopWatch, ) { - if !is_h_provided { + // None=(unknown, no engine), Some=(version of Godot). Later verified by header itself. + let is_engine_4_0; + if is_h_provided { + is_engine_4_0 = None; + } else { // No external C header file: Godot binary is present, we use it to dump C header let godot_bin = locate_godot_binary(); rerun_on_changed(&godot_bin); watch.record("locate_godot"); // Regenerate API JSON if first time or Godot version is different - let _version = read_godot_version(&godot_bin); + let version = read_godot_version(&godot_bin); + is_engine_4_0 = Some(version.major == 4 && version.minor == 0); // if !c_header_path.exists() || has_version_changed(&version) { dump_header_file(&godot_bin, inout_h_path); @@ -67,7 +73,7 @@ pub fn write_gdextension_headers( }; rerun_on_changed(inout_h_path); - patch_c_header(inout_h_path); + patch_c_header(inout_h_path, is_engine_4_0); watch.record("patch_header_h"); generate_rust_binding(inout_h_path, out_rs_path); @@ -93,7 +99,7 @@ fn update_version_file(version: &str) { } */ -fn read_godot_version(godot_bin: &Path) -> String { +fn read_godot_version(godot_bin: &Path) -> GodotVersion { let output = Command::new(godot_bin) .arg("--version") .output() @@ -116,7 +122,7 @@ fn read_godot_version(godot_bin: &Path) -> String { output.trim() ); - parsed.full_string + parsed } Err(e) => { // Don't treat this as fatal error @@ -153,21 +159,55 @@ fn dump_header_file(godot_bin: &Path, out_file: &Path) { println!("Generated {}/gdextension_interface.h.", cwd.display()); } -fn patch_c_header(inout_h_path: &Path) { +fn patch_c_header(inout_h_path: &Path, is_engine_4_0: Option) { // The C header path *must* be passed in by the invoking crate, as the path cannot be relative to this crate. // Otherwise, it can be something like `/home/runner/.cargo/git/checkouts/gdext-76630c89719e160c/efd3b94/godot-bindings`. - // Read the contents of the file into a string - let c = fs::read_to_string(inout_h_path) + println!( + "Patch C header '{}' (is_engine_4_0={is_engine_4_0:?})...", + inout_h_path.display() + ); + + let mut c = fs::read_to_string(inout_h_path) .unwrap_or_else(|_| panic!("failed to read C header file {}", inout_h_path.display())); + // Detect whether header is legacy (4.0) or modern (4.1+) format. + let is_header_4_0 = !c.contains("GDExtensionInterfaceGetProcAddress"); + println!("is_header_4_0={is_header_4_0}"); + + // Sanity check + if let Some(is_engine_4_0) = is_engine_4_0 { + assert_eq!( + is_header_4_0, is_engine_4_0, + "Mismatch between engine/header versions" + ); + } + + if is_header_4_0 { + polyfill_legacy_header(&mut c); + } + + // Patch for variant converters and type constructors + c = c.replace( + "typedef void (*GDExtensionVariantFromTypeConstructorFunc)(GDExtensionVariantPtr, GDExtensionTypePtr);", + "typedef void (*GDExtensionVariantFromTypeConstructorFunc)(GDExtensionUninitializedVariantPtr, GDExtensionTypePtr);" + ) + .replace( + "typedef void (*GDExtensionTypeFromVariantConstructorFunc)(GDExtensionTypePtr, GDExtensionVariantPtr);", + "typedef void (*GDExtensionTypeFromVariantConstructorFunc)(GDExtensionUninitializedTypePtr, GDExtensionVariantPtr);" + ) + .replace( + "typedef void (*GDExtensionPtrConstructor)(GDExtensionTypePtr p_base, const GDExtensionConstTypePtr *p_args);", + "typedef void (*GDExtensionPtrConstructor)(GDExtensionUninitializedTypePtr p_base, const GDExtensionConstTypePtr *p_args);" + ); + // Use single regex with independent "const"/"Const", as there are definitions like this: // typedef const void *GDExtensionMethodBindPtr; let c = Regex::new(r"typedef (const )?void \*GDExtension(Const)?([a-zA-Z0-9]+?)Ptr;") // .expect("regex for mut typedef") .replace_all(&c, "typedef ${1}struct __Gdext$3 *GDExtension${2}${3}Ptr;"); - println!("Patched contents:\n\n{}\n\n", c.as_ref()); + // println!("Patched contents:\n\n{}\n\n", c.as_ref()); // Write the modified contents back to the file fs::write(inout_h_path, c.as_ref()).unwrap_or_else(|_| { @@ -177,6 +217,48 @@ fn patch_c_header(inout_h_path: &Path) { ) }); } + +/// Backport Godot 4.1+ changes to the old GDExtension API, so gdext can use both uniformly. +fn polyfill_legacy_header(c: &mut String) { + // Newer Uninitialized* types -- use same types as initialized ones, because old functions are not written with Uninitialized* in mind + let pos = c + .find("typedef int64_t GDExtensionInt;") + .expect("Unexpected gdextension_interface.h format (int)"); + + c.insert_str( + pos, + "\ + // gdext polyfill\n\ + typedef struct __GdextVariant *GDExtensionUninitializedVariantPtr;\n\ + typedef struct __GdextStringName *GDExtensionUninitializedStringNamePtr;\n\ + typedef struct __GdextString *GDExtensionUninitializedStringPtr;\n\ + typedef struct __GdextObject *GDExtensionUninitializedObjectPtr;\n\ + typedef struct __GdextType *GDExtensionUninitializedTypePtr;\n\ + \n", + ); + + // Typedef GDExtensionInterfaceGetProcAddress (simply resolving to GDExtensionInterface, as it's the same parameter) + let pos = c + .find("/* INITIALIZATION */") + .expect("Unexpected gdextension_interface.h format (struct)"); + + c.insert_str( + pos, + "\ + // gdext polyfill\n\ + typedef struct {\n\ + uint32_t major;\n\ + uint32_t minor;\n\ + uint32_t patch;\n\ + const char *string;\n\ + } GDExtensionGodotVersion;\n\ + typedef void (*GDExtensionInterfaceFunctionPtr)();\n\ + typedef void (*GDExtensionInterfaceGetGodotVersion)(GDExtensionGodotVersion *r_godot_version);\n\ + typedef GDExtensionInterfaceFunctionPtr (*GDExtensionInterfaceGetProcAddress)(const char *p_function_name);\n\ + \n", + ); +} + fn locate_godot_binary() -> PathBuf { if let Ok(string) = std::env::var("GODOT4_BIN") { println!("Found GODOT4_BIN with path to executable: '{string}'"); diff --git a/godot-codegen/Cargo.toml b/godot-codegen/Cargo.toml index 4a49edbc4..d126a003d 100644 --- a/godot-codegen/Cargo.toml +++ b/godot-codegen/Cargo.toml @@ -20,3 +20,7 @@ heck = "0.4" nanoserde = "0.1.29" proc-macro2 = "1" quote = "1" + +# Since we don't use Regex for unicode parsing, the features unicode-bool/unicode-gencat are used instead of unicode-perl. +# See also https://docs.rs/regex/latest/regex/#unicode-features. +regex = { version = "1.5.5", default-features = false, features = ["std", "unicode-bool", "unicode-gencat"] } diff --git a/godot-codegen/src/api_parser.rs b/godot-codegen/src/api_parser.rs index ce73f9d3d..d8a3b8f50 100644 --- a/godot-codegen/src/api_parser.rs +++ b/godot-codegen/src/api_parser.rs @@ -15,6 +15,7 @@ use nanoserde::DeJson; #[derive(DeJson)] pub struct ExtensionApi { + pub header: Header, pub builtin_class_sizes: Vec, pub builtin_classes: Vec, pub classes: Vec, @@ -24,6 +25,16 @@ pub struct ExtensionApi { pub singletons: Vec, } +#[derive(DeJson, Clone, Debug)] +pub struct Header { + pub version_major: u8, + pub version_minor: u8, + pub version_patch: u8, + pub version_status: String, + pub version_build: String, + pub version_full_name: String, +} + #[derive(DeJson)] pub struct ClassSizes { pub build_configuration: String, @@ -248,5 +259,7 @@ pub fn load_extension_api(watch: &mut godot_bindings::StopWatch) -> (ExtensionAp DeJson::deserialize_json(json_str).expect("failed to deserialize JSON"); watch.record("deserialize_json"); + println!("Parsed extension_api.json for version {:?}", model.header); + (model, build_config) } diff --git a/godot-codegen/src/central_generator.rs b/godot-codegen/src/central_generator.rs index ba9389478..7c1373577 100644 --- a/godot-codegen/src/central_generator.rs +++ b/godot-codegen/src/central_generator.rs @@ -23,6 +23,7 @@ struct CentralItems { variant_fn_decls: Vec, variant_fn_inits: Vec, global_enum_defs: Vec, + godot_version: Header, } pub(crate) struct TypeNames { @@ -67,6 +68,7 @@ pub(crate) fn generate_sys_central_file( pub(crate) fn generate_sys_mod_file(core_gen_path: &Path, out_files: &mut Vec) { let code = quote! { pub mod central; + pub mod interface; pub mod gdextension_interface; }; @@ -99,6 +101,7 @@ pub(crate) fn generate_core_central_file( write_file(core_gen_path, "central.rs", core_code, out_files); } +// TODO(bromeon): move to util (postponed due to merge conflicts) pub(crate) fn write_file( gen_path: &Path, filename: &str, @@ -127,11 +130,17 @@ fn make_sys_code(central_items: &CentralItems) -> String { variant_op_enumerators_ord, variant_fn_decls, variant_fn_inits, + godot_version, .. } = central_items; + let build_config_struct = make_build_config(godot_version); + let sys_tokens = quote! { - use crate::{GDExtensionVariantPtr, GDExtensionTypePtr, GDExtensionConstTypePtr, GodotFfi, ffi_methods}; + use crate::{ + ffi_methods, GDExtensionConstTypePtr, GDExtensionTypePtr, GDExtensionUninitializedTypePtr, + GDExtensionUninitializedVariantPtr, GDExtensionVariantPtr, GodotFfi, + }; pub mod types { #(#opaque_types)* @@ -139,12 +148,16 @@ fn make_sys_code(central_items: &CentralItems) -> String { // ---------------------------------------------------------------------------------------------------------------------------------------------- + #build_config_struct + + // ---------------------------------------------------------------------------------------------------------------------------------------------- + pub struct GlobalMethodTable { #(#variant_fn_decls)* } impl GlobalMethodTable { - pub(crate) unsafe fn new(interface: &crate::GDExtensionInterface) -> Self { + pub(crate) unsafe fn load(interface: &crate::GDExtensionInterface) -> Self { Self { #(#variant_fn_inits)* } @@ -224,6 +237,45 @@ fn make_sys_code(central_items: &CentralItems) -> String { sys_tokens.to_string() } +fn make_build_config(header: &Header) -> TokenStream { + let version_string = header + .version_full_name + .strip_prefix("Godot Engine ") + .unwrap_or(&header.version_full_name); + let major = header.version_major; + let minor = header.version_minor; + let patch = header.version_patch; + + // Should this be mod? + quote! { + /// Provides meta-information about the library and the Godot version in use. + pub struct GdextBuild; + + impl GdextBuild { + /// Godot version against which gdext was compiled. + /// + /// Example format: `v4.0.stable.official` + pub const fn godot_static_version_string() -> &'static str { + #version_string + } + + /// Godot version against which gdext was compiled, as `(major, minor, patch)` triple. + pub const fn godot_static_version_triple() -> (u8, u8, u8) { + (#major, #minor, #patch) + } + + /// Version of the Godot engine which loaded gdext via GDExtension binding. + pub fn godot_runtime_version_string() -> String { + unsafe { + let char_ptr = crate::runtime_metadata().godot_version.string; + let c_str = std::ffi::CStr::from_ptr(char_ptr); + String::from_utf8_lossy(c_str.to_bytes()).to_string() + } + } + } + } +} + fn make_core_code(central_items: &CentralItems) -> String { let CentralItems { variant_ty_enumerators_pascal, @@ -307,6 +359,7 @@ fn make_central_items(api: &ExtensionApi, build_config: &str, ctx: &mut Context) variant_fn_decls: Vec::with_capacity(len), variant_fn_inits: Vec::with_capacity(len), global_enum_defs: Vec::new(), + godot_version: api.header.clone(), }; let mut builtin_types: Vec<_> = builtin_types_map.values().collect(); @@ -500,9 +553,12 @@ fn make_variant_fns( let variant_type = quote! { crate:: #variant_type }; // Field declaration + // The target types are uninitialized-ptrs, because Godot performs placement new on those: + // https://github.com/godotengine/godot/blob/b40b35fb39f0d0768d7ec2976135adffdce1b96d/core/variant/variant_internal.h#L1535-L1535 + let decl = quote! { - pub #to_variant: unsafe extern "C" fn(GDExtensionVariantPtr, GDExtensionTypePtr), - pub #from_variant: unsafe extern "C" fn(GDExtensionTypePtr, GDExtensionVariantPtr), + pub #to_variant: unsafe extern "C" fn(GDExtensionUninitializedVariantPtr, GDExtensionTypePtr), + pub #from_variant: unsafe extern "C" fn(GDExtensionUninitializedTypePtr, GDExtensionVariantPtr), #op_eq_decls #op_lt_decls #construct_decls @@ -575,10 +631,15 @@ fn make_construct_fns( let (construct_extra_decls, construct_extra_inits) = make_extra_constructors(type_names, constructors, builtin_types); - // Generic signature: fn(base: GDExtensionTypePtr, args: *const GDExtensionTypePtr) + // Target types are uninitialized pointers, because Godot uses placement-new for raw pointer constructions. Callstack: + // https://github.com/godotengine/godot/blob/b40b35fb39f0d0768d7ec2976135adffdce1b96d/core/extension/gdextension_interface.cpp#L511 + // https://github.com/godotengine/godot/blob/b40b35fb39f0d0768d7ec2976135adffdce1b96d/core/variant/variant_construct.cpp#L299 + // https://github.com/godotengine/godot/blob/b40b35fb39f0d0768d7ec2976135adffdce1b96d/core/variant/variant_construct.cpp#L36 + // https://github.com/godotengine/godot/blob/b40b35fb39f0d0768d7ec2976135adffdce1b96d/core/variant/variant_construct.h#L267 + // https://github.com/godotengine/godot/blob/b40b35fb39f0d0768d7ec2976135adffdce1b96d/core/variant/variant_construct.h#L50 let decls = quote! { - pub #construct_default: unsafe extern "C" fn(GDExtensionTypePtr, *const GDExtensionConstTypePtr), - pub #construct_copy: unsafe extern "C" fn(GDExtensionTypePtr, *const GDExtensionConstTypePtr), + pub #construct_default: unsafe extern "C" fn(GDExtensionUninitializedTypePtr, *const GDExtensionConstTypePtr), + pub #construct_copy: unsafe extern "C" fn(GDExtensionUninitializedTypePtr, *const GDExtensionConstTypePtr), #(#construct_extra_decls)* }; @@ -627,7 +688,7 @@ fn make_extra_constructors( let err = format_load_error(&ident); extra_decls.push(quote! { - pub #ident: unsafe extern "C" fn(GDExtensionTypePtr, *const GDExtensionConstTypePtr), + pub #ident: unsafe extern "C" fn(GDExtensionUninitializedTypePtr, *const GDExtensionConstTypePtr), }); let i = i as i32; diff --git a/godot-codegen/src/interface_generator.rs b/godot-codegen/src/interface_generator.rs new file mode 100644 index 000000000..d7c2e2bb1 --- /dev/null +++ b/godot-codegen/src/interface_generator.rs @@ -0,0 +1,215 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use crate::central_generator::write_file; +use crate::util::ident; +use proc_macro2::{Ident, Literal, TokenStream}; +use quote::quote; +use regex::Regex; +use std::fs; +use std::path::{Path, PathBuf}; + +struct GodotFuncPtr { + name: Ident, + func_ptr_ty: Ident, + doc: String, +} + +pub(crate) fn generate_sys_interface_file( + h_path: &Path, + sys_gen_path: &Path, + is_godot_4_0: bool, + out_files: &mut Vec, +) { + let code = if is_godot_4_0 { + // Compat for 4.0.x + // Most polyfills are in godot_exe.rs, fn polyfill_legacy_header() + quote! { + #[path = "../compat/compat_4_0.rs"] + mod compat_4_0; + + pub use compat_4_0::InitCompat; + } + } else { + generate_proc_address_funcs(h_path) + }; + + write_file(sys_gen_path, "interface.rs", code.to_string(), out_files); +} + +fn generate_proc_address_funcs(h_path: &Path) -> TokenStream { + let header_code = fs::read_to_string(h_path) + .expect("failed to read gdextension_interface.h for header parsing"); + let func_ptrs = parse_function_pointers(&header_code); + + let mut fptr_decls = vec![]; + let mut fptr_inits = vec![]; + for fptr in func_ptrs { + let GodotFuncPtr { + name, + func_ptr_ty, + doc, + } = fptr; + + let name_str = Literal::byte_string(format!("{}\0", name).as_bytes()); + + let decl = quote! { + #[doc = #doc] + pub #name: crate::#func_ptr_ty, + }; + + // SAFETY: transmute relies on Option and Option having the same layout. + // It might be better to transmute the raw function pointers, but then we have no type names. + let init = quote! { + #name: std::mem::transmute::< + crate::GDExtensionInterfaceFunctionPtr, + crate::#func_ptr_ty + >(get_proc_address(crate::c_str(#name_str))), + }; + + fptr_decls.push(decl); + fptr_inits.push(init); + } + + // Do not derive Copy -- even though the struct is bitwise-copyable, this is rarely needed and may point to an error. + let code = quote! { + #[path = "../compat/compat_4_1.rs"] + mod compat_4_1; + + pub use compat_4_1::InitCompat; + + pub struct GDExtensionInterface { + #( #fptr_decls )* + } + + impl GDExtensionInterface { + pub(crate) unsafe fn load( + get_proc_address: crate::GDExtensionInterfaceGetProcAddress, + ) -> Self { + let get_proc_address = get_proc_address.expect("invalid get_proc_address function pointer"); + + Self { + #( #fptr_inits )* + } + } + } + }; + code +} + +fn parse_function_pointers(header_code: &str) -> Vec { + // See https://docs.rs/regex/latest/regex for docs. + let regex = Regex::new( + r#"(?xms) + # x: ignore whitespace and allow line comments (starting with `#`) + # m: multi-line mode, ^ and $ match start and end of line + # s: . matches newlines; would otherwise require (:?\n|\r\n|\r) + ^ + # Start of comment /** + /\*\* + # followed by any characters + [^*].*? + # Identifier @name variant_can_convert + @name\s(?P[a-z0-9_]+) + (?P + .+? + ) + #(?:@param\s([a-z0-9_]+))*? + #(?:\n|.)+? + # End of comment */ + \*/ + .+? + # Return type: typedef GDExtensionBool + # or pointers with space: typedef void * + #typedef\s[A-Za-z0-9_]+?\s\*? + typedef\s[^(]+? + # Function pointer: (*GDExtensionInterfaceVariantCanConvert) + \(\*(?P[A-Za-z0-9_]+?)\) + # Parameters: (GDExtensionVariantType p_from, GDExtensionVariantType p_to); + .+?; + # $ omitted, because there can be comments after `;` + "#, + ) + .unwrap(); + + let mut func_ptrs = vec![]; + for cap in regex.captures_iter(header_code) { + let name = cap.name("name"); + let funcptr_ty = cap.name("type"); + let doc = cap.name("doc"); + + let (Some(name), Some(funcptr_ty), Some(doc)) = (name, funcptr_ty, doc) else { + // Skip unparseable ones, instead of breaking build (could just be a /** */ comment around something else) + continue; + }; + + func_ptrs.push(GodotFuncPtr { + name: ident(name.as_str()), + func_ptr_ty: ident(funcptr_ty.as_str()), + doc: doc.as_str().replace("\n *", "\n").trim().to_string(), + }); + } + + func_ptrs +} + +// fn doxygen_to_rustdoc(c_doc: &str) -> String { +// // Remove leading stars +// let mut doc = c_doc .replace("\n * ", "\n"); +// +// // FIXME only compile once +// let param_regex = Regex::new(r#"@p"#) +// } + +#[test] +fn test_parse_function_pointers() { + let header_code = r#" +/* INTERFACE: ClassDB Extension */ + +/** + * @name classdb_register_extension_class + * + * Registers an extension class in the ClassDB. + * + * Provided struct can be safely freed once the function returns. + * + * @param p_library A pointer the library received by the GDExtension's entry point function. + * @param p_class_name A pointer to a StringName with the class name. + * @param p_parent_class_name A pointer to a StringName with the parent class name. + * @param p_extension_funcs A pointer to a GDExtensionClassCreationInfo struct. + */ +typedef void (*GDExtensionInterfaceClassdbRegisterExtensionClass)(GDExtensionClassLibraryPtr p_library, GDExtensionConstStringNamePtr p_class_name, GDExtensionConstStringNamePtr p_parent_class_name, const GDExtensionClassCreationInfo *p_extension_funcs); + "#; + + let func_ptrs = parse_function_pointers(header_code); + assert_eq!(func_ptrs.len(), 1); + + let func_ptr = &func_ptrs[0]; + assert_eq!( + func_ptr.name.to_string(), + "classdb_register_extension_class" + ); + + assert_eq!( + func_ptr.func_ptr_ty.to_string(), + "GDExtensionInterfaceClassdbRegisterExtensionClass" + ); + + assert_eq!( + func_ptr.doc, + r#" + Registers an extension class in the ClassDB. + + Provided struct can be safely freed once the function returns. + + @param p_library A pointer the library received by the GDExtension's entry point function. + @param p_class_name A pointer to a StringName with the class name. + @param p_parent_class_name A pointer to a StringName with the parent class name. + @param p_extension_funcs A pointer to a GDExtensionClassCreationInfo struct. + "# + .trim() + ); +} diff --git a/godot-codegen/src/lib.rs b/godot-codegen/src/lib.rs index 5dd956bf5..e95159a2b 100644 --- a/godot-codegen/src/lib.rs +++ b/godot-codegen/src/lib.rs @@ -8,6 +8,7 @@ mod api_parser; mod central_generator; mod class_generator; mod context; +mod interface_generator; mod special_cases; mod util; mod utilities_generator; @@ -24,6 +25,7 @@ use class_generator::{ generate_builtin_class_files, generate_class_files, generate_native_structures_files, }; use context::Context; +use interface_generator::generate_sys_interface_file; use util::{ident, to_pascal_case, to_snake_case}; use utilities_generator::generate_utilities_file; @@ -31,7 +33,11 @@ use proc_macro2::{Ident, TokenStream}; use quote::{quote, ToTokens}; use std::path::{Path, PathBuf}; -pub fn generate_sys_files(sys_gen_path: &Path, watch: &mut godot_bindings::StopWatch) { +pub fn generate_sys_files( + sys_gen_path: &Path, + h_path: &Path, + watch: &mut godot_bindings::StopWatch, +) { let mut out_files = vec![]; generate_sys_mod_file(sys_gen_path, &mut out_files); @@ -43,6 +49,10 @@ pub fn generate_sys_files(sys_gen_path: &Path, watch: &mut godot_bindings::StopW generate_sys_central_file(&api, &mut ctx, build_config, sys_gen_path, &mut out_files); watch.record("generate_central_file"); + let is_godot_4_0 = api.header.version_major == 4 && api.header.version_minor == 0; + generate_sys_interface_file(h_path, sys_gen_path, is_godot_4_0, &mut out_files); + watch.record("generate_interface_file"); + rustfmt_if_needed(out_files); watch.record("rustfmt"); } diff --git a/godot-codegen/src/util.rs b/godot-codegen/src/util.rs index 02b05b535..becb17b06 100644 --- a/godot-codegen/src/util.rs +++ b/godot-codegen/src/util.rs @@ -306,6 +306,11 @@ fn to_rust_type_uncached(ty: &str, ctx: &mut Context) -> RustTy { if is_const { ty = ty.replace("const ", ""); } + // .trim() is necessary here, as the Godot extension API + // places a space between a type and its stars if it's a + // double pointer. That is, Godot writes "int*" but, if it's a + // double pointer, then it writes "int **" instead (with a + // space in the middle). let inner_type = to_rust_type(ty.trim(), ctx); return RustTy::RawPointer { inner: Box::new(inner_type), diff --git a/godot-core/Cargo.toml b/godot-core/Cargo.toml index a98e4982e..a606a6687 100644 --- a/godot-core/Cargo.toml +++ b/godot-core/Cargo.toml @@ -9,7 +9,7 @@ categories = ["game-engines", "graphics"] [features] default = [] -trace = [] +trace = ["godot-ffi/trace"] codegen-fmt = ["godot-ffi/codegen-fmt", "godot-codegen/codegen-fmt"] codegen-full = ["godot-codegen/codegen-full"] double-precision = ["godot-codegen/double-precision"] diff --git a/godot-core/src/builtin/array.rs b/godot-core/src/builtin/array.rs index 839c68a4a..55a102b06 100644 --- a/godot-core/src/builtin/array.rs +++ b/godot-core/src/builtin/array.rs @@ -713,10 +713,11 @@ impl ToVariant for Array { impl FromVariant for Array { fn try_from_variant(variant: &Variant) -> Result { if variant.get_type() != Self::variant_type() { - return Err(VariantConversionError); + return Err(VariantConversionError::BadType); } + let array = unsafe { - Self::from_sys_init_default(|self_ptr| { + Self::from_sys_init(|self_ptr| { let array_from_variant = sys::builtin_fn!(array_from_variant); array_from_variant(self_ptr, variant.var_sys()); }) diff --git a/godot-core/src/builtin/callable.rs b/godot-core/src/builtin/callable.rs index fcbea123d..ed9e9e6ee 100644 --- a/godot-core/src/builtin/callable.rs +++ b/godot-core/src/builtin/callable.rs @@ -45,7 +45,7 @@ impl Callable { // upcast not needed let method = method_name.into(); unsafe { - Self::from_sys_init_default(|self_ptr| { + Self::from_sys_init(|self_ptr| { let ctor = sys::builtin_fn!(callable_from_object_method); let args = [object.sys_const(), method.sys_const()]; ctor(self_ptr, args.as_ptr()); diff --git a/godot-core/src/builtin/dictionary.rs b/godot-core/src/builtin/dictionary.rs index eabfe2822..1bd3cb90d 100644 --- a/godot-core/src/builtin/dictionary.rs +++ b/godot-core/src/builtin/dictionary.rs @@ -14,7 +14,7 @@ use std::fmt; use std::marker::PhantomData; use std::ptr::addr_of_mut; use sys::types::OpaqueDictionary; -use sys::{ffi_methods, interface_fn, GodotFfi}; +use sys::{ffi_methods, interface_fn, AsUninit, GodotFfi}; use super::VariantArray; @@ -399,28 +399,28 @@ impl<'a> DictionaryIter<'a> { } fn call_init(dictionary: &Dictionary) -> Option { - // SAFETY: - // `dictionary` is a valid `Dictionary` since we have a reference to it, - // so this will call the implementation for dictionaries. - // `variant` is an initialized and valid `Variant`. let variant: Variant = Variant::nil(); - unsafe { Self::ffi_iterate(interface_fn!(variant_iter_init), dictionary, variant) } + let iter_fn = |dictionary, next_value: sys::GDExtensionVariantPtr, valid| unsafe { + interface_fn!(variant_iter_init)(dictionary, next_value.as_uninit(), valid) + }; + + Self::ffi_iterate(iter_fn, dictionary, variant) } fn call_next(dictionary: &Dictionary, last_key: Variant) -> Option { - // SAFETY: - // `dictionary` is a valid `Dictionary` since we have a reference to it, - // so this will call the implementation for dictionaries. - // `last_key` is an initialized and valid `Variant`, since we own a copy of it. - unsafe { Self::ffi_iterate(interface_fn!(variant_iter_next), dictionary, last_key) } + let iter_fn = |dictionary, next_value, valid| unsafe { + interface_fn!(variant_iter_next)(dictionary, next_value, valid) + }; + + Self::ffi_iterate(iter_fn, dictionary, last_key) } /// Calls the provided Godot FFI function, in order to iterate the current state. /// /// # Safety: /// `iter_fn` must point to a valid function that interprets the parameters according to their type specification. - unsafe fn ffi_iterate( - iter_fn: unsafe extern "C" fn( + fn ffi_iterate( + iter_fn: unsafe fn( sys::GDExtensionConstVariantPtr, sys::GDExtensionVariantPtr, *mut sys::GDExtensionBool, @@ -429,14 +429,20 @@ impl<'a> DictionaryIter<'a> { next_value: Variant, ) -> Option { let dictionary = dictionary.to_variant(); - let mut valid: u8 = 0; - - let has_next = iter_fn( - dictionary.var_sys(), - next_value.var_sys(), - addr_of_mut!(valid), - ); - let valid = super::u8_to_bool(valid); + let mut valid_u8: u8 = 0; + + // SAFETY: + // `dictionary` is a valid `Dictionary` since we have a reference to it, + // so this will call the implementation for dictionaries. + // `last_key` is an initialized and valid `Variant`, since we own a copy of it. + let has_next = unsafe { + iter_fn( + dictionary.var_sys(), + next_value.var_sys(), + addr_of_mut!(valid_u8), + ) + }; + let valid = super::u8_to_bool(valid_u8); let has_next = super::u8_to_bool(has_next); if has_next { diff --git a/godot-core/src/builtin/macros.rs b/godot-core/src/builtin/macros.rs index 4faa2e99e..26e4214ce 100644 --- a/godot-core/src/builtin/macros.rs +++ b/godot-core/src/builtin/macros.rs @@ -11,18 +11,11 @@ macro_rules! impl_builtin_traits_inner { impl Default for $Type { #[inline] fn default() -> Self { - // Note: can't use from_sys_init(), as that calls the default constructor - // (because most assignments expect initialized target type) - - let mut uninit = std::mem::MaybeUninit::<$Type>::uninit(); - unsafe { - let self_ptr = (*uninit.as_mut_ptr()).sys_mut(); - sys::builtin_call! { - $gd_method(self_ptr, std::ptr::null_mut()) - }; - - uninit.assume_init() + Self::from_sys_init(|self_ptr| { + let ctor = ::godot_ffi::builtin_fn!($gd_method); + ctor(self_ptr, std::ptr::null_mut()) + }) } } } @@ -33,7 +26,7 @@ macro_rules! impl_builtin_traits_inner { #[inline] fn clone(&self) -> Self { unsafe { - Self::from_sys_init_default(|self_ptr| { + Self::from_sys_init(|self_ptr| { let ctor = ::godot_ffi::builtin_fn!($gd_method); let args = [self.sys_const()]; ctor(self_ptr, args.as_ptr()); @@ -162,6 +155,7 @@ macro_rules! impl_builtin_froms { $(impl From<&$From> for $To { fn from(other: &$From) -> Self { unsafe { + // TODO should this be from_sys_init_default()? Self::from_sys_init(|ptr| { let args = [other.sys_const()]; ::godot_ffi::builtin_call! { diff --git a/godot-core/src/builtin/meta/signature.rs b/godot-core/src/builtin/meta/signature.rs index c759ed639..6c72f96af 100644 --- a/godot-core/src/builtin/meta/signature.rs +++ b/godot-core/src/builtin/meta/signature.rs @@ -9,14 +9,14 @@ use godot_ffi::VariantType; use std::fmt::Debug; #[doc(hidden)] -pub trait SignatureTuple { - type Params; - type Ret; - +pub trait VarcallSignatureTuple: PtrcallSignatureTuple { fn variant_type(index: i32) -> VariantType; fn property_info(index: i32, param_name: &str) -> PropertyInfo; fn param_metadata(index: i32) -> sys::GDExtensionClassMethodArgumentMetadata; + // TODO(uninit) - can we use this for varcall/ptrcall? + // ret: sys::GDExtensionUninitializedVariantPtr + // ret: sys::GDExtensionUninitializedTypePtr unsafe fn varcall( instance_ptr: sys::GDExtensionClassInstancePtr, args_ptr: *const sys::GDExtensionConstVariantPtr, @@ -25,6 +25,12 @@ pub trait SignatureTuple { func: fn(sys::GDExtensionClassInstancePtr, Self::Params) -> Self::Ret, method_name: &str, ); +} + +#[doc(hidden)] +pub trait PtrcallSignatureTuple { + type Params; + type Ret; // Note: this method imposes extra bounds on GodotFfi, which may not be implemented for user types. // We could fall back to varcalls in such cases, and not require GodotFfi categorically. @@ -58,19 +64,16 @@ use crate::builtin::meta::*; use crate::builtin::{FromVariant, ToVariant, Variant}; use crate::obj::GodotClass; -macro_rules! impl_signature_for_tuple { +macro_rules! impl_varcall_signature_for_tuple { ( $R:ident $(, $Pn:ident : $n:literal)* ) => { #[allow(unused_variables)] - impl<$R, $($Pn,)*> SignatureTuple for ($R, $($Pn,)*) + impl<$R, $($Pn,)*> VarcallSignatureTuple for ($R, $($Pn,)*) where $R: VariantMetadata + ToVariant + sys::GodotFuncMarshal + Debug, $( $Pn: VariantMetadata + FromVariant + sys::GodotFuncMarshal + Debug, )* { - type Params = ($($Pn,)*); - type Ret = $R; - #[inline] fn variant_type(index: i32) -> sys::VariantType { match index { @@ -122,8 +125,23 @@ macro_rules! impl_signature_for_tuple { varcall_return::<$R>(func(instance_ptr, args), ret, err) } + } + }; +} + +macro_rules! impl_ptrcall_signature_for_tuple { + ( + $R:ident + $(, $Pn:ident : $n:literal)* + ) => { + #[allow(unused_variables)] + impl<$R, $($Pn,)*> PtrcallSignatureTuple for ($R, $($Pn,)*) + where $R: sys::GodotFuncMarshal + Debug, + $( $Pn: sys::GodotFuncMarshal + Debug, )* + { + type Params = ($($Pn,)*); + type Ret = $R; - #[inline] unsafe fn ptrcall( instance_ptr: sys::GDExtensionClassInstancePtr, args_ptr: *const sys::GDExtensionConstTypePtr, @@ -168,7 +186,7 @@ unsafe fn varcall_arg( /// - It must be safe to write a `sys::GDExtensionCallError` once to `err`. unsafe fn varcall_return( ret_val: R, - ret: sys::GDExtensionConstVariantPtr, + ret: sys::GDExtensionVariantPtr, err: *mut sys::GDExtensionCallError, ) { let ret_variant = ret_val.to_variant(); // TODO write_sys @@ -219,14 +237,25 @@ fn return_error(method_name: &str, arg: &impl Debug) -> ! { panic!("{method_name}: return type {return_ty} is unable to store value {arg:?}",); } -impl_signature_for_tuple!(R); -impl_signature_for_tuple!(R, P0: 0); -impl_signature_for_tuple!(R, P0: 0, P1: 1); -impl_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2); -impl_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2, P3: 3); -impl_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2, P3: 3, P4: 4); -impl_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2, P3: 3, P4: 4, P5: 5); -impl_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2, P3: 3, P4: 4, P5: 5, P6: 6); -impl_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2, P3: 3, P4: 4, P5: 5, P6: 6, P7: 7); -impl_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2, P3: 3, P4: 4, P5: 5, P6: 6, P7: 7, P8: 8); -impl_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2, P3: 3, P4: 4, P5: 5, P6: 6, P7: 7, P8: 8, P9: 9); +impl_varcall_signature_for_tuple!(R); +impl_ptrcall_signature_for_tuple!(R); +impl_varcall_signature_for_tuple!(R, P0: 0); +impl_ptrcall_signature_for_tuple!(R, P0: 0); +impl_varcall_signature_for_tuple!(R, P0: 0, P1: 1); +impl_ptrcall_signature_for_tuple!(R, P0: 0, P1: 1); +impl_varcall_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2); +impl_ptrcall_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2); +impl_varcall_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2, P3: 3); +impl_ptrcall_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2, P3: 3); +impl_varcall_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2, P3: 3, P4: 4); +impl_ptrcall_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2, P3: 3, P4: 4); +impl_varcall_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2, P3: 3, P4: 4, P5: 5); +impl_ptrcall_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2, P3: 3, P4: 4, P5: 5); +impl_varcall_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2, P3: 3, P4: 4, P5: 5, P6: 6); +impl_ptrcall_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2, P3: 3, P4: 4, P5: 5, P6: 6); +impl_varcall_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2, P3: 3, P4: 4, P5: 5, P6: 6, P7: 7); +impl_ptrcall_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2, P3: 3, P4: 4, P5: 5, P6: 6, P7: 7); +impl_varcall_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2, P3: 3, P4: 4, P5: 5, P6: 6, P7: 7, P8: 8); +impl_ptrcall_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2, P3: 3, P4: 4, P5: 5, P6: 6, P7: 7, P8: 8); +impl_varcall_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2, P3: 3, P4: 4, P5: 5, P6: 6, P7: 7, P8: 8, P9: 9); +impl_ptrcall_signature_for_tuple!(R, P0: 0, P1: 1, P2: 2, P3: 3, P4: 4, P5: 5, P6: 6, P7: 7, P8: 8, P9: 9); diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index bf4bb3be9..3e2576d2f 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -54,12 +54,13 @@ pub use string::*; pub use transform2d::*; pub use transform3d::*; pub use variant::*; -pub use vector2::*; -pub use vector2i::*; -pub use vector3::*; -pub use vector3i::*; -pub use vector4::*; -pub use vector4i::*; +pub use vectors::vector2::*; +pub use vectors::vector2i::*; +pub use vectors::vector3::*; +pub use vectors::vector3i::*; +pub use vectors::vector4::*; +pub use vectors::vector4i::*; +pub use vectors::vector_utils::*; /// Meta-information about variant types, properties and class names. pub mod meta; @@ -79,7 +80,6 @@ pub mod dictionary { // Modules exporting declarative macros must appear first. mod macros; -mod vector_macros; // Rename imports because we re-export a subset of types under same module names. #[path = "array.rs"] @@ -105,12 +105,7 @@ mod string; mod transform2d; mod transform3d; mod variant; -mod vector2; -mod vector2i; -mod vector3; -mod vector3i; -mod vector4; -mod vector4i; +mod vectors; #[doc(hidden)] pub mod inner { diff --git a/godot-core/src/builtin/plane.rs b/godot-core/src/builtin/plane.rs index df7f23472..51d2901bf 100644 --- a/godot-core/src/builtin/plane.rs +++ b/godot-core/src/builtin/plane.rs @@ -9,7 +9,9 @@ use std::ops::Neg; use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; -use super::{is_equal_approx, real, Vector3}; +use crate::builtin::math::{is_equal_approx, is_zero_approx, CMP_EPSILON}; + +use super::{real, Vector3}; /// 3D plane in [Hessian normal form](https://mathworld.wolfram.com/HessianNormalForm.html). /// @@ -17,8 +19,6 @@ use super::{is_equal_approx, real, Vector3}; /// `dot(normal, point) + d == 0`, where `normal` is the normal vector and `d` /// the distance from the origin. /// -/// Currently most methods are only available through [`InnerPlane`](super::inner::InnerPlane). -/// /// Note: almost all methods on `Plane` require that the `normal` vector have /// unit length and will panic if this invariant is violated. This is not separately /// annotated for each method. @@ -26,7 +26,9 @@ use super::{is_equal_approx, real, Vector3}; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(C)] pub struct Plane { + /// Normal vector pointing away from the plane. pub normal: Vector3, + /// Distance between the plane and the origin point. pub d: real, } @@ -105,16 +107,125 @@ impl Plane { } } + /// Finds the shortest distance between the plane and a point. + /// + /// The distance will be positive if `point` is above the plane, and will be negative if + /// `point` is below the plane. + #[inline] + pub fn distance_to(&self, point: Vector3) -> real { + self.normal.dot(point) - self.d + } + + /// Finds the center point of the plane. + /// + /// _Godot equivalent: `Plane.get_center()`_ + #[inline] + pub fn center(&self) -> Vector3 { + self.normal * self.d + } + + /// Finds whether a point is inside the plane or not. + /// + /// A point is considered part of the plane if its distance to it is less or equal than + /// [`CMP_EPSILON`][crate::builtin::CMP_EPSILON]. + /// + /// _Godot equivalent: `Plane.has_point(Vector3 point, float tolerance=1e-05)`_ + #[inline] + #[doc(alias = "has_point")] + pub fn contains_point(&self, point: Vector3, tolerance: Option) -> bool { + let dist: real = (self.normal.dot(point) - self.d).abs(); + dist <= tolerance.unwrap_or(CMP_EPSILON) + } + + /// Finds the intersection point of three planes. + /// + /// If no intersection point is found, `None` will be returned. + #[inline] + pub fn intersect_3(&self, b: &Self, c: &Self) -> Option { + let normal0 = self.normal; + let normal1 = b.normal; + let normal2 = c.normal; + let denom: real = normal0.cross(normal1).dot(normal2); + if is_zero_approx(denom) { + return None; + } + let result = normal1.cross(normal2) * self.d + + normal2.cross(normal0) * b.d + + normal0.cross(normal1) * c.d; + Some(result / denom) + } + + /// Finds the intersection point of the plane with a ray. + /// + /// The ray starts at position `from` and has direction vector `dir`, i.e. it is unbounded in one direction. + /// + /// If no intersection is found (the ray is parallel to the plane or points away from it), `None` will be returned. + #[inline] + pub fn intersect_ray(&self, from: Vector3, dir: Vector3) -> Option { + let denom: real = self.normal.dot(dir); + if is_zero_approx(denom) { + return None; + } + let dist: real = (self.normal.dot(from) - self.d) / denom; + if dist > CMP_EPSILON { + return None; + } + Some(from - dir * dist) + } + + /// Finds the intersection point of the plane with a line segment. + /// + /// The segment starts at position 'from' and ends at position 'to', i.e. it is bounded at two directions. + /// + /// If no intersection is found (the segment is parallel to the plane or does not intersect it), `None` will be returned. + #[inline] + pub fn intersect_segment(&self, from: Vector3, to: Vector3) -> Option { + let segment = from - to; + let denom: real = self.normal.dot(segment); + if is_zero_approx(denom) { + return None; + } + let dist: real = (self.normal.dot(from) - self.d) / denom; + if !(-CMP_EPSILON..=(1.0 + CMP_EPSILON)).contains(&dist) { + return None; + } + Some(from - segment * dist) + } + + /// Finds whether the two planes are approximately equal. + /// /// Returns `true` if the two `Plane`s are approximately equal, by calling `is_equal_approx` on /// `normal` and `d` or on `-normal` and `-d`. - /// - /// _Godot equivalent: `Plane.is_equal_approx()`_ #[inline] pub fn is_equal_approx(&self, other: &Self) -> bool { (self.normal.is_equal_approx(other.normal) && is_equal_approx(self.d, other.d)) || (self.normal.is_equal_approx(-other.normal) && is_equal_approx(self.d, -other.d)) } + /// Returns `true` if the plane is finite by calling `is_finite` on `normal` and `d`. + #[inline] + pub fn is_finite(&self) -> bool { + self.normal.is_finite() && self.d.is_finite() + } + + /// Returns `true` if `point` is located above the plane. + #[inline] + pub fn is_point_over(&self, point: Vector3) -> bool { + self.normal.dot(point) > self.d + } + + /// Returns a normalized copy of the plane. + #[inline] + pub fn normalized(self) -> Self { + Plane::new(self.normal.normalized(), self.d) + } + + /// Returns the orthogonal projection of `point` to the plane. + #[inline] + pub fn project(&self, point: Vector3) -> Vector3 { + point - self.normal * self.distance_to(point) + } + #[inline] fn assert_normalized(self) { assert!( @@ -157,6 +268,9 @@ impl std::fmt::Display for Plane { #[cfg(test)] mod test { + use crate::assert_eq_approx; + use crate::assert_ne_approx; + use super::*; /// Tests that none of the constructors panic for some simple planes. @@ -184,10 +298,390 @@ mod test { #[test] #[should_panic] fn from_points_colinear_panics() { - let _ = Plane::from_points( - Vector3::new(0.0, 0.0, 0.0), - Vector3::new(0.0, 0.0, 1.0), - Vector3::new(0.0, 0.0, 2.0), + let _ = Plane::from_points(Vector3::ZERO, Vector3::BACK, Vector3::new(0.0, 0.0, 2.0)); + } + + /// Tests `distance_to()`, `center()`, `contains_point()`, and `is_point_over()`. + #[test] + fn test_spatial_relations() { + // Random plane that passes the origin point. + let origin_plane = Plane::new(Vector3::new(1.0, 2.0, 3.0).normalized(), 0.0); + + // Parallels `origin_plane`. + let parallel_origin_high = Plane::new(Vector3::new(1.0, 2.0, 3.0).normalized(), 1.0); + let parallel_origin_low = Plane::new(Vector3::new(1.0, 2.0, 3.0).normalized(), -6.5); + + // Unrelated plane. + let unrelated = Plane::new(Vector3::new(-1.0, 6.0, -5.0).normalized(), 3.2); + + // Origin point and center of `origin_plane`. + let zero = Vector3::ZERO; + assert_eq!(origin_plane.center(), zero); + + // Center of `parallel_origin_high`. + let center_origin_high = parallel_origin_high.center(); + + // Center of `parallel_origin_low`. + let center_origin_low = parallel_origin_low.center(); + + // The origin point should be in `origin_plane`, so results in 0.0 distance. + assert!(origin_plane.contains_point(zero, None)); + assert_eq!(origin_plane.distance_to(zero), 0.0); + + // No matter the normals, the absolute distance to the origin point should always be the absolute + // value of the plane's `d`. + assert_eq!(origin_plane.distance_to(zero).abs(), origin_plane.d.abs()); + assert_eq!( + parallel_origin_high.distance_to(zero).abs(), + parallel_origin_high.d.abs() + ); + assert_eq!( + parallel_origin_low.distance_to(zero).abs(), + parallel_origin_low.d.abs() + ); + assert_eq!(unrelated.distance_to(zero).abs(), unrelated.d.abs()); + + // The absolute distance between a plane and its parallel's center should always be the difference + // between both the `d`s. + assert!(parallel_origin_high.contains_point(center_origin_high, None)); + assert_eq_approx!( + origin_plane.distance_to(center_origin_high).abs(), + (origin_plane.d - parallel_origin_high.d).abs(), + is_equal_approx + ); + assert!(parallel_origin_low.contains_point(center_origin_low, None)); + assert_eq_approx!( + origin_plane.distance_to(center_origin_low).abs(), + (origin_plane.d - parallel_origin_low.d).abs(), + is_equal_approx + ); + + // As `parallel_origin_high` is higher than `origin_plane` by having larger `d` value, then its center should be + // higher than `origin_plane`. + assert!(origin_plane.is_point_over(center_origin_high)); + + // As `parallel_origin_low` is lower than `origin_plane` by having smaller `d` value, then its center should be + // lower than `origin_plane`. + assert!(!origin_plane.is_point_over(center_origin_low)); + + // By the reasonings stated above, then the following should be correct. + assert!(!parallel_origin_high.is_point_over(zero)); + assert!(!parallel_origin_high.is_point_over(center_origin_low)); + assert!(parallel_origin_low.is_point_over(zero)); + assert!(parallel_origin_low.is_point_over(center_origin_high)); + } + + /// Tests `intersect_3()`. + #[test] + fn test_three_planes_intersections() { + // Planes that intersects in (0.0, 0.0, 0.0). + let origin_plane_a = Plane::new(Vector3::new(1.0, 2.0, 0.0).normalized(), 0.0); + let origin_plane_b = Plane::new(Vector3::new(3.5, 6.0, -3.0).normalized(), 0.0); + let origin_plane_c = Plane::new(Vector3::new(-1.0, 6.0, 0.5).normalized(), 0.0); + + // Planes that parallels `origin_plane_a`. + let low_parallel_origin_a = Plane::new(Vector3::new(1.0, 2.0, 0.0).normalized(), 1.0); + let high_parallel_origin_a = Plane::new(Vector3::new(1.0, 2.0, 0.0).normalized(), 2.0); + + // Planes that intersects `origin_plane_a` and each other in a common line. + let small_rotation_origin_a = + Plane::new(origin_plane_a.normal.rotated(Vector3::BACK, 30.0), 0.0); + let large_rotation_origin_a = + Plane::new(origin_plane_a.normal.rotated(Vector3::BACK, 60.0), 0.0); + + // Planes that intersects each other in 3 parallel lines. + let prism_plane_a = Plane::new(Vector3::new(2.5, -6.0, 0.0).normalized(), 1.0); + let prism_plane_b = Plane::new(prism_plane_a.normal.rotated(Vector3::BACK, 30.0), 1.0); + let prism_plane_c = Plane::new(prism_plane_a.normal.rotated(Vector3::BACK, 60.0), 1.0); + + // Origin point. + let vec_a = Vector3::ZERO; + + // Planes that have 0 as its `d` would intersect in the origin point. + assert_eq!( + origin_plane_a.intersect_3(&origin_plane_b, &origin_plane_c), + Some(vec_a) + ); + + // Three planes that parallel each other would not intersect in a point. + assert_eq!( + origin_plane_a.intersect_3(&low_parallel_origin_a, &high_parallel_origin_a), + None + ); + + // Two planes that parallel each other with an unrelated third plane would not intersect in + // a point. + assert_eq!( + origin_plane_b.intersect_3(&low_parallel_origin_a, &high_parallel_origin_a), + None + ); + + // Three coincident planes would intersect in every point, thus no unique solution. + assert_eq!( + origin_plane_a.intersect_3(&origin_plane_a, &origin_plane_a), + None + ); + + // Two coincident planes with an unrelated third plane would intersect in every point along the + // intersection line, thus no unique solution. + assert_eq!( + origin_plane_b.intersect_3(&origin_plane_b, &large_rotation_origin_a), + None + ); + + // Two coincident planes with a parallel third plane would have no common intersection. + assert_eq!( + origin_plane_a.intersect_3(&origin_plane_a, &low_parallel_origin_a), + None + ); + + // Three planes that intersects each other in a common line would intersect in every point along + // the line, thus no unique solution. + assert_eq!( + origin_plane_a.intersect_3(&small_rotation_origin_a, &large_rotation_origin_a), + None + ); + + // Three planes that intersects each other in 3 parallel lines would not intersect in a common + // point. + assert_eq!( + prism_plane_a.intersect_3(&prism_plane_b, &prism_plane_c), + None + ); + } + + /// Tests `intersect_ray()`. + #[test] + fn test_ray_intersections() { + // Plane that is flat along the z-axis. + let xy_plane = Plane::new(Vector3::BACK, 0.0); + + // Origin point. + let zero = Vector3::ZERO; + + // Forms a straight line along the z-axis with `zero` that is perpendicular to plane. + let low_pos_z = Vector3::new(0.0, 0.0, 0.5); + let high_pos_z = Vector3::BACK; + let neg_z = Vector3::FORWARD; + + // Forms a slanted line with `zero` relative to plane. + let pos_xy = Vector3::new(0.5, 0.5, 0.0); + + // Forms a line with `high_pos_z` that is parallel with plane. + let pos_xz = Vector3::new(1.0, 0.0, 1.0); + + // From a point straight up from the origin point, a ray pointing straight down would cross + // the plane in the origin point. + assert_eq!(xy_plane.intersect_ray(low_pos_z, neg_z), Some(zero)); + assert_eq!(xy_plane.intersect_ray(high_pos_z, neg_z), Some(zero)); + + // From a point straight down the origin point, a ray pointing straight up would cross the plane + // in the origin point. + assert_eq!(xy_plane.intersect_ray(neg_z, low_pos_z), Some(zero)); + assert_eq!(xy_plane.intersect_ray(neg_z, high_pos_z), Some(zero)); + + // A ray parallel to the plane would not intersect the plane. + assert_eq!(xy_plane.intersect_ray(high_pos_z, pos_xz), None); + + // A ray pointing to the opposite direction as the plane would not intersect it. + assert_eq!(xy_plane.intersect_ray(low_pos_z, high_pos_z), None); + assert_eq!(xy_plane.intersect_ray(low_pos_z, pos_xy), None); + } + + /// Tests `intersect_segment()`. + #[test] + fn test_segment_intersections() { + // Plane that is flat along the z-axis. + let xy_plane = Plane::new(Vector3::BACK, 0.0); + + // Origin point. + let zero = Vector3::ZERO; + + // Forms a straight line along the z-axis with `zero` that is perpendicular to plane. + let low_pos_z = Vector3::new(0.0, 0.0, 0.5); + let high_pos_z = Vector3::BACK; + let low_neg_z = Vector3::FORWARD; + let high_neg_z = Vector3::new(0.0, 0.0, -0.5); + + // Forms a line with `high_pos_z` that is parallel with plane. + let pos_xz = Vector3::new(1.0, 0.0, 1.0); + + // From a point straight up from the origin point, a segment pointing straight down would cross + // the plane in the origin point only if the segment ended on or beyond the plane. + assert_eq!(xy_plane.intersect_segment(low_pos_z, low_neg_z), Some(zero)); + assert_eq!( + xy_plane.intersect_segment(high_pos_z, low_neg_z), + Some(zero) + ); + assert_eq!(xy_plane.intersect_segment(low_pos_z, zero), Some(zero)); + assert_eq!(xy_plane.intersect_segment(high_pos_z, zero), Some(zero)); + assert_eq!(xy_plane.intersect_segment(high_pos_z, low_pos_z), None); + + // From a point straight down the origin point, a segment pointing straight up would cross the plane + // in the origin point only if the segment ended on or beyond the plane. + assert_eq!(xy_plane.intersect_segment(low_neg_z, zero), Some(zero)); + assert_eq!(xy_plane.intersect_segment(low_neg_z, low_pos_z), Some(zero)); + assert_eq!( + xy_plane.intersect_segment(low_neg_z, high_pos_z), + Some(zero) + ); + assert_eq!(xy_plane.intersect_segment(low_neg_z, high_neg_z), None); + + // A segment parallel to the plane would not intersect the plane. + assert_eq!(xy_plane.intersect_segment(high_pos_z, pos_xz), None); + + // A segment pointing to the opposite direction as the plane would not intersect it. + assert_eq!(xy_plane.intersect_segment(low_pos_z, high_pos_z), None); + assert_eq!(xy_plane.intersect_segment(low_pos_z, pos_xz), None); + } + + /// Tests `is_equal_approx()`. + #[test] + fn test_equal() { + // Initial planes. + let xy_plane = Plane::new(Vector3::BACK, 0.0); + let almost_xy_plane_a = Plane::new(Vector3::new(0.01, 0.0, 1.0).normalized(), 0.0); + let almost_xy_plane_b = Plane::new(Vector3::new(0.0001, 0.0, 1.0).normalized(), 0.0); + let almost_xy_plane_c = Plane::new(Vector3::new(0.000001, 0.0, 1.0).normalized(), 0.0); + let approx_xy_plane_a = Plane::new(Vector3::new(0.000001, 0.0, 1.0).normalized(), 0.01); + let approx_xy_plane_b = Plane::new(Vector3::new(0.000001, 0.0, 1.0).normalized(), 0.000001); + + // Same planes should be equals. + assert_eq_approx!(&xy_plane, &xy_plane, Plane::is_equal_approx); + assert_eq_approx!( + &almost_xy_plane_a, + &almost_xy_plane_a, + Plane::is_equal_approx + ); + + // Planes below should be approximately equal because it's lower than the set tolerance constant. + assert_eq_approx!(&xy_plane, &almost_xy_plane_c, Plane::is_equal_approx); + + // Both attributes are approximately equal. + assert_eq_approx!(&xy_plane, &approx_xy_plane_b, Plane::is_equal_approx); + + // Although similar, planes below are not approximately equals. + assert_ne_approx!(&xy_plane, &almost_xy_plane_a, Plane::is_equal_approx); + assert_ne_approx!(&xy_plane, &almost_xy_plane_b, Plane::is_equal_approx); + + // Although approximately equal in the `normal` part, it is not approximately equal in the `d` + // part. + assert_ne_approx!(&xy_plane, &approx_xy_plane_a, Plane::is_equal_approx); + + // Although considered approximately equal with `xy_plane`, `almost_xy_plane_a` is not considered approximately + // equal with `almost_xy_plane_d` because the baseline comparison is tighter. + assert_ne_approx!( + &almost_xy_plane_a, + &approx_xy_plane_a, + Plane::is_equal_approx + ); + } + + /// Tests `normalize()`. + #[test] + fn test_normalization() { + // Non-normalized planes. + let plane = Plane { + normal: Vector3::new(0.7, 2.0, 6.0), + d: 0.0, + }; + assert_eq!(plane.normalized().normal, plane.normal.normalized()); + + let plane = Plane { + normal: Vector3::new(1.5, 7.2, 9.1), + d: 2.0, + }; + assert_eq!(plane.normalized().normal, plane.normal.normalized()); + + let plane = Plane { + normal: Vector3::new(1.4, 9.1, 1.2), + d: 5.3, + }; + assert_eq!(plane.normalized().normal, plane.normal.normalized()); + let plane = Plane { + normal: Vector3::new(4.2, 2.9, 1.5), + d: 2.4, + }; + assert_eq!(plane.normalized().normal, plane.normal.normalized()); + + // Normalized plane. + let plane = Plane { + normal: Vector3::new(5.1, 3.0, 2.1).normalized(), + d: 0.2, + }; + assert_eq!(plane.normalized().normal, plane.normal.normalized()); + } + + /// Tests `is_finite()`. + #[test] + fn test_finite() { + // Non-finite planes. + let plane = Plane { + normal: Vector3::new(0.7, real::INFINITY, -6.0), + d: 10.2, + }; + assert!(!plane.is_finite()); + + let plane = Plane { + normal: Vector3::new(0.7, 2.0, real::NEG_INFINITY), + d: 10.2, + }; + assert!(!plane.is_finite()); + + let plane = Plane { + normal: Vector3::new(0.7, real::INFINITY, -6.0), + d: real::INFINITY, + }; + assert!(!plane.is_finite()); + + let plane = Plane { + normal: Vector3::new(real::NAN, real::INFINITY, real::NEG_INFINITY), + d: real::NAN, + }; + assert!(!plane.is_finite()); + + // Finite plane. + let plane = Plane { + normal: Vector3::new(7.2, -2.9, 2.2).normalized(), + d: 3.3, + }; + assert!(plane.is_finite()); + } + + /// Tests `project()` and `center()`. + #[test] + fn test_projection() { + // Plane that is flat along the z-axis. + let xy_plane = Plane::new(Vector3::BACK, 0.0); + + // Parallels `xy_plane` + let parallel_xy_plane = Plane::new(Vector3::BACK, 3.5); + + // Random vectors. + let random_a = Vector3::new(0.0, 3.2, 1.5); + let random_b = Vector3::new(1.1, 7.3, -6.4); + let random_c = Vector3::new(0.5, -7.2, 0.2); + + // Projection of points to `xy_plane` would result in the same points, with the z aspect of the + // vector be 0.0. + assert_eq!( + xy_plane.project(random_a), + Vector3::new(random_a.x, random_a.y, 0.0) + ); + assert_eq!( + xy_plane.project(random_b), + Vector3::new(random_b.x, random_b.y, 0.0) + ); + assert_eq!( + xy_plane.project(random_c), + Vector3::new(random_c.x, random_c.y, 0.0) + ); + + // Projection of the center of a plane that parallels the plane that is being projected into + // is going to be the center of the plane that is being projected. + assert_eq!( + xy_plane.project(parallel_xy_plane.center()), + xy_plane.center() ); } diff --git a/godot-core/src/builtin/quaternion.rs b/godot-core/src/builtin/quaternion.rs index 6b0da2234..48a587a5e 100644 --- a/godot-core/src/builtin/quaternion.rs +++ b/godot-core/src/builtin/quaternion.rs @@ -9,7 +9,7 @@ use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; use crate::builtin::glam_helpers::{GlamConv, GlamType}; -use crate::builtin::{inner, math::*, vector3::*}; +use crate::builtin::{inner, math::*, Vector3}; use super::{real, RQuat}; use super::{Basis, EulerOrder}; @@ -181,7 +181,7 @@ impl Quaternion { } // pub fn spherical_cubic_interpolate(self, b: Self, pre_a: Self, post_b: Self, weight: real) -> Self {} - // TODO: Implement godot's function in rust + // TODO: Implement godot's function in Rust /* pub fn spherical_cubic_interpolate_in_time( self, diff --git a/godot-core/src/builtin/string/godot_string.rs b/godot-core/src/builtin/string/godot_string.rs index 8451583e9..8edf3b474 100644 --- a/godot-core/src/builtin/string/godot_string.rs +++ b/godot-core/src/builtin/string/godot_string.rs @@ -157,7 +157,7 @@ impl fmt::Debug for GodotString { } // ---------------------------------------------------------------------------------------------------------------------------------------------- -// Conversion from/into rust string-types +// Conversion from/into Rust string-types impl From for GodotString where @@ -223,7 +223,7 @@ impl FromStr for GodotString { impl From<&StringName> for GodotString { fn from(string: &StringName) -> Self { unsafe { - Self::from_sys_init_default(|self_ptr| { + Self::from_sys_init(|self_ptr| { let ctor = sys::builtin_fn!(string_from_string_name); let args = [string.sys_const()]; ctor(self_ptr, args.as_ptr()); @@ -244,7 +244,7 @@ impl From for GodotString { impl From<&NodePath> for GodotString { fn from(path: &NodePath) -> Self { unsafe { - Self::from_sys_init_default(|self_ptr| { + Self::from_sys_init(|self_ptr| { let ctor = sys::builtin_fn!(string_from_node_path); let args = [path.sys_const()]; ctor(self_ptr, args.as_ptr()); diff --git a/godot-core/src/builtin/string/node_path.rs b/godot-core/src/builtin/string/node_path.rs index c80691490..ff4c7ac88 100644 --- a/godot-core/src/builtin/string/node_path.rs +++ b/godot-core/src/builtin/string/node_path.rs @@ -101,7 +101,7 @@ impl_rust_string_conv!(NodePath); impl From<&GodotString> for NodePath { fn from(string: &GodotString) -> Self { unsafe { - Self::from_sys_init_default(|self_ptr| { + Self::from_sys_init(|self_ptr| { let ctor = sys::builtin_fn!(node_path_from_string); let args = [string.sys_const()]; ctor(self_ptr, args.as_ptr()); diff --git a/godot-core/src/builtin/string/string_name.rs b/godot-core/src/builtin/string/string_name.rs index 4a4998d32..b35b467a8 100644 --- a/godot-core/src/builtin/string/string_name.rs +++ b/godot-core/src/builtin/string/string_name.rs @@ -121,7 +121,6 @@ impl fmt::Debug for StringName { write!(f, "&\"{string}\"") } } - // ---------------------------------------------------------------------------------------------------------------------------------------------- // Conversion from/into other string-types @@ -130,7 +129,7 @@ impl_rust_string_conv!(StringName); impl From<&GodotString> for StringName { fn from(string: &GodotString) -> Self { unsafe { - Self::from_sys_init_default(|self_ptr| { + Self::from_sys_init(|self_ptr| { let ctor = sys::builtin_fn!(string_name_from_string); let args = [string.sys_const()]; ctor(self_ptr, args.as_ptr()); diff --git a/godot-core/src/builtin/variant/impls.rs b/godot-core/src/builtin/variant/impls.rs index 4442a3811..f108d585f 100644 --- a/godot-core/src/builtin/variant/impls.rs +++ b/godot-core/src/builtin/variant/impls.rs @@ -58,7 +58,7 @@ macro_rules! impl_variant_traits { fn try_from_variant(variant: &Variant) -> Result { // Type check -- at the moment, a strict match is required. if variant.get_type() != Self::variant_type() { - return Err(VariantConversionError) + return Err(VariantConversionError::BadType) } // In contrast to T -> Variant, the conversion Variant -> T assumes @@ -67,7 +67,7 @@ macro_rules! impl_variant_traits { // does a copy-on-write and explodes if this->_cowdata is not initialized. // We can thus NOT use Self::from_sys_init(). let result = unsafe { - Self::from_sys_init_default(|self_ptr| { + Self::from_sys_init(|self_ptr| { let converter = sys::builtin_fn!($to_fn); converter(self_ptr, variant.var_sys()); }) @@ -92,7 +92,7 @@ macro_rules! impl_variant_traits_int { impl FromVariant for $T { fn try_from_variant(v: &Variant) -> Result { i64::try_from_variant(v) - .and_then(|i| <$T>::try_from(i).map_err(|_e| VariantConversionError)) + .and_then(|i| <$T>::try_from(i).map_err(|_e| VariantConversionError::BadType)) } } @@ -251,7 +251,7 @@ impl ToVariant for T { impl FromVariant for T { fn try_from_variant(variant: &Variant) -> Result { ::try_from_variant(variant) - .and_then(|int| Self::try_from_ord(int).ok_or(VariantConversionError)) + .and_then(|int| Self::try_from_ord(int).ok_or(VariantConversionError::BadType)) } } @@ -263,71 +263,3 @@ impl VariantMetadata for T { sys::GDEXTENSION_METHOD_ARGUMENT_METADATA_INT_IS_INT32 } } - -impl ToVariant for *mut T { - fn to_variant(&self) -> Variant { - (*self as i64).to_variant() - } -} - -impl ToVariant for *const T { - fn to_variant(&self) -> Variant { - (*self as i64).to_variant() - } -} - -impl FromVariant for *mut T { - fn try_from_variant(variant: &Variant) -> Result { - let n = i64::try_from_variant(variant)?; - Ok(n as Self) - } -} - -impl FromVariant for *const T { - fn try_from_variant(variant: &Variant) -> Result { - let n = i64::try_from_variant(variant)?; - Ok(n as Self) - } -} - -impl VariantMetadata for *mut T { - fn variant_type() -> VariantType { - VariantType::Int - } - - fn property_info(property_name: &str) -> PropertyInfo { - PropertyInfo { - variant_type: Self::variant_type(), - class_name: Self::class_name(), - property_name: StringName::from(property_name), - hint: global::PropertyHint::PROPERTY_HINT_INT_IS_POINTER, - hint_string: GodotString::from("pointer"), - usage: global::PropertyUsageFlags::PROPERTY_USAGE_DEFAULT, - } - } - - fn param_metadata() -> sys::GDExtensionClassMethodArgumentMetadata { - sys::GDEXTENSION_METHOD_ARGUMENT_METADATA_INT_IS_INT64 - } -} - -impl VariantMetadata for *const T { - fn variant_type() -> VariantType { - VariantType::Int - } - - fn property_info(property_name: &str) -> PropertyInfo { - PropertyInfo { - variant_type: Self::variant_type(), - class_name: Self::class_name(), - property_name: StringName::from(property_name), - hint: global::PropertyHint::PROPERTY_HINT_INT_IS_POINTER, - hint_string: GodotString::from("pointer"), - usage: global::PropertyUsageFlags::PROPERTY_USAGE_DEFAULT, - } - } - - fn param_metadata() -> sys::GDExtensionClassMethodArgumentMetadata { - sys::GDEXTENSION_METHOD_ARGUMENT_METADATA_INT_IS_INT64 - } -} diff --git a/godot-core/src/builtin/variant/mod.rs b/godot-core/src/builtin/variant/mod.rs index 0ab888b58..ae57fc25d 100644 --- a/godot-core/src/builtin/variant/mod.rs +++ b/godot-core/src/builtin/variant/mod.rs @@ -108,18 +108,17 @@ impl Variant { let args_sys: Vec<_> = args.iter().map(|v| v.var_sys_const()).collect(); let mut error = sys::default_call_error(); - #[allow(unused_mut)] - let mut result = Variant::nil(); - - unsafe { - interface_fn!(variant_call)( - self.var_sys(), - method.string_sys(), - args_sys.as_ptr(), - args_sys.len() as i64, - result.var_sys(), - ptr::addr_of_mut!(error), - ) + let result = unsafe { + Variant::from_var_sys_init(|variant_ptr| { + interface_fn!(variant_call)( + self.var_sys(), + method.string_sys(), + args_sys.as_ptr(), + args_sys.len() as i64, + variant_ptr, + ptr::addr_of_mut!(error), + ) + }) }; if error.error != sys::GDEXTENSION_CALL_OK { @@ -133,16 +132,16 @@ impl Variant { let op_sys = op.sys(); let mut is_valid = false as u8; - #[allow(unused_mut)] - let mut result = Variant::nil(); - unsafe { - interface_fn!(variant_evaluate)( - op_sys, - self.var_sys(), - rhs.var_sys(), - result.var_sys(), - ptr::addr_of_mut!(is_valid), - ) + let result = unsafe { + Variant::from_var_sys_init(|variant_ptr| { + interface_fn!(variant_evaluate)( + op_sys, + self.var_sys(), + rhs.var_sys(), + variant_ptr, + ptr::addr_of_mut!(is_valid), + ) + }) }; if is_valid == 1 { diff --git a/godot-core/src/builtin/variant/variant_traits.rs b/godot-core/src/builtin/variant/variant_traits.rs index 794acb919..542b90239 100644 --- a/godot-core/src/builtin/variant/variant_traits.rs +++ b/godot-core/src/builtin/variant/variant_traits.rs @@ -50,11 +50,14 @@ pub trait ToVariant { // ---------------------------------------------------------------------------------------------------------------------------------------------- #[derive(Eq, PartialEq, Debug)] -pub struct VariantConversionError; -/*pub enum VariantConversionError { +//pub struct VariantConversionError; +pub enum VariantConversionError { /// Variant type does not match expected type BadType, /// Variant value cannot be represented in target type BadValue, -}*/ + + /// Variant value is missing a value for the target type + MissingValue, +} diff --git a/godot-core/src/builtin/vectors/mod.rs b/godot-core/src/builtin/vectors/mod.rs new file mode 100644 index 000000000..f864b9ca1 --- /dev/null +++ b/godot-core/src/builtin/vectors/mod.rs @@ -0,0 +1,73 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +mod vector_macros; + +pub mod vector2; +pub mod vector2i; +pub mod vector3; +pub mod vector3i; +pub mod vector4; +pub mod vector4i; + +pub mod vector_utils; + +#[cfg(test)] +mod test { + use crate::builtin::*; + use crate::assert_eq_approx; + #[test] + fn test_vector_swizzle() { + // * VectorN swizzle + let vector2 = Vector2::new(1.0, 2.0); + let vector3 = Vector3::new(1.0, 2.0, 3.0); + let vector4 = Vector4::new(1.0, 2.0, 3.0, 4.0); + + // VectorN to Vector2 + + let vc2swiz2: Vector2 = swizzle!(vector2 => y, x); + let vc3swiz2: Vector2 = swizzle!(vector3 => y, x); + let vc4swiz2: Vector2 = swizzle!(vector4 => y, x); + assert_eq_approx!(Vector2::new(2.0,1.0), vc2swiz2, Vector2::is_equal_approx); + assert_eq_approx!(Vector2::new(2.0,1.0), vc3swiz2, Vector2::is_equal_approx); + assert_eq_approx!(Vector2::new(2.0,1.0), vc4swiz2, Vector2::is_equal_approx); + + // VectorN to Vector3 + + let vc2swiz3: Vector3 = swizzle!(vector2 => y, x, x); + let vc3swiz3: Vector3 = swizzle!(vector3 => y, x, z); + let vc4swiz3: Vector3 = swizzle!(vector4 => y, x, z); + assert_eq_approx!(Vector3::new(2.0,1.0,1.0), vc2swiz3, Vector3::is_equal_approx); + assert_eq_approx!(Vector3::new(2.0,1.0,3.0), vc3swiz3, Vector3::is_equal_approx); + assert_eq_approx!(Vector3::new(2.0,1.0,3.0), vc4swiz3, Vector3::is_equal_approx); + + // VectorN to Vector4 + + let vc2swiz4: Vector4 = swizzle!(vector2 => y, x, x, y); + let vc3swiz4: Vector4 = swizzle!(vector3 => y, x, z, y); + let vc4swiz4: Vector4 = swizzle!(vector4 => y, x, z, w); + assert_eq_approx!(Vector4::new(2.0,1.0,1.0,2.0), vc2swiz4, Vector4::is_equal_approx); + assert_eq_approx!(Vector4::new(2.0,1.0,3.0,2.0), vc3swiz4, Vector4::is_equal_approx); + assert_eq_approx!(Vector4::new(2.0,1.0,3.0,4.0), vc4swiz4, Vector4::is_equal_approx); + + // * VectorNi swizzle + let vector2i = Vector2i::new(1, 2); + let vector3i = Vector3i::new(1,2,3); + let vector4i = Vector4i::new(1, 2, 3, 4); + // VectorNi to Vector2i + assert_eq!(Vector2i::new(2,1), swizzle!(vector2i => y, x)); + assert_eq!(Vector2i::new(2,1), swizzle!(vector3i => y, x)); + assert_eq!(Vector2i::new(2,1), swizzle!(vector4i => y, x)); + // VectorNi to Vector3i + assert_eq!(Vector3i::new(2,1,1), swizzle!(vector2i => y, x, x)); + assert_eq!(Vector3i::new(2,1,3), swizzle!(vector3i => y, x, z)); + assert_eq!(Vector3i::new(2,1,3), swizzle!(vector4i => y, x, z)); + // VectorNi to Vector4i + assert_eq!(Vector4i::new(2,1,1,2), swizzle!(vector2i => y, x, x, y)); + assert_eq!(Vector4i::new(2,1,3,2), swizzle!(vector3i => y, x, z, y)); + assert_eq!(Vector4i::new(2,1,3,4), swizzle!(vector4i => y, x, z, w)); + } +} diff --git a/godot-core/src/builtin/vector2.rs b/godot-core/src/builtin/vectors/vector2.rs similarity index 94% rename from godot-core/src/builtin/vector2.rs rename to godot-core/src/builtin/vectors/vector2.rs index 42ba1d549..4529c0108 100644 --- a/godot-core/src/builtin/vector2.rs +++ b/godot-core/src/builtin/vectors/vector2.rs @@ -12,9 +12,10 @@ use sys::{ffi_methods, GodotFfi}; use crate::builtin::math::*; use crate::builtin::{inner, Vector2i}; -use super::glam_helpers::GlamConv; -use super::glam_helpers::GlamType; -use super::{real, RAffine2, RVec2}; +use super::super::glam_helpers::GlamConv; +use super::super::glam_helpers::GlamType; +use super::super::{real, RAffine2, RVec2}; +use super::vector_utils::*; /// Vector used for 2D math using floating point coordinates. /// @@ -298,6 +299,10 @@ impl Vector2 { pub fn as_inner(&self) -> inner::InnerVector2 { inner::InnerVector2::from_outer(self) } + + pub fn coords(&self) -> (real, real) { + (self.x, self.y) + } } /// Formats the vector like Godot: `(x, y)`. @@ -310,7 +315,7 @@ impl fmt::Display for Vector2 { impl_common_vector_fns!(Vector2, real); impl_float_vector_fns!(Vector2, real); impl_vector_operators!(Vector2, real, (x, y)); -impl_vector_index!(Vector2, real, (x, y), Vector2Axis, (X, Y)); +impl_from_tuple_for_vector2x!(Vector2, real); // SAFETY: // This type is represented as `Self` in Godot, so `*mut Self` is sound. @@ -318,21 +323,8 @@ unsafe impl GodotFfi for Vector2 { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } -/// Enumerates the axes in a [`Vector2`]. -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] -#[repr(i32)] -pub enum Vector2Axis { - /// The X axis. - X, - - /// The Y axis. - Y, -} - -// SAFETY: -// This type is represented as `Self` in Godot, so `*mut Self` is sound. -unsafe impl GodotFfi for Vector2Axis { - ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +impl GlamConv for Vector2 { + type Glam = RVec2; } impl GlamType for RVec2 { @@ -347,10 +339,6 @@ impl GlamType for RVec2 { } } -impl GlamConv for Vector2 { - type Glam = RVec2; -} - #[cfg(test)] mod test { use crate::assert_eq_approx; diff --git a/godot-core/src/builtin/vector2i.rs b/godot-core/src/builtin/vectors/vector2i.rs similarity index 87% rename from godot-core/src/builtin/vector2i.rs rename to godot-core/src/builtin/vectors/vector2i.rs index ca2d1b6ad..d003cdf16 100644 --- a/godot-core/src/builtin/vector2i.rs +++ b/godot-core/src/builtin/vectors/vector2i.rs @@ -9,11 +9,10 @@ use std::fmt; use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; +use crate::builtin::glam_helpers::{GlamConv, GlamType}; +use crate::builtin::IVec2; use crate::builtin::Vector2; -use super::glam_helpers::{GlamConv, GlamType}; -use super::IVec2; - /// Vector used for 2D math using integer coordinates. /// /// 2-element structure that can be used to represent positions in 2D space or any other pair of @@ -80,6 +79,10 @@ impl Vector2i { fn to_glam(self) -> glam::IVec2 { IVec2::new(self.x, self.y) } + + pub fn coords(&self) -> (i32, i32) { + (self.x, self.y) + } } /// Formats the vector like Godot: `(x, y)`. @@ -91,7 +94,7 @@ impl fmt::Display for Vector2i { impl_common_vector_fns!(Vector2i, i32); impl_vector_operators!(Vector2i, i32, (x, y)); -impl_vector_index!(Vector2i, i32, (x, y), Vector2iAxis, (X, Y)); +impl_from_tuple_for_vector2x!(Vector2i, i32); // SAFETY: // This type is represented as `Self` in Godot, so `*mut Self` is sound. @@ -99,23 +102,6 @@ unsafe impl GodotFfi for Vector2i { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } -/// Enumerates the axes in a [`Vector2i`]. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] -#[repr(i32)] -pub enum Vector2iAxis { - /// The X axis. - X, - - /// The Y axis. - Y, -} - -// SAFETY: -// This type is represented as `Self` in Godot, so `*mut Self` is sound. -unsafe impl GodotFfi for Vector2iAxis { - ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } -} - impl GlamType for IVec2 { type Mapped = Vector2i; diff --git a/godot-core/src/builtin/vector3.rs b/godot-core/src/builtin/vectors/vector3.rs similarity index 94% rename from godot-core/src/builtin/vector3.rs rename to godot-core/src/builtin/vectors/vector3.rs index aea8e864a..c6f7fb103 100644 --- a/godot-core/src/builtin/vector3.rs +++ b/godot-core/src/builtin/vectors/vector3.rs @@ -13,9 +13,10 @@ use sys::{ffi_methods, GodotFfi}; use crate::builtin::math::*; use crate::builtin::Vector3i; -use super::glam_helpers::GlamConv; -use super::glam_helpers::GlamType; -use super::{real, Basis, RVec3}; +use super::super::glam_helpers::GlamConv; +use super::super::glam_helpers::GlamType; +use super::super::{real, Basis, RVec3}; +use super::vector_utils::*; /// Vector used for 3D math using floating point coordinates. /// @@ -318,6 +319,10 @@ impl Vector3 { assert!(axis.is_normalized()); Basis::from_axis_angle(axis, angle) * self } + + pub fn coords(&self) -> (real, real, real) { + (self.x, self.y, self.z) + } } /// Formats the vector like Godot: `(x, y, z)`. @@ -330,7 +335,7 @@ impl fmt::Display for Vector3 { impl_common_vector_fns!(Vector3, real); impl_float_vector_fns!(Vector3, real); impl_vector_operators!(Vector3, real, (x, y, z)); -impl_vector_index!(Vector3, real, (x, y, z), Vector3Axis, (X, Y, Z)); +impl_from_tuple_for_vector3x!(Vector3, real); // SAFETY: // This type is represented as `Self` in Godot, so `*mut Self` is sound. @@ -338,27 +343,6 @@ unsafe impl GodotFfi for Vector3 { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } -/// Enumerates the axes in a [`Vector3`]. -// TODO auto-generate this, alongside all the other builtin type's enums -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] -#[repr(i32)] -pub enum Vector3Axis { - /// The X axis. - X, - - /// The Y axis. - Y, - - /// The Z axis. - Z, -} - -// SAFETY: -// This type is represented as `Self` in Godot, so `*mut Self` is sound. -unsafe impl GodotFfi for Vector3Axis { - ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } -} - impl GlamType for RVec3 { type Mapped = Vector3; diff --git a/godot-core/src/builtin/vector3i.rs b/godot-core/src/builtin/vectors/vector3i.rs similarity index 87% rename from godot-core/src/builtin/vector3i.rs rename to godot-core/src/builtin/vectors/vector3i.rs index 9beba0e16..e7a4aeb7d 100644 --- a/godot-core/src/builtin/vector3i.rs +++ b/godot-core/src/builtin/vectors/vector3i.rs @@ -11,8 +11,9 @@ use sys::{ffi_methods, GodotFfi}; use crate::builtin::Vector3; -use super::glam_helpers::{GlamConv, GlamType}; -use super::IVec3; +use super::super::glam_helpers::{GlamConv, GlamType}; +use super::super::IVec3; + /// Vector used for 3D math using integer coordinates. /// @@ -90,6 +91,10 @@ impl Vector3i { fn to_glam(self) -> IVec3 { IVec3::new(self.x, self.y, self.z) } + + pub fn coords(&self) -> (i32, i32, i32) { + (self.x, self.y, self.z) + } } /// Formats the vector like Godot: `(x, y, z)`. @@ -101,7 +106,7 @@ impl fmt::Display for Vector3i { impl_common_vector_fns!(Vector3i, i32); impl_vector_operators!(Vector3i, i32, (x, y, z)); -impl_vector_index!(Vector3i, i32, (x, y, z), Vector3iAxis, (X, Y, Z)); +impl_from_tuple_for_vector3x!(Vector3i, i32); // SAFETY: // This type is represented as `Self` in Godot, so `*mut Self` is sound. @@ -109,26 +114,6 @@ unsafe impl GodotFfi for Vector3i { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } -/// Enumerates the axes in a [`Vector3i`]. -#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] -#[repr(i32)] -pub enum Vector3iAxis { - /// The X axis. - X, - - /// The Y axis. - Y, - - /// The Z axis. - Z, -} - -// SAFETY: -// This type is represented as `Self` in Godot, so `*mut Self` is sound. -unsafe impl GodotFfi for Vector3iAxis { - ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } -} - impl GlamType for IVec3 { type Mapped = Vector3i; diff --git a/godot-core/src/builtin/vector4.rs b/godot-core/src/builtin/vectors/vector4.rs similarity index 87% rename from godot-core/src/builtin/vector4.rs rename to godot-core/src/builtin/vectors/vector4.rs index bf5f00528..2e666a83e 100644 --- a/godot-core/src/builtin/vector4.rs +++ b/godot-core/src/builtin/vectors/vector4.rs @@ -11,9 +11,9 @@ use sys::{ffi_methods, GodotFfi}; use crate::builtin::math::*; use crate::builtin::Vector4i; +use super::super::glam_helpers::{GlamConv, GlamType}; +use super::super::{real, RVec4}; -use super::glam_helpers::{GlamConv, GlamType}; -use super::{real, RVec4}; /// Vector used for 4D math using floating point coordinates. /// @@ -42,9 +42,9 @@ pub struct Vector4 { } impl_vector_operators!(Vector4, real, (x, y, z, w)); -impl_vector_index!(Vector4, real, (x, y, z, w), Vector4Axis, (X, Y, Z, W)); impl_common_vector_fns!(Vector4, real); impl_float_vector_fns!(Vector4, real); +impl_from_tuple_for_vector4x!(Vector4, real); impl Vector4 { /// Returns a `Vector4` with the given components. @@ -92,6 +92,10 @@ impl Vector4 { && is_equal_approx(self.z, to.z) && is_equal_approx(self.w, to.w) } + + pub fn coords(&self) -> (real, real, real, real) { + (self.x, self.y, self.z, self.w) + } } /// Formats the vector like Godot: `(x, y, z, w)`. @@ -107,29 +111,6 @@ unsafe impl GodotFfi for Vector4 { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } -/// Enumerates the axes in a [`Vector4`]. -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] -#[repr(i32)] -pub enum Vector4Axis { - /// The X axis. - X, - - /// The Y axis. - Y, - - /// The Z axis. - Z, - - /// The W axis. - W, -} - -// SAFETY: -// This type is represented as `Self` in Godot, so `*mut Self` is sound. -unsafe impl GodotFfi for Vector4Axis { - ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } -} - impl GlamType for RVec4 { type Mapped = Vector4; diff --git a/godot-core/src/builtin/vector4i.rs b/godot-core/src/builtin/vectors/vector4i.rs similarity index 86% rename from godot-core/src/builtin/vector4i.rs rename to godot-core/src/builtin/vectors/vector4i.rs index 004d5d5f0..830650d48 100644 --- a/godot-core/src/builtin/vector4i.rs +++ b/godot-core/src/builtin/vectors/vector4i.rs @@ -10,10 +10,8 @@ use godot_ffi as sys; use sys::{ffi_methods, GodotFfi}; use crate::builtin::Vector4; - -use super::glam_helpers::{GlamConv, GlamType}; -use super::IVec4; - +use super::super::glam_helpers::{GlamConv, GlamType}; +use super::super::IVec4; /// Vector used for 4D math using integer coordinates. /// /// 4-element structure that can be used to represent 4D grid coordinates or sets of integers. @@ -40,8 +38,8 @@ pub struct Vector4i { } impl_vector_operators!(Vector4i, i32, (x, y, z, w)); -impl_vector_index!(Vector4i, i32, (x, y, z, w), Vector4iAxis, (X, Y, Z, W)); impl_common_vector_fns!(Vector4i, i32); +impl_from_tuple_for_vector4x!(Vector4i, i32); impl Vector4i { /// Returns a `Vector4i` with the given components. @@ -80,6 +78,10 @@ impl Vector4i { fn to_glam(self) -> IVec4 { IVec4::new(self.x, self.y, self.z, self.w) } + + pub fn coords(&self) -> (i32, i32, i32, i32) { + (self.x, self.y, self.z, self.w) + } } /// Formats the vector like Godot: `(x, y, z, w)`. @@ -95,29 +97,6 @@ unsafe impl GodotFfi for Vector4i { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } -/// Enumerates the axes in a [`Vector4i`]. -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] -#[repr(i32)] -pub enum Vector4iAxis { - /// The X axis. - X, - - /// The Y axis. - Y, - - /// The Z axis. - Z, - - /// The W axis. - W, -} - -// SAFETY: -// This type is represented as `Self` in Godot, so `*mut Self` is sound. -unsafe impl GodotFfi for Vector4iAxis { - ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } -} - impl GlamType for IVec4 { type Mapped = Vector4i; diff --git a/godot-core/src/builtin/vector_macros.rs b/godot-core/src/builtin/vectors/vector_macros.rs similarity index 91% rename from godot-core/src/builtin/vector_macros.rs rename to godot-core/src/builtin/vectors/vector_macros.rs index 3d3d8110c..1f59f974f 100644 --- a/godot-core/src/builtin/vector_macros.rs +++ b/godot-core/src/builtin/vectors/vector_macros.rs @@ -281,3 +281,42 @@ macro_rules! impl_float_vector_fns { } }; } + +macro_rules! impl_from_tuple_for_vector2x { + ( + $Vector:ty, + $Scalar:ty + ) => { + impl From<($Scalar, $Scalar)> for $Vector { + fn from(value: ($Scalar, $Scalar)) -> $Vector { + Self::new(value.0, value.1) + } + } + } +} + +macro_rules! impl_from_tuple_for_vector3x { + ( + $Vector:ty, + $Scalar:ty + ) => { + impl From<($Scalar, $Scalar, $Scalar)> for $Vector { + fn from(value: ($Scalar, $Scalar, $Scalar)) -> $Vector { + Self::new(value.0, value.1, value.2) + } + } + } +} + +macro_rules! impl_from_tuple_for_vector4x { + ( + $Vector:ty, + $Scalar:ty + ) => { + impl From<($Scalar, $Scalar, $Scalar, $Scalar)> for $Vector { + fn from(value: ($Scalar, $Scalar, $Scalar, $Scalar)) -> $Vector { + Self::new(value.0, value.1, value.2, value.3) + } + } + } +} \ No newline at end of file diff --git a/godot-core/src/builtin/vectors/vector_utils.rs b/godot-core/src/builtin/vectors/vector_utils.rs new file mode 100644 index 000000000..309029e49 --- /dev/null +++ b/godot-core/src/builtin/vectors/vector_utils.rs @@ -0,0 +1,91 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +#![macro_use] + +use crate::builtin::real; +use crate::builtin::{Vector2, Vector2i, Vector3, Vector3i, Vector4, Vector4i}; +use godot_ffi as sys; +use sys::{ffi_methods, GodotFfi}; + +macro_rules! swizzle { + ($vec:expr => $a:ident, $b:ident) => {{ + let expr = $vec; + (expr.$a, expr.$b).into() + }}; + ($vec:expr => $a:ident, $b:ident, $c:ident) => {{ + let expr = $vec; + (expr.$a, expr.$b, expr.$c).into() + }}; + ($vec:expr => $a:ident, $b:ident, $c:ident, $d:ident) => {{ + let expr = $vec; + (expr.$a, expr.$b, expr.$c, expr.$d).into() + }}; +} + +/// Enumerates the axes in a [`Vector2`]. +#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[repr(i32)] +pub enum Vector2Axis { + /// The X axis. + X, + /// The Y axis. + Y, +} + +// SAFETY: +// This type is represented as `Self` in Godot, so `*mut Self` is sound. +unsafe impl GodotFfi for Vector2Axis { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +} + +/// Enumerates the axes in a [`Vector3`]. +// TODO auto-generate this, alongside all the other builtin type's enums +#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[repr(i32)] +pub enum Vector3Axis { + /// The X axis. + X, + /// The Y axis. + Y, + /// The Z axis. + Z, +} + +// SAFETY: +// This type is represented as `Self` in Godot, so `*mut Self` is sound. +unsafe impl GodotFfi for Vector3Axis { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +} + +/// Enumerates the axes in a [`Vector4`]. +#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[repr(i32)] +pub enum Vector4Axis { + /// The X axis. + X, + /// The Y axis. + Y, + /// The Z axis. + Z, + /// The W axis. + W, +} + +// SAFETY: +// This type is represented as `Self` in Godot, so `*mut Self` is sound. +unsafe impl GodotFfi for Vector4Axis { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } +} + +impl_vector_index!(Vector2, real, (x, y), Vector2Axis, (X, Y)); +impl_vector_index!(Vector2i, i32, (x, y), Vector2Axis, (X, Y)); + +impl_vector_index!(Vector3, real, (x, y, z), Vector3Axis, (X, Y, Z)); +impl_vector_index!(Vector3i, i32, (x, y, z), Vector3Axis, (X, Y, Z)); + +impl_vector_index!(Vector4, real, (x, y, z, w), Vector4Axis, (X, Y, Z, W)); +impl_vector_index!(Vector4i, i32, (x, y, z, w), Vector4Axis, (X, Y, Z, W)); + diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index 5b3765f09..83e30eb31 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -10,12 +10,12 @@ use std::collections::btree_map::BTreeMap; #[doc(hidden)] // TODO consider body safe despite unsafe function, and explicitly mark unsafe {} locations pub unsafe fn __gdext_load_library( - interface: *const sys::GDExtensionInterface, + interface_or_get_proc_address: sys::InitCompat, library: sys::GDExtensionClassLibraryPtr, init: *mut sys::GDExtensionInitialization, ) -> sys::GDExtensionBool { let init_code = || { - sys::initialize(interface, library); + sys::initialize(interface_or_get_proc_address, library); let mut handle = InitHandle::new(); diff --git a/godot-core/src/lib.rs b/godot-core/src/lib.rs index cbd65539e..47e79d174 100644 --- a/godot-core/src/lib.rs +++ b/godot-core/src/lib.rs @@ -17,6 +17,8 @@ pub mod native_structure; pub mod obj; pub use godot_ffi as sys; +#[doc(hidden)] +pub use godot_ffi::out; pub use registry::*; /// Maps the Godot class API to Rust. @@ -64,14 +66,26 @@ pub mod private { fn print_panic(err: Box) { if let Some(s) = err.downcast_ref::<&'static str>() { - log::godot_error!("Panic msg: {s}"); + print_panic_message(s); } else if let Some(s) = err.downcast_ref::() { - log::godot_error!("Panic msg: {s}"); + print_panic_message(s.as_str()); } else { log::godot_error!("Rust panic of type ID {:?}", err.type_id()); } } + fn print_panic_message(msg: &str) { + // If the message contains newlines, print all of the lines after a line break, and indent them. + let lbegin = "\n "; + let indented = msg.replace('\n', lbegin); + + if indented.len() != msg.len() { + log::godot_error!("Panic msg:{lbegin}{indented}"); + } else { + log::godot_error!("Panic msg: {msg}"); + } + } + struct GodotPanicInfo { line: u32, file: String, @@ -134,20 +148,3 @@ pub mod private { std::io::stdout().flush().expect("flush stdout"); } } - -#[cfg(feature = "trace")] -#[macro_export] -macro_rules! out { - () => (eprintln!()); - ($fmt:literal) => (eprintln!($fmt)); - ($fmt:literal, $($arg:tt)*) => (eprintln!($fmt, $($arg)*)); -} - -#[cfg(not(feature = "trace"))] -// TODO find a better way than sink-writing to avoid warnings, #[allow(unused_variables)] doesn't work -#[macro_export] -macro_rules! out { - () => ({}); - ($fmt:literal) => ({ use std::io::{sink, Write}; let _ = write!(sink(), $fmt); }); - ($fmt:literal, $($arg:tt)*) => ({ use std::io::{sink, Write}; let _ = write!(sink(), $fmt, $($arg)*); };) -} diff --git a/godot-core/src/log.rs b/godot-core/src/log.rs index e2f619180..988ea3d10 100644 --- a/godot-core/src/log.rs +++ b/godot-core/src/log.rs @@ -4,23 +4,39 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +#[macro_export] +#[doc(hidden)] +macro_rules! inner_godot_msg { + // FIXME expr needs to be parenthesised, see usages + ($godot_fn:ident; $fmt:literal $(, $args:expr)* $(,)?) => { + //($($args:tt),* $(,)?) => { + unsafe { + let msg = format!("{}\0", format_args!($fmt $(, $args)*)); + assert!(msg.is_ascii(), "godot_error: message must be ASCII"); + + // Check whether engine is loaded, otherwise fall back to stderr. + if $crate::sys::is_initialized() { + $crate::sys::interface_fn!($godot_fn)( + $crate::sys::c_str_from_str(&msg), + $crate::sys::c_str(b"\0"), + $crate::sys::c_str_from_str(concat!(file!(), "\0")), + line!() as i32, + false as $crate::sys::GDExtensionBool, // whether to create a toast notification in editor + ); + } else { + eprintln!("[{}] {}", stringify!($godot_fn), &msg[..msg.len() - 1]); + } + } + }; +} + /// Pushes a warning message to Godot's built-in debugger and to the OS terminal. /// /// _Godot equivalent: @GlobalScope.push_warning()_ #[macro_export] macro_rules! godot_warn { ($fmt:literal $(, $args:expr)* $(,)?) => { - unsafe { - let msg = format!("{}\0", format_args!($fmt $(, $args)*)); - - $crate::sys::interface_fn!(print_warning)( - msg.as_bytes().as_ptr() as *const _, - "\0".as_bytes().as_ptr() as *const _, - concat!(file!(), "\0").as_ptr() as *const _, - line!() as _, - false as $crate::sys::GDExtensionBool, // whether to create a toast notification in editor - ); - } + $crate::inner_godot_msg!(print_warning; $fmt $(, $args)*); }; } @@ -29,37 +45,15 @@ macro_rules! godot_warn { /// _Godot equivalent: @GlobalScope.push_error()_ #[macro_export] macro_rules! godot_error { - // FIXME expr needs to be parenthesised, see usages ($fmt:literal $(, $args:expr)* $(,)?) => { - //($($args:tt),* $(,)?) => { - unsafe { - let msg = format!("{}\0", format_args!($fmt $(, $args)*)); - - $crate::sys::interface_fn!(print_error)( - msg.as_bytes().as_ptr() as *const _, - "\0".as_bytes().as_ptr() as *const _, - concat!(file!(), "\0").as_ptr() as *const _, - line!() as _, - false as $crate::sys::GDExtensionBool, // whether to create a toast notification in editor - ); - } + $crate::inner_godot_msg!(print_error; $fmt $(, $args)*); }; } #[macro_export] macro_rules! godot_script_error { ($fmt:literal $(, $args:expr)* $(,)?) => { - unsafe { - let msg = format!("{}\0", format_args!($fmt $(, $args)*)); - - $crate::sys::interface_fn!(print_script_error)( - msg.as_bytes().as_ptr() as *const _, - "\0".as_bytes().as_ptr() as *const _, - concat!(file!(), "\0").as_ptr() as *const _, - line!() as _, - false as $crate::sys::GDExtensionBool, // whether to create a toast notification in editor - ); - } + $crate::inner_godot_msg!(print_script_error; $fmt $(, $args)*); }; } diff --git a/godot-core/src/macros.rs b/godot-core/src/macros.rs index 184cbcf4e..a08d04c26 100644 --- a/godot-core/src/macros.rs +++ b/godot-core/src/macros.rs @@ -62,7 +62,7 @@ macro_rules! gdext_call_signature_method { $method_name:ident, $ptrcall_type:path ) => { - ::ptrcall::<$Class>( + ::ptrcall::<$Class>( $instance_ptr, $args, $ret, @@ -79,7 +79,7 @@ macro_rules! gdext_call_signature_method { $func:expr, $method_name:ident ) => { - ::varcall::<$Class>( + ::varcall::<$Class>( $instance_ptr, $args, $ret, @@ -183,7 +183,9 @@ macro_rules! gdext_register_method_inner { if success.is_none() { // Signal error and set return type to Nil (*err).error = sys::GDEXTENSION_CALL_ERROR_INVALID_METHOD; // no better fitting enum? - sys::interface_fn!(variant_new_nil)(ret); + + // TODO(uninit) + sys::interface_fn!(variant_new_nil)(sys::AsUninit::as_uninit(ret)); } } diff --git a/godot-core/src/obj/as_arg.rs b/godot-core/src/obj/as_arg.rs index ad55a895b..ba73d37dc 100644 --- a/godot-core/src/obj/as_arg.rs +++ b/godot-core/src/obj/as_arg.rs @@ -22,8 +22,11 @@ pub trait AsArg: Sealed { impl Sealed for Gd {} impl AsArg for Gd { fn as_arg_ptr(&self) -> sys::GDExtensionConstTypePtr { - // Pass argument to engine: increment refcount - ::maybe_inc_ref(self); + // We're passing a reference to the object to the callee. If the reference count needs to be + // incremented then the callee will do so. We do not need to prematurely do so. + // + // In Rust terms, if `T` is refcounted then we are effectively passing a `&Arc`, and the callee + // would need to call `.clone()` if desired. self.sys_const() } } diff --git a/godot-core/src/obj/gd.rs b/godot-core/src/obj/gd.rs index f827739ac..1f80150e8 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -12,7 +12,9 @@ use std::ptr; use godot_ffi as sys; use godot_ffi::VariantType; use sys::types::OpaqueObject; -use sys::{ffi_methods, interface_fn, static_assert_eq_size, GodotFfi, PtrcallType}; +use sys::{ + ffi_methods, interface_fn, static_assert_eq_size, GodotFfi, GodotNullablePtr, PtrcallType, +}; use crate::builtin::meta::{ClassName, VariantMetadata}; use crate::builtin::{ @@ -550,13 +552,20 @@ where // https://github.com/godotengine/godot-cpp/issues/954 unsafe fn from_arg_ptr(ptr: sys::GDExtensionTypePtr, call_type: PtrcallType) -> Self { - if T::Mem::pass_as_ref(call_type) { - let obj_ptr = interface_fn!(ref_get_object)(ptr as sys::GDExtensionRefPtr); - // ref_get_object increments the ref_count for us - Self::from_obj_sys_weak(obj_ptr) + let obj_ptr = if T::Mem::pass_as_ref(call_type) { + // ptr is `Ref*` + // See the docs for `PtrcallType::Virtual` for more info on `Ref`. + interface_fn!(ref_get_object)(ptr as sys::GDExtensionRefPtr) + } else if matches!(call_type, PtrcallType::Virtual) { + // ptr is `T**` + *(ptr as *mut sys::GDExtensionObjectPtr) } else { - Self::from_obj_sys(ptr as sys::GDExtensionObjectPtr) - } + // ptr is `T*` + ptr as sys::GDExtensionObjectPtr + }; + + // obj_ptr is `T*` + Self::from_obj_sys(obj_ptr) } unsafe fn move_return_ptr(self, ptr: sys::GDExtensionTypePtr, call_type: PtrcallType) { @@ -570,6 +579,11 @@ where } } +// SAFETY: +// `Gd` will only contain types that inherit from `crate::engine::Object`. +// Godots `Object` in turn is known to be nullable and always a pointer. +unsafe impl GodotNullablePtr for Gd {} + impl Gd { /// Runs `init_fn` on the address of a pointer (initialized to null). If that pointer is still null after the `init_fn` call, /// then `None` will be returned; otherwise `Gd::from_obj_sys(ptr)`. @@ -581,6 +595,11 @@ impl Gd { /// `init_fn` must be a function that correctly handles a _type pointer_ pointing to an _object pointer_. #[doc(hidden)] pub unsafe fn from_sys_init_opt(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Option { + // TODO(uninit) - should we use GDExtensionUninitializedTypePtr instead? Then update all the builtin codegen... + let init_fn = |ptr| { + init_fn(sys::AsUninit::force_init(ptr)); + }; + // Note: see _call_native_mb_ret_obj() in godot-cpp, which does things quite different (e.g. querying the instance binding). // Initialize pointer with given function, return Some(ptr) on success and None otherwise @@ -597,14 +616,14 @@ impl Gd { /// `init_fn` must be a function that correctly handles a _type pointer_ pointing to an _object pointer_. #[doc(hidden)] pub unsafe fn raw_object_init( - init_fn: impl FnOnce(sys::GDExtensionTypePtr), + init_fn: impl FnOnce(sys::GDExtensionUninitializedTypePtr), ) -> sys::GDExtensionObjectPtr { // return_ptr has type GDExtensionTypePtr = GDExtensionObjectPtr* = OpaqueObject* = Object** // (in other words, the type-ptr contains the _address_ of an object-ptr). let mut object_ptr: sys::GDExtensionObjectPtr = ptr::null_mut(); let return_ptr: *mut sys::GDExtensionObjectPtr = ptr::addr_of_mut!(object_ptr); - init_fn(return_ptr as sys::GDExtensionTypePtr); + init_fn(return_ptr as sys::GDExtensionUninitializedTypePtr); // We don't need to know if Object** is null, but if Object* is null; return_ptr has the address of a local (never null). object_ptr @@ -704,12 +723,15 @@ impl Export for Gd { impl FromVariant for Gd { fn try_from_variant(variant: &Variant) -> Result { let result_or_none = unsafe { - // TODO(#234) replace Gd:: with Self when Godot stops allowing - // illegal conversions (See - // https://github.com/godot-rust/gdext/issues/158) + // TODO(#234) replace Gd:: with Self when Godot stops allowing illegal conversions + // See https://github.com/godot-rust/gdext/issues/158 + + // TODO(uninit) - see if we can use from_sys_init() + use ::godot_ffi::AsUninit; + Gd::::from_sys_init_opt(|self_ptr| { let converter = sys::builtin_fn!(object_from_variant); - converter(self_ptr, variant.var_sys()); + converter(self_ptr.as_uninit(), variant.var_sys()); }) }; @@ -720,7 +742,7 @@ impl FromVariant for Gd { // TODO(#234) remove this cast when Godot stops allowing illegal conversions // (See https://github.com/godot-rust/gdext/issues/158) .and_then(|obj| obj.owned_cast().ok()) - .ok_or(VariantConversionError) + .ok_or(VariantConversionError::BadType) } } @@ -746,6 +768,25 @@ impl ToVariant for Gd { } } +impl ToVariant for Option> { + fn to_variant(&self) -> Variant { + match self { + Some(gd) => gd.to_variant(), + None => Variant::nil(), + } + } +} + +impl FromVariant for Option> { + fn try_from_variant(variant: &Variant) -> Result { + if variant.is_nil() { + Ok(None) + } else { + Gd::try_from_variant(variant).map(Some) + } + } +} + impl PartialEq for Gd { /// ⚠️ Returns whether two `Gd` pointers point to the same object. /// diff --git a/godot-core/src/obj/instance_id.rs b/godot-core/src/obj/instance_id.rs index 7d7962811..5f277d639 100644 --- a/godot-core/src/obj/instance_id.rs +++ b/godot-core/src/obj/instance_id.rs @@ -86,7 +86,7 @@ unsafe impl GodotFfi for InstanceId { impl FromVariant for InstanceId { fn try_from_variant(variant: &Variant) -> Result { i64::try_from_variant(variant) - .and_then(|i| InstanceId::try_from_i64(i).ok_or(VariantConversionError)) + .and_then(|i| InstanceId::try_from_i64(i).ok_or(VariantConversionError::BadValue)) } } diff --git a/godot-ffi/Cargo.toml b/godot-ffi/Cargo.toml index ffd94bcd4..ccf2b6f49 100644 --- a/godot-ffi/Cargo.toml +++ b/godot-ffi/Cargo.toml @@ -10,6 +10,7 @@ categories = ["game-engines", "graphics"] [features] custom-godot = ["godot-bindings/custom-godot"] codegen-fmt = ["godot-codegen/codegen-fmt"] +trace = [] [dependencies] paste = "1" diff --git a/godot-ffi/build.rs b/godot-ffi/build.rs index ae290afee..4c6283111 100644 --- a/godot-ffi/build.rs +++ b/godot-ffi/build.rs @@ -23,7 +23,7 @@ fn main() { godot_bindings::clear_dir(gen_path, &mut watch); godot_bindings::write_gdextension_headers(&h_path, &rs_path, &mut watch); - godot_codegen::generate_sys_files(gen_path, &mut watch); + godot_codegen::generate_sys_files(gen_path, &h_path, &mut watch); watch.write_stats_to(&gen_path.join("ffi-stats.txt")); println!("cargo:rerun-if-changed=build.rs"); diff --git a/godot-ffi/src/compat/compat_4_0.rs b/godot-ffi/src/compat/compat_4_0.rs new file mode 100644 index 000000000..709f00931 --- /dev/null +++ b/godot-ffi/src/compat/compat_4_0.rs @@ -0,0 +1,54 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +//! Legacy 4.0 API +//! +//! The old API uses a struct `GDExtensionInterface`, which is passed to the extension entry point (via pointer). +//! This struct contains function pointers to all FFI functions defined in the `gdextension_interface.h` header. + +use crate as sys; +use crate::compat::BindingCompat; + +pub type InitCompat = *const sys::GDExtensionInterface; + +impl BindingCompat for *const sys::GDExtensionInterface { + fn ensure_static_runtime_compatibility(&self) { + // We try to read the first fields of the GDExtensionInterface struct, which are version numbers. + // If those are unrealistic numbers, chances are high that `self` is in fact a function pointer (used for Godot 4.1.x). + + let interface = unsafe { &**self }; + let major = interface.version_major; + let minor = interface.version_minor; + + // We cannot print version (major/minor are parts of the function pointer). We _could_ theoretically interpret it as + // GetProcAddr function pointer and call get_godot_version, but that's not adding that much useful information and may + // also fail. + let static_version = crate::GdextBuild::godot_static_version_string(); + assert!(major == 4 && minor == 0, + "gdext was compiled against a legacy Godot version ({static_version}),\n\ + but initialized by a newer Godot binary (4.1+).\n\ + \n\ + You have multiple options:\n\ + 1) Recompile gdext against the newer Godot version.\n\ + 2) If you want to use a legacy extension under newer Godot, open the .gdextension file\n \ + and add `compatibility_minimum = 4.0` under the [configuration] section.\n" + ); + } + + fn runtime_version(&self) -> sys::GDExtensionGodotVersion { + let interface = unsafe { &**self }; + sys::GDExtensionGodotVersion { + major: interface.version_major, + minor: interface.version_minor, + patch: interface.version_patch, + string: interface.version_string, + } + } + + fn load_interface(&self) -> sys::GDExtensionInterface { + unsafe { **self } + } +} diff --git a/godot-ffi/src/compat/compat_4_1.rs b/godot-ffi/src/compat/compat_4_1.rs new file mode 100644 index 000000000..fba44e780 --- /dev/null +++ b/godot-ffi/src/compat/compat_4_1.rs @@ -0,0 +1,118 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +//! Modern 4.1+ API +//! +//! The extension entry point is passed `get_proc_address` function pointer, which can be used to load all other +//! GDExtension FFI functions dynamically. This is a departure from the previous struct-based approach. +//! +//! Relevant upstream PR: https://github.com/godotengine/godot/pull/76406 + +use crate as sys; +use crate::compat::BindingCompat; + +pub type InitCompat = sys::GDExtensionInterfaceGetProcAddress; + +#[repr(C)] +struct LegacyLayout { + version_major: u32, + version_minor: u32, + version_patch: u32, + version_string: *const std::ffi::c_char, +} + +impl BindingCompat for sys::GDExtensionInterfaceGetProcAddress { + fn ensure_static_runtime_compatibility(&self) { + // In Godot 4.0.x, before the new GetProcAddress mechanism, the init function looked as follows. + // In place of the `get_proc_address` function pointer, the `p_interface` data pointer was passed. + // + // typedef GDExtensionBool (*GDExtensionInitializationFunction)( + // const GDExtensionInterface *p_interface, + // GDExtensionClassLibraryPtr p_library, + // GDExtensionInitialization *r_initialization + // ); + // + // Also, the GDExtensionInterface struct was beginning with these fields: + // + // typedef struct { + // uint32_t version_major; + // uint32_t version_minor; + // uint32_t version_patch; + // const char *version_string; + // ... + // } GDExtensionInterface; + // + // As a result, we can try to interpret the function pointer as a legacy GDExtensionInterface data pointer and check if the + // first fields have values version_major=4 and version_minor=0. This might be deep in UB territory, but the alternative is + // to not be able to detect Godot 4.0.x at all, and run into UB anyway. + + let get_proc_address = self.expect("get_proc_address unexpectedly null"); + let data_ptr = get_proc_address as *const LegacyLayout; // crowbar it via `as` cast + + // Assumption is that we have at least 8 bytes of memory to safely read from (for both the data and the function case). + let major = unsafe { data_ptr.read().version_major }; + let minor = unsafe { data_ptr.read().version_minor }; + let patch = unsafe { data_ptr.read().version_patch }; + + if major != 4 || minor != 0 { + // Technically, major should always be 4; loading Godot 3 will crash anyway. + return; + } + + let static_version = crate::GdextBuild::godot_static_version_string(); + let runtime_version = unsafe { + let char_ptr = data_ptr.read().version_string; + let c_str = std::ffi::CStr::from_ptr(char_ptr); + + String::from_utf8_lossy(c_str.to_bytes()) + .as_ref() + .strip_prefix("Godot Engine ") + .unwrap_or(&String::from_utf8_lossy(c_str.to_bytes())) + .to_string() + }; + + // Version 4.0.999 is used to signal that we're running Godot 4.1+ but loading extensions in legacy mode. + if patch == 999 { + // Godot 4.1+ loading the extension in legacy mode. + // + // Instead of panicking, we could *theoretically* fall back to the legacy API at runtime, but then gdext would need to + // always ship two versions of gdextension_interface.h (+ generated code) and would encourage use of the legacy API. + panic!( + "gdext was compiled against a modern Godot version ({static_version}), but loaded in legacy (4.0.x) mode.\n\ + In your .gdextension file, add `compatibility_minimum = 4.1` under the [configuration] section.\n" + ) + } else { + // Truly a Godot 4.0 version. + panic!( + "gdext was compiled against a newer Godot version ({static_version}),\n\ + but loaded by a legacy Godot binary ({runtime_version}).\n\ + \n\ + You have multiple options:\n\ + 1) Run the newer Godot version.\n\ + 2) Compile gdext against the older Godot binary (see `custom-godot` feature).\n\ + \n" + ); + } + } + + fn runtime_version(&self) -> sys::GDExtensionGodotVersion { + unsafe { + let get_proc_address = self.expect("get_proc_address unexpectedly null"); + let get_godot_version = get_proc_address(sys::c_str(b"get_godot_version\0")); //.expect("get_godot_version unexpectedly null"); + + let get_godot_version = + crate::cast_fn_ptr!(get_godot_version as sys::GDExtensionInterfaceGetGodotVersion); + + let mut version = std::mem::MaybeUninit::::zeroed(); + get_godot_version(version.as_mut_ptr()); + version.assume_init() + } + } + + fn load_interface(&self) -> sys::GDExtensionInterface { + unsafe { sys::GDExtensionInterface::load(*self) } + } +} diff --git a/godot-ffi/src/compat/mod.rs b/godot-ffi/src/compat/mod.rs new file mode 100644 index 000000000..612524750 --- /dev/null +++ b/godot-ffi/src/compat/mod.rs @@ -0,0 +1,38 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use crate as sys; + +/// Dispatch at runtime between Godot 4.0 legacy and 4.1+ APIs. +/// +/// Provides a compatibility layer to be able to use 4.0.x extensions under Godot versions >= 4.1. +/// Also performs deterministic checks and expressive errors for cases where compatibility cannot be provided. +pub(crate) trait BindingCompat { + // Implementation note: these methods could be unsafe, but that would remove any `unsafe` statements _inside_ + // the function bodies, making reasoning about them harder. Also, the call site is already an unsafe function, + // so it would not add safety there, either. + // Either case, given the spec of the GDExtension C API in 4.0 and 4.1, the operations should be safe. + + /// Panics on mismatch between compiled and runtime Godot version. + /// + /// This can happen in the following cases, with their respective sub-cases: + /// + /// 1) When a gdext version compiled against 4.1+ GDExtension API is invoked with an entry point using the legacy calling convention. + /// a) The .gdextension file's `[configuration]` section does not contain a `compatibility_minimum = 4.1` statement. + /// b) gdext was compiled against a 4.1+ Godot version, but at runtime the library is loaded from a 4.0.x version. + /// + /// 2) When a gdext version compiled against 4.0.x GDExtension API is invoked using the modern way. + /// + /// This is no guarantee, but rather a best-effort heuristic to attempt aborting rather than causing UB/crashes. + /// Changes in the way how Godot loads GDExtension can invalidate assumptions made here. + fn ensure_static_runtime_compatibility(&self); + + /// Return version dynamically passed via `gdextension_interface.h` file. + fn runtime_version(&self) -> sys::GDExtensionGodotVersion; + + /// Return the interface, either as-is from the header (legacy) or code-generated (modern API). + fn load_interface(&self) -> sys::GDExtensionInterface; +} diff --git a/godot-ffi/src/godot_ffi.rs b/godot-ffi/src/godot_ffi.rs index 0cf87bdcc..b344bac10 100644 --- a/godot-ffi/src/godot_ffi.rs +++ b/godot-ffi/src/godot_ffi.rs @@ -4,8 +4,8 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate as sys; -use std::fmt::Debug; +use crate::{self as sys, ptr_then}; +use std::{fmt::Debug, ptr}; /// Adds methods to convert from and to Godot FFI pointers. /// See [crate::ffi_methods] for ergonomic implementation. @@ -28,7 +28,7 @@ pub unsafe trait GodotFfi { /// /// # Safety /// `init_fn` must be a function that correctly handles a (possibly-uninitialized) _type ptr_. - unsafe fn from_sys_init(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Self; + unsafe fn from_sys_init(init_fn: impl FnOnce(sys::GDExtensionUninitializedTypePtr)) -> Self; /// Like [`Self::from_sys_init`], but pre-initializes the sys pointer to a `Default::default()` instance /// before calling `init_fn`. @@ -44,18 +44,20 @@ pub unsafe trait GodotFfi { where Self: Sized, // + Default { - Self::from_sys_init(init_fn) + // SAFETY: this default implementation is potentially incorrect. + // By implementing the GodotFfi trait, you acknowledge that these may need to be overridden. + Self::from_sys_init(|ptr| init_fn(sys::AsUninit::force_init(ptr))) // TODO consider using this, if all the implementors support it // let mut result = Self::default(); - // init_fn(result.sys_mut()); + // init_fn(result.sys_mut().as_uninit()); // result } /// Return Godot opaque pointer, for an immutable operation. /// /// Note that this is a `*mut` pointer despite taking `&self` by shared-ref. - /// This is because most of Godot's rust API is not const-correct. This can still + /// This is because most of Godot's Rust API is not const-correct. This can still /// enhance user code (calling `sys_mut` ensures no aliasing at the time of the call). fn sys(&self) -> sys::GDExtensionTypePtr; @@ -94,6 +96,53 @@ pub unsafe trait GodotFfi { unsafe fn move_return_ptr(self, dst: sys::GDExtensionTypePtr, call_type: PtrcallType); } +/// Marks a type as having a nullable counterpart in Godot. +/// +/// This trait primarily exists to implement GodotFfi for `Option>`, which is not possible +/// due to Rusts orphan rule. The rule also enforces better API design, though. `godot_ffi` should +/// not concern itself with the details of how Godot types work and merely defines the FFI abstraction. +/// By having a marker trait for nullable types, we can provide a generic implementation for +/// compatible types, without knowing their definition. +/// +/// # Safety +/// +/// The type has to have a pointer-sized counterpart in Godot, which needs to be nullable. +/// So far, this only applies to class types (Object hierarchy). +pub unsafe trait GodotNullablePtr: GodotFfi {} + +unsafe impl GodotFfi for Option +where + T: GodotNullablePtr, +{ + fn sys(&self) -> sys::GDExtensionTypePtr { + match self { + Some(value) => value.sys(), + None => ptr::null_mut() as sys::GDExtensionTypePtr, + } + } + + unsafe fn from_sys(ptr: sys::GDExtensionTypePtr) -> Self { + ptr_then(ptr, |ptr| T::from_sys(ptr)) + } + + unsafe fn from_sys_init(init_fn: impl FnOnce(sys::GDExtensionUninitializedTypePtr)) -> Self { + let mut raw = std::mem::MaybeUninit::uninit(); + init_fn(raw.as_mut_ptr() as sys::GDExtensionUninitializedTypePtr); + + Self::from_sys(raw.assume_init()) + } + + unsafe fn from_arg_ptr(ptr: sys::GDExtensionTypePtr, call_type: PtrcallType) -> Self { + ptr_then(ptr, |ptr| T::from_arg_ptr(ptr, call_type)) + } + + unsafe fn move_return_ptr(self, ptr: sys::GDExtensionTypePtr, call_type: PtrcallType) { + if let Some(value) = self { + value.move_return_ptr(ptr, call_type) + } + } +} + /// An indication of what type of pointer call is being made. #[derive(Default, Copy, Clone, Eq, PartialEq, Debug)] pub enum PtrcallType { @@ -106,11 +155,15 @@ pub enum PtrcallType { /// Virtual pointer call. /// - /// A virtual call behaves like [`PtrcallType::Standard`], except for `RefCounted` objects. - /// `RefCounted` objects are instead passed in and returned as `Ref` objects in Godot. + /// A virtual call behaves like [`PtrcallType::Standard`], except for Objects. + /// + /// Objects that do not inherit from `RefCounted` are passed in as `Object**` + /// (`*mut GDExtensionObjectPtr` in GDExtension terms), and objects that inherit from + /// `RefCounted` are passed in as `Ref*` (`GDExtensionRefPtr` in GDExtension + /// terms) and returned as `Ref` objects in Godot. /// - /// To properly get a value from an argument in a pointer call, you must use `ref_get_object`. And to - /// return a value you must use `ref_set_object`. + /// To get a `GDExtensionObjectPtr` from a `GDExtensionRefPtr`, you must use `ref_get_object`, and to + /// set a `GDExtensionRefPtr` to some object, you must use `ref_set_object`. /// /// See also https://github.com/godotengine/godot-cpp/issues/954. Virtual, @@ -163,9 +216,9 @@ macro_rules! ffi_methods_one { }; (OpaquePtr $Ptr:ty; $( #[$attr:meta] )? $vis:vis $from_sys_init:ident = from_sys_init) => { $( #[$attr] )? $vis - unsafe fn $from_sys_init(init: impl FnOnce($Ptr)) -> Self { + unsafe fn $from_sys_init(init: impl FnOnce(<$Ptr as $crate::AsUninit>::Ptr)) -> Self { let mut raw = std::mem::MaybeUninit::uninit(); - init(raw.as_mut_ptr() as $Ptr); + init(raw.as_mut_ptr() as <$Ptr as $crate::AsUninit>::Ptr); Self::from_opaque(raw.assume_init()) } @@ -199,7 +252,7 @@ macro_rules! ffi_methods_one { }; (OpaqueValue $Ptr:ty; $( #[$attr:meta] )? $vis:vis $from_sys_init:ident = from_sys_init) => { $( #[$attr] )? $vis - unsafe fn $from_sys_init(init: impl FnOnce($Ptr)) -> Self { + unsafe fn $from_sys_init(init: impl FnOnce(<$Ptr as $crate::AsUninit>::Ptr)) -> Self { let mut raw = std::mem::MaybeUninit::uninit(); init(std::mem::transmute(raw.as_mut_ptr())); Self::from_opaque(raw.assume_init()) @@ -233,9 +286,9 @@ macro_rules! ffi_methods_one { }; (SelfPtr $Ptr:ty; $( #[$attr:meta] )? $vis:vis $from_sys_init:ident = from_sys_init) => { $( #[$attr] )? $vis - unsafe fn $from_sys_init(init: impl FnOnce($Ptr)) -> Self { + unsafe fn $from_sys_init(init: impl FnOnce(<$Ptr as $crate::AsUninit>::Ptr)) -> Self { let mut raw = std::mem::MaybeUninit::::uninit(); - init(raw.as_mut_ptr() as $Ptr); + init(raw.as_mut_ptr() as <$Ptr as $crate::AsUninit>::Ptr); raw.assume_init() } @@ -445,7 +498,7 @@ mod scalars { // Do nothing } - unsafe fn from_sys_init(_init: impl FnOnce(sys::GDExtensionTypePtr)) -> Self { + unsafe fn from_sys_init(_init: impl FnOnce(sys::GDExtensionUninitializedTypePtr)) -> Self { // Do nothing } diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index fa94018cb..50beaee36 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -19,18 +19,23 @@ )] pub(crate) mod gen; +mod compat; mod godot_ffi; mod opaque; mod plugins; +use compat::BindingCompat; +use std::ffi::CStr; + // See https://github.com/dtolnay/paste/issues/69#issuecomment-962418430 // and https://users.rust-lang.org/t/proc-macros-using-third-party-crate/42465/4 #[doc(hidden)] pub use paste; -pub use crate::godot_ffi::{GodotFfi, GodotFuncMarshal, PtrcallType}; +pub use crate::godot_ffi::{GodotFfi, GodotFuncMarshal, GodotNullablePtr, PtrcallType}; pub use gen::central::*; pub use gen::gdextension_interface::*; +pub use gen::interface::*; // The impls only compile if those are different types -- ensures type safety through patch trait Distinct {} @@ -40,10 +45,34 @@ impl Distinct for GDExtensionConstTypePtr {} // ---------------------------------------------------------------------------------------------------------------------------------------------- +#[cfg(feature = "trace")] +#[macro_export] +macro_rules! out { + () => (eprintln!()); + ($fmt:literal) => (eprintln!($fmt)); + ($fmt:literal, $($arg:tt)*) => (eprintln!($fmt, $($arg)*)); +} + +#[cfg(not(feature = "trace"))] +// TODO find a better way than sink-writing to avoid warnings, #[allow(unused_variables)] doesn't work +#[macro_export] +macro_rules! out { + () => ({}); + ($fmt:literal) => ({ use std::io::{sink, Write}; let _ = write!(sink(), $fmt); }); + ($fmt:literal, $($arg:tt)*) => ({ use std::io::{sink, Write}; let _ = write!(sink(), $fmt, $($arg)*); };) +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + struct GodotBinding { interface: GDExtensionInterface, library: GDExtensionClassLibraryPtr, method_table: GlobalMethodTable, + runtime_metadata: GdextRuntimeMetadata, +} + +struct GdextRuntimeMetadata { + godot_version: GDExtensionGodotVersion, } /// Late-init globals @@ -53,25 +82,57 @@ static mut BINDING: Option = None; /// # Safety /// -/// - The `interface` pointer must be a valid pointer to a [`GDExtensionInterface`] object. +/// - The `interface` pointer must be either: +/// - a data pointer to a [`GDExtensionInterface`] object (for Godot 4.0.x) +/// - a function pointer of type [`GDExtensionInterfaceGetProcAddress`] (for Godot 4.1+) /// - The `library` pointer must be the pointer given by Godot at initialisation. /// - This function must not be called from multiple threads. /// - This function must be called before any use of [`get_library`]. -pub unsafe fn initialize( - interface: *const GDExtensionInterface, - library: GDExtensionClassLibraryPtr, -) { - let ver = std::ffi::CStr::from_ptr((*interface).version_string); - println!( - "Initialize GDExtension API for Rust: {}", - ver.to_str().unwrap() +pub unsafe fn initialize(compat: InitCompat, library: GDExtensionClassLibraryPtr) { + out!("Initialize gdext..."); + + out!( + "Godot version against which gdext was compiled: {}", + GdextBuild::godot_static_version_string() ); + // Before anything else: if we run into a Godot binary that's compiled differently from gdext, proceeding would be UB -> panic. + compat.ensure_static_runtime_compatibility(); + + let version = compat.runtime_version(); + out!("Godot version of GDExtension API at runtime: {version:?}"); + + let interface = compat.load_interface(); + out!("Loaded interface."); + + let method_table = GlobalMethodTable::load(&interface); + out!("Loaded builtin table."); + + let runtime_metadata = GdextRuntimeMetadata { + godot_version: version, + }; + BINDING = Some(GodotBinding { - interface: *interface, - method_table: GlobalMethodTable::new(&*interface), + interface, + method_table, library, + runtime_metadata, }); + out!("Assigned binding."); + + println!( + "Initialize GDExtension API for Rust: {}", + CStr::from_ptr(version.string) + .to_str() + .expect("unknown Godot version") + ); +} + +/// # Safety +/// +/// Must be called from the same thread as `initialize()` previously. +pub unsafe fn is_initialized() -> bool { + BINDING.is_some() } /// # Safety @@ -98,6 +159,14 @@ pub unsafe fn method_table() -> &'static GlobalMethodTable { &unwrap_ref_unchecked(&BINDING).method_table } +/// # Safety +/// +/// Must be accessed from the main thread. +#[inline(always)] +pub(crate) unsafe fn runtime_metadata() -> &'static GdextRuntimeMetadata { + &BINDING.as_ref().unwrap().runtime_metadata +} + /// Makes sure that Godot is running, or panics. Debug mode only! macro_rules! debug_assert_godot { ($expr:expr) => { @@ -187,7 +256,77 @@ pub fn to_const_ptr(ptr: *mut T) -> *const T { ptr as *const T } +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +/// Convert a GDExtension pointer type to its uninitialized version. +pub trait AsUninit { + type Ptr; + + #[allow(clippy::wrong_self_convention)] + fn as_uninit(self) -> Self::Ptr; + + fn force_init(uninit: Self::Ptr) -> Self; +} + +macro_rules! impl_as_uninit { + ($Ptr:ty, $Uninit:ty) => { + impl AsUninit for $Ptr { + type Ptr = $Uninit; + + fn as_uninit(self) -> $Uninit { + self as $Uninit + } + + fn force_init(uninit: Self::Ptr) -> Self { + uninit as Self + } + } + }; +} + +#[rustfmt::skip] +impl_as_uninit!(GDExtensionStringNamePtr, GDExtensionUninitializedStringNamePtr); +impl_as_uninit!(GDExtensionVariantPtr, GDExtensionUninitializedVariantPtr); +impl_as_uninit!(GDExtensionStringPtr, GDExtensionUninitializedStringPtr); +impl_as_uninit!(GDExtensionObjectPtr, GDExtensionUninitializedObjectPtr); +impl_as_uninit!(GDExtensionTypePtr, GDExtensionUninitializedTypePtr); + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +/// Metafunction to extract inner function pointer types from all the bindgen Option type names. +pub(crate) trait Inner: Sized { + type FnPtr: Sized; + + fn extract(self, error_msg: &str) -> Self::FnPtr; +} + +impl Inner for Option { + type FnPtr = T; + + fn extract(self, error_msg: &str) -> Self::FnPtr { + self.expect(error_msg) + } +} + +/// Extract a function pointer from its `Option` and convert it to the (dereferenced) target type. +/// +/// ```ignore +/// let get_godot_version = get_proc_address(sys::c_str(b"get_godot_version\0")); +/// let get_godot_version = sys::cast_fn_ptr!(get_godot_version as sys::GDExtensionInterfaceGetGodotVersion); +/// ``` +#[allow(unused)] +#[macro_export] +macro_rules! cast_fn_ptr { + ($option:ident as $ToType:ty) => {{ + let ptr = $option.expect("null function pointer"); + std::mem::transmute::::FnPtr>(ptr) + }}; +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + /// If `ptr` is not null, returns `Some(mapper(ptr))`; otherwise `None`. +#[inline] pub fn ptr_then(ptr: *mut T, mapper: F) -> Option where F: FnOnce(*mut T) -> R, @@ -200,6 +339,22 @@ where } } +/// Returns a C `const char*` for a null-terminated byte string. +#[inline] +pub fn c_str(s: &[u8]) -> *const std::ffi::c_char { + // Ensure null-terminated + debug_assert!(!s.is_empty() && s[s.len() - 1] == 0); + + s.as_ptr() as *const std::ffi::c_char +} + +#[inline] +pub fn c_str_from_str(s: &str) -> *const std::ffi::c_char { + debug_assert!(s.is_ascii()); + + c_str(s.as_bytes()) +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- #[doc(hidden)] diff --git a/godot-macros/src/derive_from_variant.rs b/godot-macros/src/derive_from_variant.rs new file mode 100644 index 000000000..98a32edd8 --- /dev/null +++ b/godot-macros/src/derive_from_variant.rs @@ -0,0 +1,200 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use crate::util::{decl_get_info, DeclInfo}; +use crate::ParseResult; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use venial::{Declaration, StructFields}; + +pub fn transform(decl: Declaration) -> ParseResult { + let DeclInfo { + where_, + generic_params, + name, + name_string, + } = decl_get_info(&decl); + let mut body = quote! { + let root = variant.try_to::()?; + let root = root.get(#name_string).ok_or(godot::builtin::VariantConversionError::BadType)?; + }; + + match decl { + Declaration::Struct(s) => match s.fields { + venial::StructFields::Unit => { + body = quote! { + #body + return Ok(Self); + } + } + venial::StructFields::Tuple(fields) => { + if fields.fields.len() == 1 { + body = quote! { + #body + let root = root.try_to()?; + Ok(Self(root)) + }; + } else { + let ident_and_set = fields.fields.iter().enumerate().map(|(k, _)| { + let ident = format_ident!("__{}", k); + ( + ident.clone(), + quote! { + let #ident = root.pop_front().ok_or(godot::builtin::VariantConversionError::MissingValue)?; + }, + + ) + }); + let (idents, ident_set): (Vec<_>, Vec<_>) = ident_and_set.unzip(); + body = quote! { + #body + let mut root = root.try_to::>()?; + #( + #ident_set + + )* + Ok(Self( + #(#idents.try_to()?,)* + )) + }; + } + } + venial::StructFields::Named(fields) => { + let fields = fields.fields.iter().map(|(field, _)|{ + let ident = &field.name; + let string_ident = &field.name.to_string(); + ( + quote!{ + let #ident = root.get(#string_ident).ok_or(godot::builtin::VariantConversionError::MissingValue)?; + }, + + quote!{ + #ident :#ident.try_to()? + } + ) + + }); + let (set_idents, set_self): (Vec<_>, Vec<_>) = fields.unzip(); + body = quote! { + #body + let root = root.try_to::()?; + #( + #set_idents + )* + Ok(Self{ #(#set_self,)* }) + } + } + }, + Declaration::Enum(enum_) => { + if enum_.variants.is_empty() { + body = quote! { + panic!(); + } + } else { + let mut matches = quote! {}; + for (enum_v, _) in &enum_.variants.inner { + let variant_name = enum_v.name.clone(); + let variant_name_string = enum_v.name.to_string(); + let if_let_content = match &enum_v.contents { + StructFields::Unit => quote! { + let child = root.try_to::(); + if child == Ok(String::from(#variant_name_string)) { + return Ok(Self::#variant_name); + } + }, + StructFields::Tuple(fields) => { + if fields.fields.len() == 1 { + let (field, _) = fields.fields.first().unwrap(); + let field_type = &field.ty; + quote! { + let child = root.try_to::(); + if let Ok(child) = child { + if let Some(variant) = child.get(#variant_name_string) { + return Ok(Self::#variant_name(variant.try_to::<#field_type>()?)); + } + } + } + } else { + let fields = fields.fields.iter().enumerate() + .map(|(k, (field, _))|{ + let ident = format_ident!("__{k}"); + let field_type = &field.ty; + ( + quote!{#ident}, + + quote!{ + let #ident = variant + .pop_front() + .ok_or(godot::builtin::VariantConversionError::MissingValue)? + .try_to::<#field_type>()?; + }) + }); + let (idents, set_idents): (Vec<_>, Vec<_>) = fields.unzip(); + + quote! { + let child = root.try_to::(); + if let Ok(child) = child { + if let Some(variant) = child.get(#variant_name_string) { + let mut variant = variant.try_to::>()?; + #(#set_idents)* + return Ok(Self::#variant_name(#(#idents ,)*)); + } + } + } + } + } + StructFields::Named(fields) => { + let fields = fields.fields.iter().map(|(field, _)| { + let field_name = &field.name; + let field_name_string = &field.name.to_string(); + let field_type = &field.ty; + ( + quote!{#field_name}, + quote!{ + let #field_name = variant.get(#field_name_string).ok_or(godot::builtin::VariantConversionError::MissingValue)?.try_to::<#field_type>()?; + } + ) + }); + let (fields, set_fields): (Vec<_>, Vec<_>) = fields.unzip(); + quote! { + if let Ok(root) = root.try_to::() { + if let Some(variant) = root.get(#variant_name_string) { + let variant = variant.try_to::()?; + #( + #set_fields + )* + return Ok(Self::#variant_name{ #(#fields,)* }); + } + } + } + } + }; + matches = quote! { + #matches + #if_let_content + }; + } + body = quote! { + #body + #matches + Err(godot::builtin::VariantConversionError::MissingValue) + }; + } + } + _ => unreachable!(), + } + + let gen = generic_params.as_ref().map(|x| x.as_inline_args()); + Ok(quote! { + impl #generic_params godot::builtin::FromVariant for #name #gen #where_ { + fn try_from_variant( + variant: &godot::builtin::Variant + ) -> Result { + #body + } + } + }) +} diff --git a/godot-macros/src/derive_to_variant.rs b/godot-macros/src/derive_to_variant.rs new file mode 100644 index 000000000..88664106c --- /dev/null +++ b/godot-macros/src/derive_to_variant.rs @@ -0,0 +1,230 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use crate::util::{decl_get_info, DeclInfo}; +use crate::ParseResult; +use proc_macro2::TokenStream; +#[allow(unused_imports)] +use quote::ToTokens; +use quote::{format_ident, quote}; +use venial::{Declaration, StructFields}; + +pub fn transform(decl: Declaration) -> ParseResult { + let mut body = quote! { + let mut root = godot::builtin::Dictionary::new(); + }; + + let DeclInfo { + where_, + generic_params, + name, + name_string, + } = decl_get_info(&decl); + + match &decl { + Declaration::Struct(struct_) => match &struct_.fields { + StructFields::Unit => make_struct_unit(&mut body, name_string), + StructFields::Tuple(fields) => make_struct_tuple(&mut body, fields, name_string), + StructFields::Named(named_struct) => { + make_struct_named(&mut body, named_struct, name_string); + } + }, + Declaration::Enum(enum_) => { + let arms = enum_.variants.iter().map(|(enum_v, _)| { + let variant_name = enum_v.name.clone(); + let variant_name_string = enum_v.name.to_string(); + let fields = match &enum_v.contents { + StructFields::Unit => quote! {}, + StructFields::Tuple(s) => make_tuple_enum_field(s), + StructFields::Named(named) => make_named_enum_field(named), + }; + let arm_content = match &enum_v.contents { + StructFields::Unit => quote! { + #variant_name_string.to_variant() + }, + + StructFields::Tuple(fields) => make_enum_tuple_arm(fields, variant_name_string), + StructFields::Named(fields) => make_enum_named_arm(fields, variant_name_string), + }; + quote! { + Self::#variant_name #fields => { + #arm_content + } + } + }); + + body = quote! { + #body + let content = match core::clone::Clone::clone(self) { + #( + #arms + )* + }; + root.insert(#name_string, content); + }; + } + // This is unreachable cause this case has already returned + // with an error in decl_get_info call. + _ => unreachable!(), + }; + body = quote! { + #body + root.to_variant() + }; + + let gen = generic_params.as_ref().map(|x| x.as_inline_args()); + // we need to allow unreachable for Uninhabited enums because it uses match self {} + // it's okay since we can't ever have a value to call to_variant on it anyway. + let allow_unreachable = matches!(&decl,Declaration::Enum(e) if e.variants.is_empty()); + let allow_unreachable = if allow_unreachable { + quote! { + #[allow(unreachable_code)] + } + } else { + quote! {} + }; + + Ok(quote! { + impl #generic_params godot::builtin::ToVariant for #name #gen #where_ { + #allow_unreachable + fn to_variant(&self) -> godot::builtin::Variant { + #body + } + } + }) +} + +fn make_named_enum_field(named: &venial::NamedStructFields) -> TokenStream { + let fields = named.fields.iter().map(|(field, _)| &field.name); + quote!( + {#(#fields ,)*} + ) +} + +fn make_tuple_enum_field(s: &venial::TupleStructFields) -> TokenStream { + let fields = s + .fields + .iter() + .enumerate() + .map(|(k, _)| format_ident!("__{}", k)); + quote! { + (#(#fields ,)*) + } +} + +fn make_enum_named_arm( + fields: &venial::NamedStructFields, + variant_name_string: String, +) -> TokenStream { + let fields = fields + .fields + .iter() + .map(|(field, _)| (field.name.clone(), field.name.to_string())) + .map(|(ident, ident_string)| { + quote!( + root.insert(#ident_string,#ident.to_variant()); + ) + }); + quote! { + let mut root = godot::builtin::Dictionary::new(); + #( + #fields + )* + godot::builtin::dict!{ #variant_name_string : root}.to_variant() + } +} + +fn make_enum_tuple_arm( + fields: &venial::TupleStructFields, + variant_name_string: String, +) -> TokenStream { + if fields.fields.len() == 1 { + return quote! {godot::builtin::dict! { #variant_name_string : __0}.to_variant()}; + } + let fields = fields + .fields + .iter() + .enumerate() + .map(|(k, _)| format_ident!("__{}", k)) + .map(|ident| { + quote!( + root.push(#ident.to_variant()); + ) + }); + quote! { + let mut root = godot::builtin::Array::new(); + #( + #fields + + )* + godot::builtin::dict!{ #variant_name_string: root }.to_variant() + } +} + +fn make_struct_named( + body: &mut TokenStream, + fields: &venial::NamedStructFields, + string_ident: String, +) { + let fields = fields.fields.items().map(|nf| { + let field_name = nf.name.clone(); + let field_name_string = nf.name.to_string(); + + quote!( + fields.insert(#field_name_string, self.#field_name.to_variant()); + ) + }); + + *body = quote! { + #body + let mut fields = godot::builtin::Dictionary::new(); + #( + #fields + )* + root.insert(#string_ident, fields.to_variant()); + }; +} + +fn make_struct_tuple( + body: &mut TokenStream, + fields: &venial::TupleStructFields, + string_ident: String, +) { + if fields.fields.len() == 1 { + *body = quote! { + #body + root.insert(#string_ident, self.0.to_variant()); + }; + + return; + } + let fields = fields + .fields + .iter() + .enumerate() + .map(|(k, _)| proc_macro2::Literal::usize_unsuffixed(k)) + .map(|ident| { + quote!( + fields.push(self.#ident.to_variant()); + ) + }); + + *body = quote! { + #body + let mut fields = godot::builtin::Array::new(); + #( + #fields + )* + root.insert(#string_ident, fields.to_variant()); + }; +} + +fn make_struct_unit(body: &mut TokenStream, string_ident: String) { + *body = quote! { + #body + root.insert(#string_ident, godot::builtin::Variant::nil()); + } +} diff --git a/godot-macros/src/gdextension.rs b/godot-macros/src/gdextension.rs index 43b993443..737950c4d 100644 --- a/godot-macros/src/gdextension.rs +++ b/godot-macros/src/gdextension.rs @@ -38,12 +38,12 @@ pub fn transform(decl: Declaration) -> ParseResult { #[no_mangle] unsafe extern "C" fn #entry_point( - interface: *const ::godot::sys::GDExtensionInterface, + interface_or_get_proc_address: ::godot::sys::InitCompat, library: ::godot::sys::GDExtensionClassLibraryPtr, init: *mut ::godot::sys::GDExtensionInitialization, ) -> ::godot::sys::GDExtensionBool { ::godot::init::__gdext_load_library::<#impl_ty>( - interface, + interface_or_get_proc_address, library, init ) diff --git a/godot-macros/src/lib.rs b/godot-macros/src/lib.rs index b8527497a..3d3a5aebf 100644 --- a/godot-macros/src/lib.rs +++ b/godot-macros/src/lib.rs @@ -87,7 +87,9 @@ use proc_macro2::TokenStream as TokenStream2; use quote::quote; use venial::Declaration; +mod derive_from_variant; mod derive_godot_class; +mod derive_to_variant; mod gdextension; mod godot_api; mod itest; @@ -260,6 +262,16 @@ pub fn derive_native_class(input: TokenStream) -> TokenStream { translate(input, derive_godot_class::transform) } +#[proc_macro_derive(ToVariant, attributes(variant))] +pub fn derive_to_variant(input: TokenStream) -> TokenStream { + translate(input, derive_to_variant::transform) +} + +#[proc_macro_derive(FromVariant, attributes(variant))] +pub fn derive_from_variant(input: TokenStream) -> TokenStream { + translate(input, derive_from_variant::transform) +} + #[proc_macro_attribute] pub fn godot_api(_meta: TokenStream, input: TokenStream) -> TokenStream { translate(input, godot_api::transform) diff --git a/godot-macros/src/util.rs b/godot-macros/src/util.rs index be770c4b6..bec1e1c81 100644 --- a/godot-macros/src/util.rs +++ b/godot-macros/src/util.rs @@ -11,7 +11,7 @@ use proc_macro2::{Ident, Span, TokenStream, TokenTree}; use quote::spanned::Spanned; use quote::{format_ident, ToTokens}; use std::collections::HashMap; -use venial::{Attribute, Error, Function, Impl}; +use venial::{Attribute, Error, Function, GenericParamList, Impl, WhereClause}; pub fn ident(s: &str) -> Ident { format_ident!("{}", s) @@ -625,3 +625,37 @@ pub(crate) fn path_ends_with(path: &[TokenTree], expected: &str) -> bool { .map(|last| last.to_string() == expected) .unwrap_or(false) } + +pub(crate) struct DeclInfo { + pub where_: Option, + pub generic_params: Option, + pub name: proc_macro2::Ident, + pub name_string: String, +} + +pub(crate) fn decl_get_info(decl: &venial::Declaration) -> DeclInfo { + let (where_, generic_params, name, name_string) = + if let venial::Declaration::Struct(struct_) = decl { + ( + struct_.where_clause.clone(), + struct_.generic_params.clone(), + struct_.name.clone(), + struct_.name.to_string(), + ) + } else if let venial::Declaration::Enum(enum_) = decl { + ( + enum_.where_clause.clone(), + enum_.generic_params.clone(), + enum_.name.clone(), + enum_.name.to_string(), + ) + } else { + panic!("only enums and structs are supported at the moment.") + }; + DeclInfo { + where_, + generic_params, + name, + name_string, + } +} diff --git a/godot/src/lib.rs b/godot/src/lib.rs index 1d781eb57..bdb95716c 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -151,7 +151,7 @@ pub mod init { /// Export user-defined classes and methods to be called by the engine. pub mod bind { pub use super::export::{Export, TypeStringHint}; - pub use godot_macros::{godot_api, GodotClass}; + pub use godot_macros::{godot_api, FromVariant, GodotClass, ToVariant}; } /// Testing facilities (unstable). @@ -165,7 +165,7 @@ pub use godot_core::private; /// Often-imported symbols. pub mod prelude { - pub use super::bind::{godot_api, Export, GodotClass, TypeStringHint}; + pub use super::bind::{godot_api, Export, FromVariant, GodotClass, ToVariant, TypeStringHint}; pub use super::builtin::*; pub use super::builtin::{array, dict, varray}; // Re-export macros. pub use super::engine::{ diff --git a/itest/godot/.godot/global_script_class_cache.cfg b/itest/godot/.godot/global_script_class_cache.cfg index a4591ed9c..fa91f4c7a 100644 --- a/itest/godot/.godot/global_script_class_cache.cfg +++ b/itest/godot/.godot/global_script_class_cache.cfg @@ -1,4 +1,10 @@ list=Array[Dictionary]([{ +"base": &"Node", +"class": &"GDScriptTestRunner", +"icon": "", +"language": &"GDScript", +"path": "res://TestRunner.gd" +}, { "base": &"RefCounted", "class": &"TestStats", "icon": "", @@ -10,4 +16,10 @@ list=Array[Dictionary]([{ "icon": "", "language": &"GDScript", "path": "res://TestSuite.gd" +}, { +"base": &"TestSuite", +"class": &"TestSuiteSpecial", +"icon": "", +"language": &"GDScript", +"path": "res://TestSuiteSpecial.gd" }]) diff --git a/itest/godot/ManualFfiTests.gd b/itest/godot/ManualFfiTests.gd index 398f2684e..936eb0a75 100644 --- a/itest/godot/ManualFfiTests.gd +++ b/itest/godot/ManualFfiTests.gd @@ -149,4 +149,88 @@ func test_refcounted_as_object_return_from_user_func_ptrcall(): func test_custom_constructor(): var obj = CustomConstructor.construct_object(42) assert_eq(obj.val, 42) - obj.free() \ No newline at end of file + obj.free() + +func test_option_refcounted_none_varcall(): + var ffi := OptionFfiTest.new() + + var from_rust: Variant = ffi.return_option_refcounted_none() + assert_that(ffi.accept_option_refcounted_none(from_rust), "ffi.accept_option_refcounted_none(from_rust)") + + var from_gdscript: Variant = null + var mirrored: Variant = ffi.mirror_option_refcounted(from_gdscript) + assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript") + +func test_option_refcounted_none_ptrcall(): + var ffi := OptionFfiTest.new() + + var from_rust: Object = ffi.return_option_refcounted_none() + assert_that(ffi.accept_option_refcounted_none(from_rust), "ffi.accept_option_refcounted_none(from_rust)") + + var from_gdscript: Object = null + var mirrored: Object = ffi.mirror_option_refcounted(from_gdscript) + assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript") + +func test_option_refcounted_some_varcall(): + var ffi := OptionFfiTest.new() + + var from_rust: Variant = ffi.return_option_refcounted_some() + assert_that(ffi.accept_option_refcounted_some(from_rust), "ffi.accept_option_refcounted_some(from_rust)") + + var from_gdscript: Variant = RefCounted.new() + var mirrored: Variant = ffi.mirror_option_refcounted(from_gdscript) + assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript") + +func test_option_refcounted_some_ptrcall(): + var ffi := OptionFfiTest.new() + + var from_rust: Object = ffi.return_option_refcounted_some() + assert_that(ffi.accept_option_refcounted_some(from_rust), "ffi.accept_option_refcounted_some(from_rust)") + + var from_gdscript: Object = RefCounted.new() + var mirrored: Object = ffi.mirror_option_refcounted(from_gdscript) + assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript") + +func test_option_node_none_varcall(): + var ffi := OptionFfiTest.new() + + var from_rust: Variant = ffi.return_option_node_none() + assert_that(ffi.accept_option_node_none(from_rust), "ffi.accept_option_node_none(from_rust)") + + var from_gdscript: Variant = null + var mirrored: Variant = ffi.mirror_option_node(from_gdscript) + assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript") + +func test_option_node_none_ptrcall(): + var ffi := OptionFfiTest.new() + + var from_rust: Node = ffi.return_option_node_none() + assert_that(ffi.accept_option_node_none(from_rust), "ffi.accept_option_node_none(from_rust)") + + var from_gdscript: Node = null + var mirrored: Node = ffi.mirror_option_node(from_gdscript) + assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript") + +func test_option_node_some_varcall(): + var ffi := OptionFfiTest.new() + + var from_rust: Variant = ffi.return_option_node_some() + assert_that(ffi.accept_option_node_some(from_rust), "ffi.accept_option_node_some(from_rust)") + + var from_gdscript: Variant = Node.new() + var mirrored: Variant = ffi.mirror_option_node(from_gdscript) + assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript") + from_gdscript.free() + from_rust.free() + +func test_option_node_some_ptrcall(): + var ffi := OptionFfiTest.new() + + var from_rust: Node = ffi.return_option_node_some() + assert_that(ffi.accept_option_node_some(from_rust), "ffi.accept_option_node_some(from_rust)") + + var from_gdscript: Node = Node.new() + var mirrored: Node = ffi.mirror_option_node(from_gdscript) + assert_eq(mirrored, from_gdscript, "mirrored == from_gdscript") + from_gdscript.free() + from_rust.free() \ No newline at end of file diff --git a/itest/godot/SpecialTests.gd b/itest/godot/SpecialTests.gd new file mode 100644 index 000000000..0eb3dcc1d --- /dev/null +++ b/itest/godot/SpecialTests.gd @@ -0,0 +1,49 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +extends TestSuiteSpecial + +# Tests in here require specific setup/configuration that is not easily achievable through the standard +# integration testing API. +# +# Using the standard API if possible is highly preferred. + +# Test that we can call `_input_event` on a class defined in rust, as a virtual method. +# +# This tests #267, which was caused by us incorrectly handing Objects when passed as arguments to virtual +# methods. `_input_event` is the easiest such method to test. However it can only be triggered by letting a +# full physics frame pass after calling `push_unhandled_input`. Thus we cannot use the standard API for +# testing this at the moment, since we dont have any way to let frames pass in between the start and end of +# an integration test. +func test_collision_object_2d_input_event(): + var root: Node = Engine.get_main_loop().root + + var window := Window.new() + window.physics_object_picking = true + root.add_child(window) + + var collision_object := CollisionObject2DTest.new() + collision_object.input_pickable = true + + var collision_shape := CollisionShape2D.new() + collision_shape.shape = RectangleShape2D.new() + collision_object.add_child(collision_shape) + + window.add_child(collision_object) + + assert_that(not collision_object.input_event_called()) + assert_eq(collision_object.get_viewport(), null) + + var event := InputEventMouseMotion.new() + event.global_position = Vector2.ZERO + window.push_unhandled_input(event) + + # Ensure we run a full physics frame + await root.get_tree().physics_frame + + assert_that(collision_object.input_event_called()) + assert_eq(collision_object.get_viewport(), window) + + window.queue_free() + diff --git a/itest/godot/TestRunner.gd b/itest/godot/TestRunner.gd index 3e28abe2f..4d492e346 100644 --- a/itest/godot/TestRunner.gd +++ b/itest/godot/TestRunner.gd @@ -3,8 +3,12 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. extends Node +class_name GDScriptTestRunner func _ready(): + # Ensure physics is initialized, for tests that require it. + await get_tree().physics_frame + var allow_focus := true var unrecognized_args: Array = [] for arg in OS.get_cmdline_user_args(): @@ -22,9 +26,9 @@ func _ready(): var rust_runner = IntegrationTests.new() var gdscript_suites: Array = [ - preload("res://ManualFfiTests.gd").new(), - preload("res://gen/GenFfiTests.gd").new(), - preload("res://InheritTests.gd").new() + load("res://ManualFfiTests.gd").new(), + load("res://gen/GenFfiTests.gd").new(), + load("res://InheritTests.gd").new() ] var gdscript_tests: Array = [] @@ -32,7 +36,17 @@ func _ready(): for method in suite.get_method_list(): var method_name: String = method.name if method_name.begins_with("test_"): - gdscript_tests.push_back(GDScriptTestCase.new(suite, method_name)) + gdscript_tests.push_back(GDScriptExecutableTestCase.new(suite, method_name)) + + var special_case_test_suites: Array = [ + load("res://SpecialTests.gd").new(), + ] + + for suite in special_case_test_suites: + for method in suite.get_method_list(): + var method_name: String = method.name + if method_name.begins_with("test_"): + gdscript_tests.push_back(await suite.run_test(suite, method_name)) var success: bool = rust_runner.run_all_tests( gdscript_tests, @@ -55,13 +69,32 @@ class GDScriptTestCase: self.method_name = method_name self.suite_name = _suite_name(suite) + func run(): + push_error("run unimplemented") + return false + + static func _suite_name(suite: Object) -> String: + var script: GDScript = suite.get_script() + return str(script.resource_path.get_file().get_basename(), ".gd") + +# Standard test case used for whenever something can be tested by just running a gdscript function. +class GDScriptExecutableTestCase extends GDScriptTestCase: func run(): # This is a no-op if the suite doesn't have this property. suite.set("_assertion_failed", false) var result = suite.call(method_name) var ok: bool = (result == true || result == null) && !suite.get("_assertion_failed") return ok - - static func _suite_name(suite: Object) -> String: - var script: GDScript = suite.get_script() - return str(script.resource_path.get_file().get_basename(), ".gd") + +# Hardcoded test case used for special cases where the standard testing API is not sufficient. +# +# Stores the errors generated during the execution, so they can be printed when it is appropriate to do so. +# As we may not run this test case at the time we say we do in the terminal. +class GDScriptHardcodedTestCase extends GDScriptTestCase: + # Errors generated during execution of the test. + var errors: Array[String] = [] + var execution_time_seconds: float = 0 + var result: bool = false + + func run(): + return result diff --git a/itest/godot/TestSuite.gd b/itest/godot/TestSuite.gd index b766a5fca..b22969984 100644 --- a/itest/godot/TestSuite.gd +++ b/itest/godot/TestSuite.gd @@ -7,6 +7,12 @@ extends RefCounted var _assertion_failed: bool = false +func print_newline(): + printerr() + +func print_error(s: String): + push_error(s) + ## Asserts that `what` is `true`, but does not abort the test. Returns `what` so you can return ## early from the test function if the assertion failed. func assert_that(what: bool, message: String = "") -> bool: @@ -15,11 +21,11 @@ func assert_that(what: bool, message: String = "") -> bool: _assertion_failed = true - printerr() # previous line not yet broken + print_newline() # previous line not yet broken if message: - push_error("GDScript assertion failed: %s" % message) + print_error("GDScript assertion failed: %s" % message) else: - push_error("GDScript assertion failed.") + print_error("GDScript assertion failed.") return false func assert_eq(left, right, message: String = "") -> bool: @@ -28,9 +34,9 @@ func assert_eq(left, right, message: String = "") -> bool: _assertion_failed = true - printerr() # previous line not yet broken + print_newline() # previous line not yet broken if message: - push_error("GDScript assertion failed: %s\n left: %s\n right: %s" % [message, left, right]) + print_error("GDScript assertion failed: %s\n left: %s\n right: %s" % [message, left, right]) else: - push_error("GDScript assertion failed: `(left == right)`\n left: %s\n right: %s" % [left, right]) + print_error("GDScript assertion failed: `(left == right)`\n left: %s\n right: %s" % [left, right]) return false diff --git a/itest/godot/TestSuiteSpecial.gd b/itest/godot/TestSuiteSpecial.gd new file mode 100644 index 000000000..13f49412c --- /dev/null +++ b/itest/godot/TestSuiteSpecial.gd @@ -0,0 +1,34 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +class_name TestSuiteSpecial +extends TestSuite + +var errors: Array[String] = [] + +func print_newline(): + errors.push_back("") + +func print_error(s: String): + errors.push_back(s) + +# Run a special test case, generating a hardcoded test-case based on the outcome of the test. +func run_test(suite: Object, method_name: String) -> GDScriptTestRunner.GDScriptHardcodedTestCase: + var callable: Callable = Callable(suite, method_name) + + _assertion_failed = false + var start_time = Time.get_ticks_usec() + var result = await callable.call() + var end_time = Time.get_ticks_usec() + + var test_case := GDScriptTestRunner.GDScriptHardcodedTestCase.new(suite, method_name) + test_case.execution_time_seconds = float(end_time - start_time) / 1000000.0 + test_case.result = (result or result == null) and not _assertion_failed + test_case.errors = clear_errors() + return test_case + +func clear_errors() -> Array[String]: + var old_errors := errors + errors = [] + return old_errors diff --git a/itest/godot/itest.gdextension b/itest/godot/itest.gdextension index 4a9d78fe5..366028dda 100644 --- a/itest/godot/itest.gdextension +++ b/itest/godot/itest.gdextension @@ -1,5 +1,6 @@ [configuration] entry_symbol = "itest_init" +compatibility_minimum = 4.0 [libraries] linux.debug.x86_64 = "res://../../target/debug/libitest.so" diff --git a/itest/rust/src/derive_variant.rs b/itest/rust/src/derive_variant.rs new file mode 100644 index 000000000..182659631 --- /dev/null +++ b/itest/rust/src/derive_variant.rs @@ -0,0 +1,155 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use std::fmt::Debug; + +use crate::itest; +use godot::bind::FromVariant; +use godot::bind::ToVariant; +use godot::builtin::{dict, varray, FromVariant, ToVariant}; + +#[derive(FromVariant, ToVariant, PartialEq, Debug)] +struct StructUnit; + +#[derive(FromVariant, ToVariant, PartialEq, Debug)] +struct StructNewType(String); + +#[derive(FromVariant, ToVariant, PartialEq, Debug)] +struct StructTuple(String, i32); + +#[derive(FromVariant, ToVariant, PartialEq, Debug)] +struct StructNamed { + field1: String, + field2: i32, +} + +#[derive(FromVariant, ToVariant, PartialEq, Debug)] +struct StructGenWhere(T) +where + T: ToVariant + FromVariant; + +trait Bound {} + +#[derive(FromVariant, ToVariant, PartialEq, Debug)] +struct StructGenBound(T); + +#[derive(FromVariant, ToVariant, PartialEq, Debug, Clone)] +enum Uninhabited {} + +#[derive(FromVariant, ToVariant, PartialEq, Debug, Clone)] +enum Enum { + Unit, + OneTuple(i32), + Named { data: String }, + Tuple(String, i32), +} + +fn roundtrip(value: T, expected: U) +where + T: ToVariant + FromVariant + std::cmp::PartialEq + Debug, + U: ToVariant, +{ + let expected = expected.to_variant(); + + assert_eq!(value.to_variant(), expected, "testing converting to"); + assert_eq!( + value, + T::from_variant(&expected), + "testing converting back from" + ); +} + +#[itest] +fn unit_struct() { + roundtrip( + StructUnit, + dict! { "StructUnit": godot::builtin::Variant::nil() }, + ); +} + +#[itest] +fn new_type_struct() { + roundtrip( + StructNewType(String::from("five")), + dict! { "StructNewType" : "five" }, + ) +} + +#[itest] +fn tuple_struct() { + roundtrip( + StructTuple(String::from("one"), 2), + dict! { + "StructTuple": varray!["one", 2] + }, + ) +} + +#[itest] +fn named_struct() { + roundtrip( + StructNamed { + field1: String::from("four"), + field2: 5, + }, + dict! { + "StructNamed": dict! { "field1": "four", "field2": 5 } + }, + ) +} + +#[itest] +fn generics() { + roundtrip( + StructGenWhere(String::from("4")), + dict! { "StructGenWhere": "4" }, + ) +} + +impl Bound for String {} + +#[itest] +fn generics_bound() { + roundtrip( + StructGenBound(String::from("4")), + dict! { "StructGenBound": "4" }, + ) +} + +#[itest] +fn enum_unit() { + roundtrip(Enum::Unit, dict! { "Enum": "Unit" }) +} + +#[itest] +fn enum_one_tuple() { + roundtrip( + Enum::OneTuple(4), + dict! { + "Enum": dict! { "OneTuple" : 4 } + }, + ) +} + +#[itest] +fn enum_tuple() { + roundtrip( + Enum::Tuple(String::from("four"), 5), + dict! { "Enum": dict! { "Tuple" : varray!["four", 5] } }, + ) +} + +#[itest] +fn enum_named() { + roundtrip( + Enum::Named { + data: String::from("data"), + }, + dict! { + "Enum": dict!{ "Named": dict!{ "data": "data" } } + }, + ) +} diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index 276521da6..9366ea52b 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -16,6 +16,7 @@ mod builtin_test; mod callable_test; mod codegen_test; mod color_test; +mod derive_variant; mod dictionary_test; mod enum_test; mod export_test; @@ -24,6 +25,7 @@ mod init_test; mod native_structures_test; mod node_test; mod object_test; +mod option_ffi_test; mod packed_array_test; mod projection_test; mod quaternion_test; diff --git a/itest/rust/src/native_structures_test.rs b/itest/rust/src/native_structures_test.rs index c3b0f180e..e66fb0c5f 100644 --- a/itest/rust/src/native_structures_test.rs +++ b/itest/rust/src/native_structures_test.rs @@ -70,7 +70,7 @@ impl TextServerExtensionVirtual for TestTextServer { } #[itest] -fn test_native_structures() { +fn test_native_structures_codegen() { // Test construction of a few simple types. let _ = AudioFrame { left: 0.0, diff --git a/itest/rust/src/object_test.rs b/itest/rust/src/object_test.rs index 848d169e3..daa689719 100644 --- a/itest/rust/src/object_test.rs +++ b/itest/rust/src/object_test.rs @@ -95,7 +95,9 @@ fn object_user_roundtrip_write() { assert_eq!(obj.bind().value, value); let obj2 = unsafe { - Gd::::from_sys_init(|ptr| obj.move_return_ptr(ptr, sys::PtrcallType::Standard)) + Gd::::from_sys_init(|ptr| { + obj.move_return_ptr(sys::AsUninit::force_init(ptr), sys::PtrcallType::Standard) + }) }; assert_eq!(obj2.bind().value, value); } // drop @@ -336,7 +338,7 @@ fn object_engine_convert_variant_nil() { assert_eq!( Gd::::try_from_variant(&nil), - Err(VariantConversionError), + Err(VariantConversionError::BadType), "try_from_variant(&nil)" ); diff --git a/itest/rust/src/option_ffi_test.rs b/itest/rust/src/option_ffi_test.rs new file mode 100644 index 000000000..46ea2402e --- /dev/null +++ b/itest/rust/src/option_ffi_test.rs @@ -0,0 +1,87 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use godot::prelude::{godot_api, Gd, GodotClass, Node, Object, RefCounted}; +use godot::sys::GodotFfi; + +use crate::itest; + +#[itest] +fn option_some_sys_conversion() { + let v = Some(Object::new_alloc()); + let ptr = v.sys(); + + let v2 = unsafe { Option::>::from_sys(ptr) }; + assert_eq!(v2, v); + + v.unwrap().free(); +} + +#[itest] +fn option_none_sys_conversion() { + let v = None; + let ptr = v.sys(); + + let v2 = unsafe { Option::>::from_sys(ptr) }; + assert_eq!(v2, v); +} + +#[derive(GodotClass, Debug)] +#[class(base = RefCounted, init)] +struct OptionFfiTest; + +#[godot_api] +impl OptionFfiTest { + #[func] + fn return_option_refcounted_none(&self) -> Option> { + None + } + + #[func] + fn accept_option_refcounted_none(&self, value: Option>) -> bool { + value.is_none() + } + + #[func] + fn return_option_refcounted_some(&self) -> Option> { + Some(RefCounted::new()) + } + + #[func] + fn accept_option_refcounted_some(&self, value: Option>) -> bool { + value.is_some() + } + + #[func] + fn mirror_option_refcounted(&self, value: Option>) -> Option> { + value + } + + #[func] + fn return_option_node_none(&self) -> Option> { + None + } + + #[func] + fn accept_option_node_none(&self, value: Option>) -> bool { + value.is_none() + } + + #[func] + fn return_option_node_some(&self) -> Option> { + Some(Node::new_alloc()) + } + + #[func] + fn accept_option_node_some(&self, value: Option>) -> bool { + value.is_some() + } + + #[func] + fn mirror_option_node(&self, value: Option>) -> Option> { + value + } +} diff --git a/itest/rust/src/runner.rs b/itest/rust/src/runner.rs index 70c452f73..0bf63a6ca 100644 --- a/itest/rust/src/runner.rs +++ b/itest/rust/src/runner.rs @@ -7,8 +7,9 @@ use std::time::{Duration, Instant}; use godot::bind::{godot_api, GodotClass}; -use godot::builtin::{ToVariant, Variant, VariantArray}; +use godot::builtin::{Array, GodotString, ToVariant, Variant, VariantArray}; use godot::engine::Node; +use godot::log::godot_error; use godot::obj::Gd; use crate::{RustTestCase, TestContext}; @@ -57,8 +58,8 @@ impl IntegrationTests { self.run_rust_tests(rust_tests, scene_tree); let rust_time = clock.elapsed(); let gdscript_time = if !focus_run { - self.run_gdscript_tests(gdscript_tests); - Some(clock.elapsed() - rust_time) + let extra_duration = self.run_gdscript_tests(gdscript_tests); + Some((clock.elapsed() - rust_time) + extra_duration) } else { None }; @@ -79,22 +80,31 @@ impl IntegrationTests { } } - fn run_gdscript_tests(&mut self, tests: VariantArray) { + fn run_gdscript_tests(&mut self, tests: VariantArray) -> Duration { let mut last_file = None; + let mut extra_duration = Duration::new(0, 0); + for test in tests.iter_shared() { let test_file = get_property(&test, "suite_name"); let test_case = get_property(&test, "method_name"); print_test_pre(&test_case, test_file, &mut last_file, true); let result = test.call("run", &[]); + if let Some(duration) = get_execution_time(&test) { + extra_duration += duration; + } let success = result.try_to::().unwrap_or_else(|_| { panic!("GDScript test case {test} returned non-bool: {result}") }); + for error in get_errors(&test).iter_shared() { + godot_error!("{error}"); + } let outcome = TestOutcome::from_bool(success); self.update_stats(&outcome); print_test_post(&test_case, outcome); } + extra_duration } fn conclude( @@ -224,6 +234,20 @@ fn get_property(test: &Variant, property: &str) -> String { test.call("get", &[property.to_variant()]).to::() } +fn get_execution_time(test: &Variant) -> Option { + let seconds = test + .call("get", &["execution_time_seconds".to_variant()]) + .try_to::() + .ok()?; + Some(Duration::from_secs_f64(seconds)) +} + +fn get_errors(test: &Variant) -> Array { + test.call("get", &["errors".to_variant()]) + .try_to::>() + .unwrap_or(Array::new()) +} + #[must_use] enum TestOutcome { Passed, diff --git a/itest/rust/src/variant_test.rs b/itest/rust/src/variant_test.rs index 9809e669a..83af5b26c 100644 --- a/itest/rust/src/variant_test.rs +++ b/itest/rust/src/variant_test.rs @@ -297,20 +297,23 @@ fn variant_null_object_is_nil() { fn variant_conversion_fails() { assert_eq!( "hello".to_variant().try_to::(), - Err(VariantConversionError) + Err(VariantConversionError::BadType) + ); + assert_eq!( + 28.to_variant().try_to::(), + Err(VariantConversionError::BadType) ); - assert_eq!(28.to_variant().try_to::(), Err(VariantConversionError)); assert_eq!( 10.to_variant().try_to::(), - Err(VariantConversionError) + Err(VariantConversionError::BadType) ); assert_eq!( false.to_variant().try_to::(), - Err(VariantConversionError) + Err(VariantConversionError::BadType) ); assert_eq!( VariantArray::default().to_variant().try_to::(), - Err(VariantConversionError) + Err(VariantConversionError::BadType) ); //assert_eq!( // Dictionary::default().to_variant().try_to::(), @@ -318,7 +321,7 @@ fn variant_conversion_fails() { //); assert_eq!( Variant::nil().to_variant().try_to::(), - Err(VariantConversionError) + Err(VariantConversionError::BadType) ); } diff --git a/itest/rust/src/virtual_methods_test.rs b/itest/rust/src/virtual_methods_test.rs index 54314f17c..22ae44793 100644 --- a/itest/rust/src/virtual_methods_test.rs +++ b/itest/rust/src/virtual_methods_test.rs @@ -19,7 +19,7 @@ use godot::engine::resource_loader::CacheMode; use godot::engine::{ BoxMesh, InputEvent, InputEventAction, Node, Node2D, Node2DVirtual, NodeVirtual, PrimitiveMesh, PrimitiveMeshVirtual, RefCounted, RefCountedVirtual, ResourceFormatLoader, - ResourceFormatLoaderVirtual, ResourceLoader, Window, + ResourceFormatLoaderVirtual, ResourceLoader, RigidBody2DVirtual, Viewport, Window, }; use godot::obj::{Base, Gd, Share}; use godot::private::class_macros::assert_eq_approx; @@ -389,8 +389,7 @@ fn test_virtual_method_with_return() { ); } -// TODO: Fix memory leak in this test. -#[itest(skip)] +#[itest] fn test_format_loader(_test_context: &TestContext) { let format_loader = Gd::::new_default(); let mut loader = ResourceLoader::singleton(); @@ -416,7 +415,7 @@ fn test_format_loader(_test_context: &TestContext) { fn test_input_event(test_context: &TestContext) { let obj = Gd::::new_default(); assert_eq!(obj.bind().event, None); - let test_viewport = Window::new_alloc(); + let mut test_viewport = Window::new_alloc(); test_context.scene_tree.share().add_child( test_viewport.share().upcast(), @@ -439,7 +438,51 @@ fn test_input_event(test_context: &TestContext) { .share() .push_input(event.share().upcast(), false); - assert_eq!(obj.bind().event, Some(event.upcast())); + assert_eq!(obj.bind().event, Some(event.upcast::())); + + test_viewport.queue_free(); +} + +// We were incrementing/decrementing the refcount wrong. Which only showed up if you had multiple virtual +// methods handle the same refcounted object. Related to https://github.com/godot-rust/gdext/issues/257. +#[itest] +fn test_input_event_multiple(test_context: &TestContext) { + let mut objs = Vec::new(); + for _ in 0..5 { + let obj = Gd::::new_default(); + assert_eq!(obj.bind().event, None); + objs.push(obj); + } + let mut test_viewport = Window::new_alloc(); + + test_context.scene_tree.share().add_child( + test_viewport.share().upcast(), + false, + InternalMode::INTERNAL_MODE_DISABLED, + ); + + for obj in objs.iter() { + test_viewport.share().add_child( + obj.share().upcast(), + false, + InternalMode::INTERNAL_MODE_DISABLED, + ) + } + + let mut event = InputEventAction::new(); + event.set_action("debug".into()); + event.set_pressed(true); + + // We're running in headless mode, so Input.parse_input_event does not work + test_viewport + .share() + .push_input(event.share().upcast(), false); + + for obj in objs.iter() { + assert_eq!(obj.bind().event, Some(event.share().upcast::())); + } + + test_viewport.queue_free(); } #[itest] @@ -463,3 +506,35 @@ fn test_notifications() { ); obj.free(); } + +// Used in `test_collision_object_2d_input_event` in `SpecialTests.gd`. +#[derive(GodotClass)] +#[class(init, base = RigidBody2D)] +pub struct CollisionObject2DTest { + input_event_called: bool, + viewport: Option>, +} + +#[godot_api] +impl RigidBody2DVirtual for CollisionObject2DTest { + fn input_event(&mut self, viewport: Gd, _event: Gd, _shape_idx: i64) { + self.input_event_called = true; + self.viewport = Some(viewport); + } +} + +#[godot_api] +impl CollisionObject2DTest { + #[func] + fn input_event_called(&self) -> bool { + self.input_event_called + } + + #[func] + fn get_viewport(&self) -> Variant { + self.viewport + .as_ref() + .map(ToVariant::to_variant) + .unwrap_or(Variant::nil()) + } +}