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()) + } +}