diff --git a/.github/composite/godot-itest/action.yml b/.github/composite/godot-itest/action.yml index 7045fd458..4a383eb14 100644 --- a/.github/composite/godot-itest/action.yml +++ b/.github/composite/godot-itest/action.yml @@ -56,6 +56,11 @@ inputs: default: '' description: "If provided, acts as an argument for '--target', and results in output files written to ./target/{target}" + rust-target-dir: + required: false + default: '' + description: "If provided, determines where to find the generated dylib. Example: 'release' if Godot editor build would look for debug otherwise." + rust-cache-key: required: false default: '' @@ -151,6 +156,7 @@ runs: env: RUSTFLAGS: ${{ inputs.rust-env-rustflags }} TARGET: ${{ inputs.rust-target }} + OUTPUT_DIR: ${{ inputs.rust-target-dir || 'debug' }} run: | targetArgs="" if [[ -n "$TARGET" ]]; then @@ -162,8 +168,23 @@ runs: # Instead of modifying .gdextension, rename the output directory. if [[ -n "$TARGET" ]]; then - rm -rf target/debug - mv target/$TARGET/debug target + rm -rf target/debug target/release + echo "Build output (tree -L 2 target/$TARGET/$OUTPUT_DIR):" + tree -L 2 target/$TARGET/$OUTPUT_DIR || echo "(tree not installed)" + mv target/$TARGET/$OUTPUT_DIR target/ || { + echo "::error::Output dir $TARGET/$OUTPUT_DIR not found" + exit 1 + } + # echo "Intermediate dir (tree -L 2 target):" + # tree -L 2 target || echo "(tree not installed)" + if [[ "$OUTPUT_DIR" != "debug" ]]; then + mv target/$OUTPUT_DIR target/debug + fi + echo "Resulting dir (tree -L 2 target):" + tree -L 2 target || echo "(tree not installed)" + # echo "itest.gdextension contents:" + # cat itest/godot/itest.gdextension + # echo "----------------------------------------------------" fi shell: bash diff --git a/.github/workflows/full-ci.yml b/.github/workflows/full-ci.yml index a11adf0a6..17dbebb25 100644 --- a/.github/workflows/full-ci.yml +++ b/.github/workflows/full-ci.yml @@ -378,14 +378,14 @@ jobs: godot-prebuilt-patch: '4.2.2' hot-reload: api-4-2 - # Memory checks: special Godot binaries compiled with AddressSanitizer/LeakSanitizer to detect UB/leaks. + # Memory checks: special Godot binaries compiled with AddressSanitizer/LeakSanitizer to detect UB/leaks. Always Linux. # See also https://rustc-dev-guide.rust-lang.org/sanitizers.html. # # Additionally, the Godot source is patched to make dlclose() a no-op, as unloading dynamic libraries loses stacktrace and # cause false positives like println!. See https://github.com/google/sanitizers/issues/89. # # There is also a gcc variant besides clang, which is currently not used. - - name: linux-memcheck-nightly + - name: memcheck-nightly os: ubuntu-22.04 artifact-name: linux-memcheck-nightly godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san @@ -395,7 +395,28 @@ jobs: # Sanitizers can't build proc-macros and build scripts; with --target, cargo ignores RUSTFLAGS for those two. rust-target: x86_64-unknown-linux-gnu - - name: linux-memcheck-4.2 + - name: memcheck-release-disengaged + os: ubuntu-22.04 + artifact-name: linux-memcheck-nightly + godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san + rust-toolchain: nightly + rust-env-rustflags: -Zrandomize-layout -Zsanitizer=address + # Currently without itest/codegen-full-experimental. + rust-extra-args: --release --features godot/safeguards-release-disengaged + rust-target: x86_64-unknown-linux-gnu + rust-target-dir: release # We're running Godot debug build with Rust release dylib. + + - name: memcheck-dev-balanced + os: ubuntu-22.04 + artifact-name: linux-memcheck-nightly + godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san + rust-toolchain: nightly + rust-env-rustflags: -Zrandomize-layout -Zsanitizer=address + # Currently without itest/codegen-full-experimental. + rust-extra-args: --features godot/safeguards-dev-balanced + rust-target: x86_64-unknown-linux-gnu + + - name: memcheck-4.2 os: ubuntu-22.04 artifact-name: linux-memcheck-4.2 godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san @@ -419,6 +440,7 @@ jobs: rust-toolchain: ${{ matrix.rust-toolchain || 'stable' }} rust-env-rustflags: ${{ matrix.rust-env-rustflags }} rust-target: ${{ matrix.rust-target }} + rust-target-dir: ${{ matrix.rust-target-dir }} rust-cache-key: ${{ matrix.rust-cache-key }} with-llvm: ${{ contains(matrix.name, 'macos') && contains(matrix.rust-extra-args, 'api-custom') }} godot-check-header: ${{ matrix.godot-check-header }} @@ -500,7 +522,7 @@ jobs: - name: "Determine success or failure" run: | DEPENDENCIES='${{ toJson(needs) }}' - + echo "Dependency jobs:" all_success=true for job in $(echo "$DEPENDENCIES" | jq -r 'keys[]'); do @@ -510,7 +532,7 @@ jobs: all_success=false fi done - + if [[ "$all_success" == "false" ]]; then echo "One or more dependency jobs failed or were cancelled." exit 1 diff --git a/.github/workflows/minimal-ci.yml b/.github/workflows/minimal-ci.yml index 691f7669f..ae02c0bce 100644 --- a/.github/workflows/minimal-ci.yml +++ b/.github/workflows/minimal-ci.yml @@ -210,9 +210,9 @@ jobs: godot-binary: godot.linuxbsd.editor.dev.x86_64 godot-prebuilt-patch: '4.2.2' - # Memory checkers + # Memory checkers (always Linux). - - name: linux-memcheck + - name: memcheck os: ubuntu-22.04 artifact-name: linux-memcheck-nightly godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san @@ -222,6 +222,27 @@ jobs: # Sanitizers can't build proc-macros and build scripts; with --target, cargo ignores RUSTFLAGS for those two. rust-target: x86_64-unknown-linux-gnu + - name: memcheck-release-disengaged + os: ubuntu-22.04 + artifact-name: linux-memcheck-nightly + godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san + rust-toolchain: nightly + rust-env-rustflags: -Zrandomize-layout -Zsanitizer=address + # Currently without itest/codegen-full-experimental. + rust-extra-args: --release --features godot/safeguards-release-disengaged + rust-target: x86_64-unknown-linux-gnu + rust-target-dir: release # We're running Godot debug build with Rust release dylib. + + - name: memcheck-dev-balanced + os: ubuntu-22.04 + artifact-name: linux-memcheck-nightly + godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san + rust-toolchain: nightly + rust-env-rustflags: -Zrandomize-layout -Zsanitizer=address + # Currently without itest/codegen-full-experimental. + rust-extra-args: --features godot/safeguards-dev-balanced + rust-target: x86_64-unknown-linux-gnu + steps: - uses: actions/checkout@v4 @@ -236,6 +257,7 @@ jobs: rust-toolchain: ${{ matrix.rust-toolchain || 'stable' }} rust-env-rustflags: ${{ matrix.rust-env-rustflags }} rust-target: ${{ matrix.rust-target }} + rust-target-dir: ${{ matrix.rust-target-dir }} with-llvm: ${{ contains(matrix.name, 'macos') && contains(matrix.rust-extra-args, 'api-custom') }} godot-check-header: ${{ matrix.godot-check-header }} godot-indirect-json: ${{ matrix.godot-indirect-json }} diff --git a/.gitignore b/.gitignore index bb87efffe..876de4335 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ exp.rs # Windows specific desktop.ini + +# Test outputs +/itest/rust/src/register_tests/res/actual_registered_docs.xml diff --git a/Cargo.toml b/Cargo.toml index 60fe25aa1..e93fac674 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ which = "7" # * quote: 1.0.37 allows tokenizing CStr. # * venial: 0.6.1 contains some bugfixes. heck = "0.5" -litrs = "0.4" +litrs = { version = "1.0", features = ["proc-macro2"] } markdown = "=1.0.0-alpha.23" nanoserde = "0.2" proc-macro2 = "1.0.80" diff --git a/Changelog.md b/Changelog.md index c99f09692..f3f603fc3 100644 --- a/Changelog.md +++ b/Changelog.md @@ -10,12 +10,67 @@ Cutting-edge API docs of the `master` branch are available [here](https://godot- ## Quick navigation -- [v0.4.0](#v040) +- [v0.4.0](#v040), [v0.4.1](#v041), [v0.4.2](#v042) - [v0.3.0](#v030), [v0.3.1](#v031), [v0.3.2](#v032), [v0.3.3](#v033), [v0.3.4](#v034), [v0.3.5](#v035) - [v0.2.0](#v020), [v0.2.1](#v021), [v0.2.2](#v022), [v0.2.3](#v023), [v0.2.4](#v024) - [v0.1.1](#v011), [v0.1.2](#v012), [v0.1.3](#v013) +## [v0.4.2](https://docs.rs/godot/0.4.2) + +_26 October 2025_ + +### 🌻 Features + +- Simple API to fetch autoloads ([#1381](https://github.com/godot-rust/gdext/pull/1381)) +- Experimental support for required parameters/returns in Godot APIs ([#1383](https://github.com/godot-rust/gdext/pull/1383)) + +### 🧹 Quality of life + +- `ExtensionLibrary::on_main_loop_*`: merge into new `on_stage_init/deinit` API ([#1380](https://github.com/godot-rust/gdext/pull/1380)) +- Rename builtin `hash()` -> `hash_u32()`; add tests ([#1366](https://github.com/godot-rust/gdext/pull/1366)) + +### 🛠️ Bugfixes + +- Backport Godot fix for incorrect `Glyph` native-struct ([#1369](https://github.com/godot-rust/gdext/pull/1369)) +- Validate call params for `gd_self` virtual methods ([#1382](https://github.com/godot-rust/gdext/pull/1382)) +- Fix codegen regression: `Array>` -> `Array` ([#1385](https://github.com/godot-rust/gdext/pull/1385)) + + +## [v0.4.1](https://docs.rs/godot/0.4.1) + +_23 October 2025_ + +### 🌻 Features + +- Add main loop callbacks to `ExtensionLibrary` ([#1313](https://github.com/godot-rust/gdext/pull/1313), [#1380](https://github.com/godot-rust/gdext/pull/1380)) +- Class Docs – register docs in `#[godot_api(secondary)]`, simplify docs registration logic ([#1355](https://github.com/godot-rust/gdext/pull/1355)) +- Codegen: support sys types in engine APIs ([#1363](https://github.com/godot-rust/gdext/pull/1363), [#1365](https://github.com/godot-rust/gdext/pull/1365)) + +### 📈 Performance + +- Use Rust `str` instead of `CStr` in `ClassIdSource` ([#1334](https://github.com/godot-rust/gdext/pull/1334)) + +### 🧹 Quality of life + +- Preserve doc comments for signal ([#1353](https://github.com/godot-rust/gdext/pull/1353)) +- Provide error context for typed array clone check ([#1348](https://github.com/godot-rust/gdext/pull/1348)) +- Improve spans; use tuple type for virtual signatures ([#1370](https://github.com/godot-rust/gdext/pull/1370)) +- Preserve span of arguments for better compile errors ([#1373](https://github.com/godot-rust/gdext/pull/1373)) +- Update to litrs 1.0 ([#1377](https://github.com/godot-rust/gdext/pull/1377)) +- Allow opening itests in editor ([#1379](https://github.com/godot-rust/gdext/pull/1379)) + +### 🛠️ Bugfixes + +- Ease `AsArg>>` bounds to make it usable with signals ([#1371](https://github.com/godot-rust/gdext/pull/1371)) +- Handle panic in OnReady `auto_init` ([#1351](https://github.com/godot-rust/gdext/pull/1351)) +- Update `GFile::read_as_gstring_entire()` after Godot removes `skip_cr` parameter ([#1349](https://github.com/godot-rust/gdext/pull/1349)) +- Fix `Callable::from_sync_fn` doc example using deprecated `Result` return ([#1347](https://github.com/godot-rust/gdext/pull/1347)) +- Deprecate `#[class(no_init)]` for editor plugins ([#1378](https://github.com/godot-rust/gdext/pull/1378)) +- Initialize and cache proper return value for generic, typed array ([#1357](https://github.com/godot-rust/gdext/pull/1357)) +- Fix hot-reload crashes on macOS when the `.gdextension` file changes ([#1367](https://github.com/godot-rust/gdext/pull/1367)) + + ## [v0.4.0](https://docs.rs/godot/0.4.0) _29 September 2025_ diff --git a/check.sh b/check.sh index b377bfc3a..44a98926c 100755 --- a/check.sh +++ b/check.sh @@ -142,7 +142,13 @@ function findGodot() { # builtins like `test`. function cmd_fmt() { - run cargo fmt --all -- --check + # Run rustfmt in nightly toolchain if available. + if [[ $(rustup toolchain list) =~ nightly ]]; then + run cargo +nightly fmt --all -- --check + else + log -e "${YELLOW}Warning: nightly toolchain not found; stable rustfmt might not pass CI.${END}" + run cargo fmt --all -- --check + fi } function cmd_clippy() { diff --git a/godot-bindings/Cargo.toml b/godot-bindings/Cargo.toml index 8f5980866..4b70ab396 100644 --- a/godot-bindings/Cargo.toml +++ b/godot-bindings/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot-bindings" -version = "0.4.0" +version = "0.4.2" edition = "2021" rust-version = "1.87" license = "MPL-2.0" @@ -33,6 +33,10 @@ api-custom = ["dep:bindgen", "dep:regex", "dep:which"] api-custom-json = ["dep:nanoserde", "dep:bindgen", "dep:regex", "dep:which"] api-custom-extheader = [] +# Safeguard levels (see godot/lib.rs for detailed documentation). +safeguards-dev-balanced = [] +safeguards-release-disengaged = [] + [dependencies] gdextension-api = { workspace = true } diff --git a/godot-bindings/build.rs b/godot-bindings/build.rs index edceaa9f3..dbd915cf1 100644 --- a/godot-bindings/build.rs +++ b/godot-bindings/build.rs @@ -10,7 +10,7 @@ // It's the only purpose of this build.rs file. If a better solution is found, this file can be removed. #[rustfmt::skip] -fn main() { +fn main() { let mut count = 0; if cfg!(feature = "api-custom") { count += 1; } if cfg!(feature = "api-custom-json") { count += 1; } diff --git a/godot-bindings/src/lib.rs b/godot-bindings/src/lib.rs index 5e3346010..464fd3983 100644 --- a/godot-bindings/src/lib.rs +++ b/godot-bindings/src/lib.rs @@ -267,3 +267,29 @@ pub fn before_api(major_minor: &str) -> bool { pub fn since_api(major_minor: &str) -> bool { !before_api(major_minor) } + +pub fn emit_safeguard_levels() { + // Levels: disengaged (0), balanced (1), strict (2) + let mut safeguards_level = if cfg!(debug_assertions) { 2 } else { 1 }; + + // Override default level with Cargo feature, in dev/release profiles. + #[cfg(debug_assertions)] + if cfg!(feature = "safeguards-dev-balanced") { + safeguards_level = 1; + } + #[cfg(not(debug_assertions))] + if cfg!(feature = "safeguards-release-disengaged") { + safeguards_level = 0; + } + + println!(r#"cargo:rustc-check-cfg=cfg(safeguards_balanced)"#); + println!(r#"cargo:rustc-check-cfg=cfg(safeguards_strict)"#); + + // Emit #[cfg]s cumulatively: strict builds get both balanced and strict. + if safeguards_level >= 1 { + println!(r#"cargo:rustc-cfg=safeguards_balanced"#); + } + if safeguards_level >= 2 { + println!(r#"cargo:rustc-cfg=safeguards_strict"#); + } +} diff --git a/godot-cell/Cargo.toml b/godot-cell/Cargo.toml index 35d4e2697..9a3d30d4a 100644 --- a/godot-cell/Cargo.toml +++ b/godot-cell/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot-cell" -version = "0.4.0" +version = "0.4.2" edition = "2021" rust-version = "1.87" license = "MPL-2.0" diff --git a/godot-cell/src/cell.rs b/godot-cell/src/cell.rs index 3885751ba..7c525e6cc 100644 --- a/godot-cell/src/cell.rs +++ b/godot-cell/src/cell.rs @@ -10,7 +10,6 @@ use std::error::Error; use std::marker::PhantomPinned; use std::pin::Pin; use std::ptr::NonNull; -use std::sync::Mutex; use crate::borrow_state::BorrowState; use crate::guards::{InaccessibleGuard, MutGuard, RefGuard}; @@ -77,7 +76,7 @@ impl GdCell { #[derive(Debug)] pub(crate) struct GdCellInner { /// The mutable state of this cell. - pub(crate) state: Mutex>, + pub(crate) state: UnsafeCell>, /// The actual value we're handing out references to, uses `UnsafeCell` as we're passing out `&mut` /// references to its contents even when we only have a `&` reference to the cell. value: UnsafeCell, @@ -89,12 +88,12 @@ impl GdCellInner { /// Creates a new cell storing `value`. pub fn new(value: T) -> Pin> { let cell = Box::pin(Self { - state: Mutex::new(CellState::new()), + state: UnsafeCell::new(CellState::new()), value: UnsafeCell::new(value), _pin: PhantomPinned, }); - cell.state.lock().unwrap().initialize_ptr(&cell.value); + unsafe { (&mut *cell.state.get()).initialize_ptr(&cell.value) } cell } @@ -103,20 +102,26 @@ impl GdCellInner { /// /// Fails if an accessible mutable reference exists. pub fn borrow(self: Pin<&Self>) -> Result, Box> { - let mut state = self.state.lock().unwrap(); - state.borrow_state.increment_shared()?; + { + let state = unsafe { &mut *self.state.get() }; + state.borrow_state.increment_shared()?; + } + + let state = unsafe { &*self.state.get() }; + let value = state.get_ptr(); // SAFETY: `increment_shared` succeeded, therefore there cannot currently be any accessible mutable // references. - unsafe { Ok(RefGuard::new(&self.get_ref().state, state.get_ptr())) } + unsafe { Ok(RefGuard::new(self.state.get(), value)) } } /// Returns a new mutable reference to the contents of the cell. /// /// Fails if an accessible mutable reference exists, or a shared reference exists. pub fn borrow_mut(self: Pin<&Self>) -> Result, Box> { - let mut state = self.state.lock().unwrap(); + let state = unsafe { &mut *self.state.get() }; state.borrow_state.increment_mut()?; + let count = state.borrow_state.mut_count(); let value = state.get_ptr(); @@ -131,7 +136,7 @@ impl GdCellInner { // If `make_inaccessible` is called and succeeds, then a mutable reference from this guard is passed // in. In which case, we cannot use this guard again until the resulting inaccessible guard is // dropped. - unsafe { Ok(MutGuard::new(&self.get_ref().state, count, value)) } + unsafe { Ok(MutGuard::new(state, count, value)) } } /// Make the current mutable borrow inaccessible, thus freeing the value up to be reborrowed again. @@ -144,7 +149,7 @@ impl GdCellInner { self: Pin<&'cell Self>, current_ref: &'val mut T, ) -> Result, Box> { - InaccessibleGuard::new(&self.get_ref().state, current_ref) + InaccessibleGuard::new(self.state.get(), current_ref) } /// Returns `true` if there are any mutable or shared references, regardless of whether the mutable @@ -157,14 +162,14 @@ impl GdCellInner { /// cell hands out a new borrow before it is destroyed. So we still need to ensure that this cannot /// happen at the same time. pub fn is_currently_bound(self: Pin<&Self>) -> bool { - let state = self.state.lock().unwrap(); + let state = unsafe { &*self.state.get() }; state.borrow_state.shared_count() > 0 || state.borrow_state.mut_count() > 0 } /// Similar to [`Self::is_currently_bound`] but only counts mutable references and ignores shared references. pub(crate) fn is_currently_mutably_bound(self: Pin<&Self>) -> bool { - let state = self.state.lock().unwrap(); + let state = unsafe { &*self.state.get() }; state.borrow_state.mut_count() > 0 } diff --git a/godot-cell/src/guards.rs b/godot-cell/src/guards.rs index c38661c70..ddd90e354 100644 --- a/godot-cell/src/guards.rs +++ b/godot-cell/src/guards.rs @@ -4,10 +4,9 @@ * 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::marker::PhantomData; use std::ops::{Deref, DerefMut}; use std::ptr::NonNull; -use std::sync::{Mutex, MutexGuard}; use crate::cell::CellState; @@ -19,10 +18,11 @@ use crate::cell::CellState; #[derive(Debug)] pub struct RefGuard<'a, T> { /// The current state of borrows to the borrowed value. - state: &'a Mutex>, + state: *mut CellState, /// A pointer to the borrowed value. value: NonNull, + _phantoms: PhantomData<&'a ()>, } impl<'a, T> RefGuard<'a, T> { @@ -40,8 +40,12 @@ impl<'a, T> RefGuard<'a, T> { /// /// These conditions ensure that it is safe to call [`as_ref()`](NonNull::as_ref) on `value` for as long /// as the returned guard exists. - pub(crate) unsafe fn new(state: &'a Mutex>, value: NonNull) -> Self { - Self { state, value } + pub(crate) unsafe fn new(state: *mut CellState, value: NonNull) -> Self { + Self { + state, + value, + _phantoms: PhantomData, + } } } @@ -56,8 +60,7 @@ impl Deref for RefGuard<'_, T> { impl Drop for RefGuard<'_, T> { fn drop(&mut self) { - self.state - .lock() + unsafe { self.state.as_mut() } .unwrap() .borrow_state .decrement_shared() @@ -74,7 +77,7 @@ impl Drop for RefGuard<'_, T> { /// reference handed out by this guard. #[derive(Debug)] pub struct MutGuard<'a, T> { - state: &'a Mutex>, + state: &'a mut CellState, count: usize, value: NonNull, } @@ -109,11 +112,7 @@ impl<'a, T> MutGuard<'a, T> { /// prevent any new references from being made. /// - When it is made inaccessible, [`GdCell`](super::GdCell) will also ensure that any new references /// are derived from this guard's `value` pointer, thus preventing `value` from being invalidated. - pub(crate) unsafe fn new( - state: &'a Mutex>, - count: usize, - value: NonNull, - ) -> Self { + pub(crate) unsafe fn new(state: &'a mut CellState, count: usize, value: NonNull) -> Self { Self { state, count, @@ -126,7 +125,7 @@ impl Deref for MutGuard<'_, T> { type Target = T; fn deref(&self) -> &Self::Target { - let count = self.state.lock().unwrap().borrow_state.mut_count(); + let count = self.state.borrow_state.mut_count(); // This is just a best-effort error check. It should never be triggered. assert_eq!( self.count, @@ -152,7 +151,7 @@ impl Deref for MutGuard<'_, T> { impl DerefMut for MutGuard<'_, T> { fn deref_mut(&mut self) -> &mut Self::Target { - let count = self.state.lock().unwrap().borrow_state.mut_count(); + let count = self.state.borrow_state.mut_count(); // This is just a best-effort error check. It should never be triggered. assert_eq!( self.count, @@ -179,12 +178,7 @@ impl DerefMut for MutGuard<'_, T> { impl Drop for MutGuard<'_, T> { fn drop(&mut self) { - self.state - .lock() - .unwrap() - .borrow_state - .decrement_mut() - .unwrap(); + self.state.borrow_state.decrement_mut().unwrap(); } } @@ -199,9 +193,10 @@ impl Drop for MutGuard<'_, T> { /// is dropped, it resets the state to what it was before, as if this guard never existed. #[derive(Debug)] pub struct InaccessibleGuard<'a, T> { - state: &'a Mutex>, + state: *mut CellState, stack_depth: usize, prev_ptr: NonNull, + _phantoms: PhantomData<&'a ()>, } impl<'a, T> InaccessibleGuard<'a, T> { @@ -216,15 +211,15 @@ impl<'a, T> InaccessibleGuard<'a, T> { /// - There are any shared references. /// - `new_ref` is not equal to the pointer in `state`. pub(crate) fn new<'b>( - state: &'a Mutex>, + state: *mut CellState, new_ref: &'b mut T, ) -> Result> where 'a: 'b, { - let mut guard = state.lock().unwrap(); + let cell_state = unsafe { state.as_mut() }.unwrap(); - let current_ptr = guard.get_ptr(); + let current_ptr = cell_state.get_ptr(); let new_ptr = NonNull::from(new_ref); if current_ptr != new_ptr { @@ -232,23 +227,21 @@ impl<'a, T> InaccessibleGuard<'a, T> { return Err("wrong reference passed in".into()); } - guard.borrow_state.set_inaccessible()?; - let prev_ptr = guard.get_ptr(); - let stack_depth = guard.push_ptr(new_ptr); + cell_state.borrow_state.set_inaccessible()?; + let prev_ptr = cell_state.get_ptr(); + let stack_depth = cell_state.push_ptr(new_ptr); Ok(Self { state, stack_depth, prev_ptr, + _phantoms: PhantomData, }) } /// Single implementation of drop-logic for use in both drop implementations. - fn perform_drop( - mut state: MutexGuard<'_, CellState>, - prev_ptr: NonNull, - stack_depth: usize, - ) { + fn perform_drop(state: *mut CellState, prev_ptr: NonNull, stack_depth: usize) { + let state = unsafe { state.as_mut() }.unwrap(); if state.stack_depth != stack_depth { state .borrow_state @@ -266,11 +259,11 @@ impl<'a, T> InaccessibleGuard<'a, T> { #[doc(hidden)] pub fn try_drop(self) -> Result<(), std::mem::ManuallyDrop> { let manual = std::mem::ManuallyDrop::new(self); - let state = manual.state.lock().unwrap(); + let state = unsafe { manual.state.as_mut() }.unwrap(); if !state.borrow_state.may_unset_inaccessible() || state.stack_depth != manual.stack_depth { return Err(manual); } - Self::perform_drop(state, manual.prev_ptr, manual.stack_depth); + Self::perform_drop(manual.state, manual.prev_ptr, manual.stack_depth); Ok(()) } @@ -280,7 +273,6 @@ impl Drop for InaccessibleGuard<'_, T> { fn drop(&mut self) { // Default behavior of drop-logic simply panics and poisons the cell on failure. This is appropriate // for single-threaded code where no errors should happen here. - let state = self.state.lock().unwrap(); - Self::perform_drop(state, self.prev_ptr, self.stack_depth); + Self::perform_drop(self.state, self.prev_ptr, self.stack_depth); } } diff --git a/godot-codegen/Cargo.toml b/godot-codegen/Cargo.toml index f8a2f6137..eafd1f320 100644 --- a/godot-codegen/Cargo.toml +++ b/godot-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot-codegen" -version = "0.4.0" +version = "0.4.2" edition = "2021" rust-version = "1.87" license = "MPL-2.0" @@ -20,9 +20,10 @@ api-custom = ["godot-bindings/api-custom"] api-custom-json = ["godot-bindings/api-custom-json"] experimental-godot-api = [] experimental-threads = [] +experimental-required-objs = [] [dependencies] -godot-bindings = { path = "../godot-bindings", version = "=0.4.0" } +godot-bindings = { path = "../godot-bindings", version = "=0.4.2" } heck = { workspace = true } nanoserde = { workspace = true } @@ -31,7 +32,7 @@ quote = { workspace = true } regex = { workspace = true } [build-dependencies] -godot-bindings = { path = "../godot-bindings", version = "=0.4.0" } # emit_godot_version_cfg +godot-bindings = { path = "../godot-bindings", version = "=0.4.2" } # emit_godot_version_cfg # https://docs.rs/about/metadata [package.metadata.docs.rs] diff --git a/godot-codegen/src/context.rs b/godot-codegen/src/context.rs index 9a62fbbef..8419e2b05 100644 --- a/godot-codegen/src/context.rs +++ b/godot-codegen/src/context.rs @@ -252,6 +252,14 @@ impl<'a> Context<'a> { .unwrap_or_else(|| panic!("did not register table index for key {key:?}")) } + /// Yields cached sys pointer types – various pointer types declared in `gdextension_interface` + /// and used as parameters in exposed Godot APIs. + pub fn cached_sys_pointer_types(&self) -> impl Iterator { + self.cached_rust_types + .values() + .filter(|rust_ty| rust_ty.is_sys_pointer()) + } + /// Whether an interface trait is generated for a class. /// /// False if the class is "Godot-abstract"/final, thus there are no virtual functions to inherit. diff --git a/godot-codegen/src/conv/type_conversions.rs b/godot-codegen/src/conv/type_conversions.rs index 5e55d922b..d75a453d1 100644 --- a/godot-codegen/src/conv/type_conversions.rs +++ b/godot-codegen/src/conv/type_conversions.rs @@ -67,7 +67,11 @@ fn to_hardcoded_rust_ident(full_ty: &GodotTy) -> Option<&str> { ("real_t", None) => "real", ("void", None) => "c_void", - (ty, Some(meta)) => panic!("unhandled type {ty:?} with meta {meta:?}"), + // meta="required" is a special case of non-null object parameters/return types. + // Other metas are unrecognized. + (ty, Some(meta)) if meta != "required" => { + panic!("unhandled type {ty:?} with meta {meta:?}") + } _ => return None, }; @@ -170,6 +174,18 @@ fn to_rust_type_uncached(full_ty: &GodotTy, ctx: &mut Context) -> RustTy { ty = ty.replace("const ", ""); } + // Sys pointer type defined in `gdextension_interface` and used as param for given method, e.g. `GDExtensionInitializationFunction`. + // Note: we branch here to avoid clashes with actual GDExtension classes. + if ty.starts_with("GDExtension") { + let ty = rustify_ty(&ty); + return RustTy::RawPointer { + inner: Box::new(RustTy::SysPointerType { + tokens: quote! { sys::#ty }, + }), + is_const, + }; + } + // .trim() is necessary here, as Godot places a space between a type and the stars when representing a double pointer. // Example: "int*" but "int **". let inner_type = to_rust_type(ty.trim(), None, ctx); @@ -216,8 +232,11 @@ fn to_rust_type_uncached(full_ty: &GodotTy, ctx: &mut Context) -> RustTy { elem_type: quote! { Array<#rust_elem_ty> }, } } else { + // In Array, store Gd and not Option elements. + let without_option = rust_elem_ty.tokens_non_null(); + RustTy::EngineArray { - tokens: quote! { Array<#rust_elem_ty> }, + tokens: quote! { Array<#without_option> }, elem_class: elem_ty.to_string(), } }; @@ -232,13 +251,30 @@ fn to_rust_type_uncached(full_ty: &GodotTy, ctx: &mut Context) -> RustTy { arg_passing: ctx.get_builtin_arg_passing(full_ty), } } else { - let ty = rustify_ty(ty); - let qualified_class = quote! { crate::classes::#ty }; + let is_nullable = if cfg!(feature = "experimental-required-objs") { + full_ty.meta.as_ref().is_none_or(|m| m != "required") + } else { + true + }; + + let inner_class = rustify_ty(ty); + let qualified_class = quote! { crate::classes::#inner_class }; + + // Stores unwrapped Gd directly in `gd_tokens`. + let gd_tokens = quote! { Gd<#qualified_class> }; + + // Use Option for `impl_as_object_arg` if nullable. + let impl_as_object_arg = if is_nullable { + quote! { impl AsArg>> } + } else { + quote! { impl AsArg> } + }; RustTy::EngineClass { - tokens: quote! { Gd<#qualified_class> }, - impl_as_object_arg: quote! { impl AsArg>> }, - inner_class: ty, + gd_tokens, + impl_as_object_arg, + inner_class, + is_nullable, } } } diff --git a/godot-codegen/src/generator/builtins.rs b/godot-codegen/src/generator/builtins.rs index 321fd0a8b..bfdcc9acf 100644 --- a/godot-codegen/src/generator/builtins.rs +++ b/godot-codegen/src/generator/builtins.rs @@ -197,14 +197,27 @@ fn make_special_builtin_methods(class_name: &TyName, _ctx: &Context) -> TokenStr /// Get the safety docs of an unsafe method, or `None` if it is safe. fn method_safety_doc(class_name: &TyName, method: &BuiltinMethod) -> Option { - if class_name.godot_ty == "Array" - && &method.return_value().type_tokens().to_string() == "VariantArray" - { - return Some(quote! { - /// # Safety - /// - /// You must ensure that the returned array fulfils the safety invariants of [`Array`](crate::builtin::Array). - }); + if class_name.godot_ty == "Array" { + if method.is_generic() { + return Some(quote! { + /// # Safety + /// You must ensure that the returned array fulfils the safety invariants of [`Array`](crate::builtin::Array), this being: + /// - Any values written to the array must match the runtime type of the array. + /// - Any values read from the array must be convertible to the type `T`. + /// + /// If the safety invariant of `Array` is intact, which it must be for any publicly accessible arrays, then `T` must match + /// the runtime type of the array. This then implies that both of the conditions above hold. This means that you only need + /// to keep the above conditions in mind if you are intentionally violating the safety invariant of `Array`. + /// + /// In the current implementation, both cases will produce a panic rather than undefined behavior, but this should not be relied upon. + }); + } else if &method.return_value().type_tokens().to_string() == "VariantArray" { + return Some(quote! { + /// # Safety + /// + /// You must ensure that the returned array fulfils the safety invariants of [`Array`](crate::builtin::Array). + }); + } } None @@ -252,11 +265,13 @@ fn make_builtin_method_definition( let receiver = functions_common::make_receiver(method.qualifier(), ffi_arg_in); let object_ptr = &receiver.ffi_arg; + let maybe_generic_params = method.return_value().generic_params(); + let ptrcall_invocation = quote! { let method_bind = sys::builtin_method_table().#fptr_access; - Signature::::out_builtin_ptrcall( + Signature::::out_builtin_ptrcall( method_bind, #builtin_name_str, #method_name_str, diff --git a/godot-codegen/src/generator/central_files.rs b/godot-codegen/src/generator/central_files.rs index 15a562e2b..5d662073a 100644 --- a/godot-codegen/src/generator/central_files.rs +++ b/godot-codegen/src/generator/central_files.rs @@ -10,6 +10,7 @@ use quote::{format_ident, quote, ToTokens}; use crate::context::Context; use crate::conv; +use crate::generator::sys_pointer_types::make_godotconvert_for_systypes; use crate::generator::{enums, gdext_build_struct}; use crate::models::domain::ExtensionApi; use crate::util::ident; @@ -60,6 +61,7 @@ pub fn make_core_central_code(api: &ExtensionApi, ctx: &mut Context) -> TokenStr let (global_enum_defs, global_reexported_enum_defs) = make_global_enums(api); let variant_type_traits = make_variant_type_enum(api, false); + let sys_types_godotconvert_impl = make_godotconvert_for_systypes(ctx); // TODO impl Clone, Debug, PartialEq, PartialOrd, Hash for VariantDispatch // TODO could use try_to().unwrap_unchecked(), since type is already verified. Also directly overload from_variant(). @@ -121,6 +123,11 @@ pub fn make_core_central_code(api: &ExtensionApi, ctx: &mut Context) -> TokenStr use crate::sys; #( #global_reexported_enum_defs )* } + + pub mod sys_pointer_types { + use crate::sys; + #( #sys_types_godotconvert_impl )* + } } } diff --git a/godot-codegen/src/generator/classes.rs b/godot-codegen/src/generator/classes.rs index 621946ef0..6d9462de3 100644 --- a/godot-codegen/src/generator/classes.rs +++ b/godot-codegen/src/generator/classes.rs @@ -192,11 +192,14 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas let (assoc_memory, assoc_dyn_memory, is_exportable) = make_bounds(class, ctx); let internal_methods = quote! { - fn __checked_id(&self) -> Option { - // SAFETY: only Option due to layout-compatibility with RawGd; it is always Some because stored in Gd which is non-null. - let rtti = unsafe { self.rtti.as_ref().unwrap_unchecked() }; - let instance_id = rtti.check_type::(); - Some(instance_id) + /// Creates a validated object for FFI boundary crossing. + /// + /// Low-level internal method. Validation (liveness/type checks) depend on safeguard level. + fn __validated_obj(&self) -> crate::obj::ValidatedObject { + // SAFETY: Self has the same layout as RawGd (object_ptr + rtti fields in same order). + let raw_gd = unsafe { std::mem::transmute::<&Self, &crate::obj::RawGd>(self) }; + + raw_gd.validated_object() } #[doc(hidden)] @@ -579,10 +582,10 @@ fn make_class_method_definition( let table_index = ctx.get_table_index(&MethodTableKey::from_class(class, method)); - let maybe_instance_id = if method.qualifier() == FnQualifier::Static { + let validated_obj = if method.qualifier() == FnQualifier::Static { quote! { None } } else { - quote! { self.__checked_id() } + quote! { Some(self.__validated_obj()) } }; let fptr_access = if cfg!(feature = "codegen-lazy-fptrs") { @@ -598,7 +601,6 @@ fn make_class_method_definition( quote! { fptr_by_index(#table_index) } }; - let object_ptr = &receiver.ffi_arg; let ptrcall_invocation = quote! { let method_bind = sys::#get_method_table().#fptr_access; @@ -606,8 +608,7 @@ fn make_class_method_definition( method_bind, #rust_class_name, #rust_method_name, - #object_ptr, - #maybe_instance_id, + #validated_obj, args, ) }; @@ -619,8 +620,7 @@ fn make_class_method_definition( method_bind, #rust_class_name, #rust_method_name, - #object_ptr, - #maybe_instance_id, + #validated_obj, args, varargs ) diff --git a/godot-codegen/src/generator/default_parameters.rs b/godot-codegen/src/generator/default_parameters.rs index 7f0ac3d82..8ec204cb2 100644 --- a/godot-codegen/src/generator/default_parameters.rs +++ b/godot-codegen/src/generator/default_parameters.rs @@ -11,7 +11,7 @@ use quote::{format_ident, quote}; use crate::generator::functions_common; use crate::generator::functions_common::{ - make_arg_expr, make_param_or_field_type, FnArgExpr, FnCode, FnKind, FnParamDecl, FnParamTokens, + make_arg_expr, make_param_or_field_type, FnArgExpr, FnCode, FnKind, FnParamDecl, }; use crate::models::domain::{FnParam, FnQualifier, Function, RustTy, TyName}; use crate::util::{ident, safe_ident}; @@ -59,15 +59,6 @@ pub fn make_function_definition_with_defaults( &default_fn_params, ); - // ExBuilder::new() constructor signature. - let FnParamTokens { - func_general_lifetime: simple_fn_lifetime, - .. - } = fns::make_params_exprs( - required_fn_params.iter().cloned(), - FnKind::ExBuilderConstructor, - ); - let return_decl = &sig.return_value().decl; // If either the builder has a lifetime (non-static/global method), or one of its parameters is a reference, @@ -119,7 +110,7 @@ pub fn make_function_definition_with_defaults( // Lifetime is set if any parameter is a reference. #[doc = #default_parameter_usage] #[inline] - #vis fn #simple_fn_name #simple_fn_lifetime ( + #vis fn #simple_fn_name ( #simple_receiver_param #( #class_method_required_params, )* ) #return_decl { diff --git a/godot-codegen/src/generator/functions_common.rs b/godot-codegen/src/generator/functions_common.rs index aa14d177b..5708bfddf 100644 --- a/godot-codegen/src/generator/functions_common.rs +++ b/godot-codegen/src/generator/functions_common.rs @@ -98,7 +98,6 @@ pub struct FnParamTokens { /// Generic argument list `<'a0, 'a1, ...>` after `type CallSig`, if available. pub callsig_lifetime_args: Option, pub arg_exprs: Vec, - pub func_general_lifetime: Option, } pub fn make_function_definition( @@ -142,7 +141,6 @@ pub fn make_function_definition( callsig_param_types: param_types, callsig_lifetime_args, arg_exprs: arg_names, - func_general_lifetime: fn_lifetime, } = if sig.is_virtual() { make_params_exprs_virtual(sig.params().iter(), sig) } else { @@ -175,11 +173,14 @@ pub fn make_function_definition( default_structs_code = TokenStream::new(); }; + let maybe_func_generic_params = sig.return_value().generic_params(); + let maybe_func_generic_bounds = sig.return_value().where_clause(); + let call_sig_decl = { let return_ty = &sig.return_value().type_tokens(); quote! { - type CallRet = #return_ty; + type CallRet #maybe_func_generic_params = #return_ty; type CallParams #callsig_lifetime_args = (#(#param_types,)*); } }; @@ -279,10 +280,12 @@ pub fn make_function_definition( quote! { #maybe_safety_doc - #vis #maybe_unsafe fn #primary_fn_name #fn_lifetime ( + #vis #maybe_unsafe fn #primary_fn_name #maybe_func_generic_params ( #receiver_param #( #params, )* - ) #return_decl { + ) #return_decl + #maybe_func_generic_bounds + { #call_sig_decl let args = (#( #arg_names, )*); @@ -357,10 +360,7 @@ pub(crate) enum FnKind { /// `call()` forwarding to `try_call()`. DelegateTry, - /// Default extender `new()` associated function -- optional receiver and required parameters. - ExBuilderConstructor, - - /// Same as [`ExBuilderConstructor`], but for a builder with an explicit lifetime. + /// Default extender `new()` associated function -- optional receiver and required parameters. Has an explicit lifetime. ExBuilderConstructorLifetimed, /// Default extender `new()` associated function -- only default parameters. @@ -446,23 +446,23 @@ pub(crate) fn make_param_or_field_type( let mut special_ty = None; let param_ty = match ty { - // Objects: impl AsArg> + // Objects: impl AsArg> or impl AsArg>>. RustTy::EngineClass { - impl_as_object_arg, - inner_class, - .. + impl_as_object_arg, .. } => { let lft = lifetimes.next(); - special_ty = Some(quote! { CowArg<#lft, Option>> }); + + // #ty is already Gd<...> or Option> depending on nullability. + special_ty = Some(quote! { CowArg<#lft, #ty> }); match decl { FnParamDecl::FnPublic => quote! { #impl_as_object_arg }, FnParamDecl::FnPublicLifetime => quote! { #impl_as_object_arg + 'a }, FnParamDecl::FnInternal => { - quote! { CowArg>> } + quote! { CowArg<#ty> } } FnParamDecl::Field => { - quote! { CowArg<'a, Option>> } + quote! { CowArg<'a, #ty> } } } } @@ -489,6 +489,7 @@ pub(crate) fn make_param_or_field_type( .. } | RustTy::BuiltinArray { .. } + | RustTy::GenericArray | RustTy::EngineArray { .. } => { let lft = lifetimes.next(); special_ty = Some(quote! { RefArg<#lft, #ty> }); @@ -572,7 +573,6 @@ pub(crate) fn make_params_exprs<'a>( // Methods relevant in the context of default parameters. Flow in this order. // Note that for builder methods of Ex* structs, there's a direct call in default_parameters.rs to the parameter manipulation methods, // bypassing this method. So one case is missing here. - FnKind::ExBuilderConstructor => (FnParamDecl::FnPublic, FnArgExpr::StoreInField), FnKind::ExBuilderConstructorLifetimed => { (FnParamDecl::FnPublicLifetime, FnArgExpr::StoreInField) } @@ -603,6 +603,36 @@ pub(crate) fn make_params_exprs<'a>( ret } +/// Returns the type for a virtual method parameter. +/// +/// Generates `Option>` instead of `Gd` for object parameters (which are currently all nullable). +/// +/// Used for consistency between virtual trait definitions and `type Sig = ...` type-safety declarations +/// (which are used to improve compile-time errors on mismatch). +pub(crate) fn make_virtual_param_type( + param_ty: &RustTy, + param_name: &Ident, + function_sig: &dyn Function, +) -> TokenStream { + match param_ty { + RustTy::EngineClass { gd_tokens, .. } => { + if special_cases::is_class_method_param_required( + function_sig.surrounding_class().unwrap(), + function_sig.godot_name(), + param_name, + ) { + // For special-cased EngineClass params, use Gd without Option. + gd_tokens.clone() + } else { + // In general, virtual methods accept Option>, since we don't know whether objects are nullable or required. + quote! { Option<#gd_tokens> } + } + } + + _ => quote! { #param_ty }, + } +} + /// For virtual functions, returns the parameter declarations, type tokens, and names. pub(crate) fn make_params_exprs_virtual<'a>( method_args: impl Iterator, @@ -614,30 +644,13 @@ pub(crate) fn make_params_exprs_virtual<'a>( let param_name = ¶m.name; let param_ty = ¶m.type_; - match ¶m.type_ { - // Virtual methods accept Option>, since we don't know whether objects are nullable or required. - RustTy::EngineClass { .. } - if !special_cases::is_class_method_param_required( - function_sig.surrounding_class().unwrap(), - function_sig.godot_name(), - param_name, - ) => - { - ret.param_decls - .push(quote! { #param_name: Option<#param_ty> }); - ret.arg_exprs.push(quote! { #param_name }); - ret.callsig_param_types.push(quote! { #param_ty }); - } + // Map parameter types (e.g. virtual functions need Option instead of Gd). + let param_ty_tokens = make_virtual_param_type(param_ty, param_name, function_sig); - // All other methods and parameter types: standard handling. - // For now, virtual methods always receive their parameter by value. - //_ => ret.push_regular(param_name, param_ty, true, false, false), - _ => { - ret.param_decls.push(quote! { #param_name: #param_ty }); - ret.arg_exprs.push(quote! { #param_name }); - ret.callsig_param_types.push(quote! { #param_ty }); - } - } + ret.param_decls + .push(quote! { #param_name: #param_ty_tokens }); + ret.arg_exprs.push(quote! { #param_name }); + ret.callsig_param_types.push(quote! { #param_ty }); } ret diff --git a/godot-codegen/src/generator/mod.rs b/godot-codegen/src/generator/mod.rs index 15b315270..e440c5411 100644 --- a/godot-codegen/src/generator/mod.rs +++ b/godot-codegen/src/generator/mod.rs @@ -28,8 +28,9 @@ pub mod method_tables; pub mod native_structures; pub mod notifications; pub mod signals; +pub mod sys_pointer_types; pub mod utility_functions; -pub mod virtual_definition_consts; +pub mod virtual_definitions; pub mod virtual_traits; // ---------------------------------------------------------------------------------------------------------------------------------------------- @@ -56,7 +57,6 @@ pub fn generate_sys_module_file(sys_gen_path: &Path, submit_fn: &mut SubmitFn) { pub mod central; pub mod gdextension_interface; pub mod interface; - pub mod virtual_consts; }; submit_fn(sys_gen_path.join("mod.rs"), code); @@ -86,12 +86,6 @@ pub fn generate_sys_classes_file( submit_fn(sys_gen_path.join(filename), code); watch.record(format!("generate_classes_{}_file", api_level.lower())); } - - // From 4.4 onward, generate table that maps all virtual methods to their known hashes. - // This allows Godot to fall back to an older compatibility function if one is not supported. - let code = virtual_definition_consts::make_virtual_consts_file(api, ctx); - submit_fn(sys_gen_path.join("virtual_consts.rs"), code); - watch.record("generate_virtual_consts_file"); } pub fn generate_sys_utilities_file( @@ -131,6 +125,7 @@ pub fn generate_core_mod_file(gen_path: &Path, submit_fn: &mut SubmitFn) { pub mod builtin_classes; pub mod utilities; pub mod native; + pub mod virtuals; }; submit_fn(gen_path.join("mod.rs"), code); diff --git a/godot-codegen/src/generator/native_structures.rs b/godot-codegen/src/generator/native_structures.rs index 435b373b8..01f7bfd50 100644 --- a/godot-codegen/src/generator/native_structures.rs +++ b/godot-codegen/src/generator/native_structures.rs @@ -219,6 +219,7 @@ fn make_native_structure_field_and_accessor( let obj = #snake_field_name.upcast(); + #[cfg(safeguards_balanced)] assert!(obj.is_instance_valid(), "provided node is dead"); let id = obj.instance_id().to_u64(); diff --git a/godot-codegen/src/generator/signals.rs b/godot-codegen/src/generator/signals.rs index ca099390f..f749fc656 100644 --- a/godot-codegen/src/generator/signals.rs +++ b/godot-codegen/src/generator/signals.rs @@ -10,6 +10,8 @@ // for these signals, and integration is slightly different due to lack of WithBaseField trait. Nonetheless, some parts could potentially // be extracted into a future crate shared by godot-codegen and godot-macros. +// TODO(v0.5): signal parameters are Gd instead of conservatively Option>, which is a bug. + use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; @@ -291,9 +293,10 @@ impl SignalParams { for param in params.iter() { let param_name = safe_ident(¶m.name.to_string()); let param_ty = ¶m.type_; + let param_ty_tokens = param_ty.tokens_non_null(); - param_list.extend(quote! { #param_name: #param_ty, }); - type_list.extend(quote! { #param_ty, }); + param_list.extend(quote! { #param_name: #param_ty_tokens, }); + type_list.extend(quote! { #param_ty_tokens, }); name_list.extend(quote! { #param_name, }); let formatted_ty = match param_ty { diff --git a/godot-codegen/src/generator/sys_pointer_types.rs b/godot-codegen/src/generator/sys_pointer_types.rs new file mode 100644 index 000000000..ef8833406 --- /dev/null +++ b/godot-codegen/src/generator/sys_pointer_types.rs @@ -0,0 +1,37 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * 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 proc_macro2::TokenStream; +use quote::quote; + +use crate::context::Context; + +/// Creates `GodotConvert`, `ToGodot` and `FromGodot` impl +/// for SysPointerTypes – various pointer types declared in `gdextension_interface` +/// and used as parameters in exposed Godot APIs. +pub fn make_godotconvert_for_systypes(ctx: &mut Context) -> Vec { + ctx.cached_sys_pointer_types().map(|sys_pointer_type| { + quote! { + impl crate::meta::GodotConvert for #sys_pointer_type { + type Via = i64; + } + + impl crate::meta::ToGodot for #sys_pointer_type { + type Pass = crate::meta::ByValue; + fn to_godot(&self) -> Self::Via { + *self as i64 + } + } + + impl crate::meta::FromGodot for #sys_pointer_type { + fn try_from_godot(via: Self::Via) -> Result { + Ok(via as Self) + } + } + } + }).collect() +} diff --git a/godot-codegen/src/generator/virtual_definition_consts.rs b/godot-codegen/src/generator/virtual_definitions.rs similarity index 61% rename from godot-codegen/src/generator/virtual_definition_consts.rs rename to godot-codegen/src/generator/virtual_definitions.rs index 9aa25f632..c8143481b 100644 --- a/godot-codegen/src/generator/virtual_definition_consts.rs +++ b/godot-codegen/src/generator/virtual_definitions.rs @@ -6,12 +6,13 @@ */ use proc_macro2::TokenStream; -use quote::quote; +use quote::{format_ident, quote}; use crate::context::Context; +use crate::generator::functions_common::make_virtual_param_type; use crate::models::domain::{Class, ClassLike, ExtensionApi, FnDirection, Function}; -pub fn make_virtual_consts_file(api: &ExtensionApi, ctx: &mut Context) -> TokenStream { +pub fn make_virtual_definitions_file(api: &ExtensionApi, ctx: &mut Context) -> TokenStream { make_virtual_hashes_for_all_classes(&api.classes, ctx) } @@ -21,7 +22,7 @@ fn make_virtual_hashes_for_all_classes(all_classes: &[Class], ctx: &mut Context) .map(|class| make_virtual_hashes_for_class(class, ctx)); quote! { - #![allow(non_snake_case, non_upper_case_globals, unused_imports)] + #![allow(non_snake_case, non_camel_case_types, non_upper_case_globals, unused_imports)] #( #modules )* } @@ -34,6 +35,12 @@ fn make_virtual_hashes_for_class(class: &Class, ctx: &mut Context) -> TokenStrea let use_base_class = if let Some(base_class) = ctx.inheritance_tree().direct_base(class_name) { quote! { pub use super::#base_class::*; + + // For type references in `Sig_*` signature tuples: + pub use crate::builtin::*; + pub use crate::classes::native::*; + pub use crate::obj::Gd; + pub use std::ffi::c_void; } } else { TokenStream::new() @@ -51,13 +58,27 @@ fn make_virtual_hashes_for_class(class: &Class, ctx: &mut Context) -> TokenStrea let rust_name = method.name_ident(); let godot_name_str = method.godot_name(); + // Generate parameter types, wrapping EngineClass in Option<> just like the trait does + let param_types = method + .params() + .iter() + .map(|param| make_virtual_param_type(¶m.type_, ¶m.name, method)); + + let rust_sig_name = format_ident!("Sig_{rust_name}"); + let sig_decl = quote! { + // Pub to allow "inheritance" from other modules. + pub type #rust_sig_name = ( #(#param_types,)* ); + }; + #[cfg(since_api = "4.4")] let constant = quote! { - pub const #rust_name: (&'static str, u32) = (#godot_name_str, #hash); + pub const #rust_name: (&str, u32) = (#godot_name_str, #hash); + #sig_decl }; #[cfg(before_api = "4.4")] let constant = quote! { - pub const #rust_name: &'static str = #godot_name_str; + pub const #rust_name: &str = #godot_name_str; + #sig_decl }; Some(constant) diff --git a/godot-codegen/src/lib.rs b/godot-codegen/src/lib.rs index 955c27e13..71d5ce128 100644 --- a/godot-codegen/src/lib.rs +++ b/godot-codegen/src/lib.rs @@ -37,7 +37,7 @@ use crate::generator::utility_functions::generate_utilities_file; use crate::generator::{ generate_core_central_file, generate_core_mod_file, generate_sys_builtin_lifecycle_file, generate_sys_builtin_methods_file, generate_sys_central_file, generate_sys_classes_file, - generate_sys_module_file, generate_sys_utilities_file, + generate_sys_module_file, generate_sys_utilities_file, virtual_definitions, }; use crate::models::domain::{ApiView, ExtensionApi}; use crate::models::json::{load_extension_api, JsonExtensionApi}; @@ -51,6 +51,17 @@ pub const IS_CODEGEN_FULL: bool = false; #[cfg(feature = "codegen-full")] pub const IS_CODEGEN_FULL: bool = true; +#[cfg(all(feature = "experimental-required-objs", before_api = "4.6"))] +fn __feature_warning() { + // Not a hard error, it's experimental anyway and allows more flexibility like this. + #[must_use = "The `experimental-required-objs` feature needs at least Godot 4.6-dev version"] + fn feature_has_no_effect() -> i32 { + 1 + } + + feature_has_no_effect(); +} + fn write_file(path: &Path, contents: String) { let dir = path.parent().unwrap(); let _ = std::fs::create_dir_all(dir); @@ -167,12 +178,6 @@ pub fn generate_core_files(core_gen_path: &Path) { // Deallocate all the JSON models; no longer needed for codegen. // drop(json_api); - generate_core_central_file(&api, &mut ctx, core_gen_path, &mut submit_fn); - watch.record("generate_central_file"); - - generate_utilities_file(&api, core_gen_path, &mut submit_fn); - watch.record("generate_utilities_file"); - // Class files -- currently output in godot-core; could maybe be separated cleaner // Note: deletes entire generated directory! generate_class_files( @@ -200,6 +205,22 @@ pub fn generate_core_files(core_gen_path: &Path) { ); watch.record("generate_native_structures_files"); + // Note – generated at the very end since context could be updated while processing other files. + // For example – SysPointerTypes (ones defined in gdextension_interface) are declared only + // as parameters for various APIs. + generate_core_central_file(&api, &mut ctx, core_gen_path, &mut submit_fn); + watch.record("generate_central_file"); + + generate_utilities_file(&api, core_gen_path, &mut submit_fn); + watch.record("generate_utilities_file"); + + // From 4.4 onward, generate table that maps all virtual methods to their known hashes. + // This allows Godot to fall back to an older compatibility function if one is not supported. + // Also expose tuple signatures of virtual methods. + let code = virtual_definitions::make_virtual_definitions_file(&api, &mut ctx); + submit_fn(core_gen_path.join("virtuals.rs"), code); + watch.record("generate_virtual_definitions"); + #[cfg(feature = "codegen-rustfmt")] { rustfmt_files(); diff --git a/godot-codegen/src/models/domain.rs b/godot-codegen/src/models/domain.rs index 7da3aa270..5154bae04 100644 --- a/godot-codegen/src/models/domain.rs +++ b/godot-codegen/src/models/domain.rs @@ -300,28 +300,40 @@ pub trait Function: fmt::Display { fn name(&self) -> &str { &self.common().name } + /// Rust name as `Ident`. Might be cached in future. fn name_ident(&self) -> Ident { safe_ident(self.name()) } + fn godot_name(&self) -> &str { &self.common().godot_name } + fn params(&self) -> &[FnParam] { &self.common().parameters } + fn return_value(&self) -> &FnReturn { &self.common().return_value } + fn is_vararg(&self) -> bool { self.common().is_vararg } + fn is_private(&self) -> bool { self.common().is_private } + fn is_virtual(&self) -> bool { matches!(self.direction(), FnDirection::Virtual { .. }) } + + fn is_generic(&self) -> bool { + matches!(self.return_value().type_, Some(RustTy::GenericArray)) + } + fn direction(&self) -> FnDirection { self.common().direction } @@ -621,6 +633,13 @@ impl FnReturn { Self::with_enum_replacements(return_value, &[], ctx) } + pub fn with_generic_builtin(generic_type: RustTy) -> Self { + Self { + decl: generic_type.return_decl(), + type_: Some(generic_type), + } + } + pub fn with_enum_replacements( return_value: &Option, replacements: EnumReplacements, @@ -657,18 +676,19 @@ impl FnReturn { pub fn type_tokens(&self) -> TokenStream { match &self.type_ { - Some(RustTy::EngineClass { tokens, .. }) => { - quote! { Option<#tokens> } - } - Some(ty) => { - quote! { #ty } - } - _ => { - quote! { () } - } + Some(ty) => ty.to_token_stream(), + _ => quote! { () }, } } + pub fn generic_params(&self) -> Option { + self.type_.as_ref()?.generic_params() + } + + pub fn where_clause(&self) -> Option { + self.type_.as_ref()?.where_clause() + } + pub fn call_result_decl(&self) -> TokenStream { let ret = self.type_tokens(); quote! { -> Result<#ret, crate::meta::error::CallError> } @@ -700,42 +720,57 @@ pub enum RustTy { /// `bool`, `Vector3i`, `Array`, `GString` BuiltinIdent { ty: Ident, arg_passing: ArgPassing }, + /// Pointers declared in `gdextension_interface` such as `sys::GDExtensionInitializationFunction` + /// used as parameters in some APIs. + SysPointerType { tokens: TokenStream }, + /// `Array` /// /// Note that untyped arrays are mapped as `BuiltinIdent("Array")`. BuiltinArray { elem_type: TokenStream }, + /// Will be included as `Array` in the generated source. + /// + /// Set by [`builtin_method_generic_ret`](crate::special_cases::builtin_method_generic_ret) + GenericArray, + /// C-style raw pointer to a `RustTy`. RawPointer { inner: Box, is_const: bool }, - /// `Array>` + /// `Array>`. Never contains `Option` elements. EngineArray { tokens: TokenStream, - #[allow(dead_code)] // only read in minimal config + + #[allow(dead_code)] // Only read in minimal config. elem_class: String, }, /// `module::Enum` or `module::Bitfield` EngineEnum { tokens: TokenStream, - /// `None` for globals - #[allow(dead_code)] // only read in minimal config + + /// `None` for globals. + #[allow(dead_code)] // Only read in minimal config. surrounding_class: Option, + is_bitfield: bool, }, /// `Gd` EngineClass { - /// Tokens with full `Gd` (e.g. used in return type position). - tokens: TokenStream, + /// Tokens with full `Gd`, never `Option>`. + gd_tokens: TokenStream, - /// Signature declaration with `impl AsArg>`. + /// Signature declaration with `impl AsArg>` or `impl AsArg>>`. impl_as_object_arg: TokenStream, - /// only inner `T` - #[allow(dead_code)] - // only read in minimal config + RustTy::default_extender_field_decl() + /// Only inner `Node`. inner_class: Ident, + + /// Whether this object parameter/return is nullable in the GDExtension API. + /// + /// Defaults to true (nullable). Only false when meta="required". + is_nullable: bool, }, /// Receiver type of default parameters extender constructor. @@ -754,8 +789,38 @@ impl RustTy { pub fn return_decl(&self) -> TokenStream { match self { - Self::EngineClass { tokens, .. } => quote! { -> Option<#tokens> }, - other => quote! { -> #other }, + Self::GenericArray => quote! { -> Array }, + _ => quote! { -> #self }, + } + } + + /// Returns tokens without `Option` wrapper, even for nullable engine classes. + /// + /// For `EngineClass`, always returns `Gd` regardless of nullability. For other types, behaves the same as `ToTokens`. + // Might also be useful to directly extract inner `gd_tokens` field. + pub fn tokens_non_null(&self) -> TokenStream { + match self { + Self::EngineClass { gd_tokens, .. } => gd_tokens.clone(), + other => other.to_token_stream(), + } + } + + pub fn generic_params(&self) -> Option { + if matches!(self, Self::GenericArray) { + Some(quote! { < Ret > }) + } else { + None + } + } + + pub fn where_clause(&self) -> Option { + if matches!(self, Self::GenericArray) { + Some(quote! { + where + Ret: crate::meta::ArrayElement, + }) + } else { + None } } @@ -770,6 +835,13 @@ impl RustTy { "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "isize" | "usize" ) } + + pub fn is_sys_pointer(&self) -> bool { + let RustTy::RawPointer { inner, .. } = self else { + return false; + }; + matches!(**inner, RustTy::SysPointerType { .. }) + } } impl ToTokens for RustTy { @@ -787,8 +859,21 @@ impl ToTokens for RustTy { } => quote! { *mut #inner }.to_tokens(tokens), RustTy::EngineArray { tokens: path, .. } => path.to_tokens(tokens), RustTy::EngineEnum { tokens: path, .. } => path.to_tokens(tokens), - RustTy::EngineClass { tokens: path, .. } => path.to_tokens(tokens), + RustTy::EngineClass { + is_nullable, + gd_tokens: path, + .. + } => { + // Return nullable-aware type: Option> if nullable, else Gd. + if *is_nullable { + quote! { Option<#path> }.to_tokens(tokens) + } else { + path.to_tokens(tokens) + } + } RustTy::ExtenderReceiver { tokens: path } => path.to_tokens(tokens), + RustTy::GenericArray => quote! { Array }.to_tokens(tokens), + RustTy::SysPointerType { tokens: path } => path.to_tokens(tokens), } } } diff --git a/godot-codegen/src/models/domain_mapping.rs b/godot-codegen/src/models/domain_mapping.rs index dbc89d0ee..2f90391e3 100644 --- a/godot-codegen/src/models/domain_mapping.rs +++ b/godot-codegen/src/models/domain_mapping.rs @@ -366,10 +366,17 @@ impl BuiltinMethod { return None; } - let return_value = method - .return_type - .as_deref() - .map(JsonMethodReturn::from_type_no_meta); + let return_value = if let Some(generic) = + special_cases::builtin_method_generic_ret(builtin_name, method) + { + generic + } else { + let return_value = &method + .return_type + .as_deref() + .map(JsonMethodReturn::from_type_no_meta); + FnReturn::new(return_value, ctx) + }; Some(Self { common: FunctionCommon { @@ -381,7 +388,7 @@ impl BuiltinMethod { parameters: FnParam::builder() .no_defaults() .build_many(&method.arguments, ctx), - return_value: FnReturn::new(&return_value, ctx), + return_value, is_vararg: method.is_vararg, is_private: false, // See 'exposed' below. Could be special_cases::is_method_private(builtin_name, &method.name), is_virtual_required: false, @@ -796,9 +803,14 @@ fn validate_enum_replacements( impl NativeStructure { pub fn from_json(json: &JsonNativeStructure) -> Self { + // Some native-struct definitions are incorrect in earlier Godot versions; this backports corrections. + let format = special_cases::get_native_struct_definition(&json.name) + .map(|s| s.to_string()) + .unwrap_or_else(|| json.format.clone()); + Self { name: json.name.clone(), - format: json.format.clone(), + format, } } } diff --git a/godot-codegen/src/models/json.rs b/godot-codegen/src/models/json.rs index 335f29146..6e3b4ad0c 100644 --- a/godot-codegen/src/models/json.rs +++ b/godot-codegen/src/models/json.rs @@ -238,6 +238,7 @@ pub struct JsonMethodArg { pub name: String, #[nserde(rename = "type")] pub type_: String, + /// Extra information about the type (e.g. which integer). Value "required" indicates non-nullable class types (Godot 4.6+). pub meta: Option, pub default_value: Option, } @@ -247,6 +248,7 @@ pub struct JsonMethodArg { pub struct JsonMethodReturn { #[nserde(rename = "type")] pub type_: String, + /// Extra information about the type (e.g. which integer). Value "required" indicates non-nullable class types (Godot 4.6+). pub meta: Option, } diff --git a/godot-codegen/src/special_cases/codegen_special_cases.rs b/godot-codegen/src/special_cases/codegen_special_cases.rs index c045e4630..222337fef 100644 --- a/godot-codegen/src/special_cases/codegen_special_cases.rs +++ b/godot-codegen/src/special_cases/codegen_special_cases.rs @@ -48,7 +48,9 @@ fn is_type_excluded(ty: &str, ctx: &mut Context) -> bool { match ty { RustTy::BuiltinIdent { .. } => false, RustTy::BuiltinArray { .. } => false, + RustTy::GenericArray => false, RustTy::RawPointer { inner, .. } => is_rust_type_excluded(inner), + RustTy::SysPointerType { .. } => true, RustTy::EngineArray { elem_class, .. } => is_class_excluded(elem_class.as_str()), RustTy::EngineEnum { surrounding_class, .. diff --git a/godot-codegen/src/special_cases/special_cases.rs b/godot-codegen/src/special_cases/special_cases.rs index 518a5c1af..af9358e72 100644 --- a/godot-codegen/src/special_cases/special_cases.rs +++ b/godot-codegen/src/special_cases/special_cases.rs @@ -33,7 +33,7 @@ use proc_macro2::Ident; use crate::conv::to_enum_type_uncached; use crate::models::domain::{ - ClassCodegenLevel, Enum, EnumReplacements, RustTy, TyName, VirtualMethodPresence, + ClassCodegenLevel, Enum, EnumReplacements, FnReturn, RustTy, TyName, VirtualMethodPresence, }; use crate::models::json::{JsonBuiltinMethod, JsonClassMethod, JsonSignal, JsonUtilityFunction}; use crate::special_cases::codegen_special_cases; @@ -97,6 +97,22 @@ pub fn is_native_struct_excluded(ty: &str) -> bool { codegen_special_cases::is_native_struct_excluded(ty) } +/// Overrides the definition string for native structures, if they have incorrect definitions in the JSON. +#[rustfmt::skip] +pub fn get_native_struct_definition(struct_name: &str) -> Option<&'static str> { + match struct_name { + // Glyph struct definition was corrected in Godot 4.6 to include missing `span_index` field. + // See https://github.com/godotengine/godot/pull/108369. + // #[cfg(before_api = "4.6")] // TODO(v0.5): enable this once upstream PR is merged. + "Glyph" => Some( + "int start = -1;int end = -1;uint8_t count = 0;uint8_t repeat = 1;uint16_t flags = 0;float x_off = 0.f;float y_off = 0.f;\ + float advance = 0.f;RID font_rid;int font_size = 0;int32_t index = 0;int span_index = -1" + ), + + _ => None, + } +} + #[rustfmt::skip] pub fn is_godot_type_deleted(godot_ty: &str) -> bool { // Note: parameter can be a class or builtin name, but also something like "enum::AESContext.Mode". @@ -742,6 +758,10 @@ pub fn is_class_method_param_required( godot_method_name: &str, param: &Ident, // Don't use `&str` to avoid to_string() allocations for each check on call-site. ) -> bool { + // TODO(v0.5): this overlaps now slightly with Godot's own "required" meta in extension_api.json. + // Having an override list can always be useful, but possibly the two inputs (here + JSON) should be evaluated at the same time, + // during JSON->Domain mapping. + // Note: magically, it's enough if a base class method is declared here; it will be picked up by derived classes. match (class_name.godot_ty.as_str(), godot_method_name) { @@ -778,6 +798,27 @@ pub fn is_builtin_method_deleted(_class_name: &TyName, method: &JsonBuiltinMetho codegen_special_cases::is_builtin_method_excluded(method) } +/// Returns some generic type – such as `GenericArray` representing `Array` – if method is marked as generic, `None` otherwise. +/// +/// Usually required to initialize the return value and cache its type (see also https://github.com/godot-rust/gdext/pull/1357). +#[rustfmt::skip] +pub fn builtin_method_generic_ret( + class_name: &TyName, + method: &JsonBuiltinMethod, +) -> Option { + match ( + class_name.rust_ty.to_string().as_str(), + method.name.as_str(), + ) { + | ("Array", "duplicate") + | ("Array", "slice") + | ("Array", "filter") + + => Some(FnReturn::with_generic_builtin(RustTy::GenericArray)), + _ => None, + } +} + /// True if signal is absent from codegen (only when surrounding class is excluded). pub fn is_signal_deleted(_class_name: &TyName, signal: &JsonSignal) -> bool { // If any argument type (a class) is excluded. diff --git a/godot-core/Cargo.toml b/godot-core/Cargo.toml index f45ccd38a..69dcc43dd 100644 --- a/godot-core/Cargo.toml +++ b/godot-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot-core" -version = "0.4.0" +version = "0.4.2" edition = "2021" rust-version = "1.87" license = "MPL-2.0" @@ -22,6 +22,7 @@ codegen-lazy-fptrs = [ double-precision = ["godot-codegen/double-precision"] experimental-godot-api = ["godot-codegen/experimental-godot-api"] experimental-threads = ["godot-ffi/experimental-threads", "godot-codegen/experimental-threads"] +experimental-required-objs = ["godot-codegen/experimental-required-objs"] experimental-wasm-nothreads = ["godot-ffi/experimental-wasm-nothreads"] debug-log = ["godot-ffi/debug-log"] trace = [] @@ -38,17 +39,21 @@ api-4-4 = ["godot-ffi/api-4-4"] api-4-5 = ["godot-ffi/api-4-5"] # ]] +# Safeguard levels (see godot/lib.rs for detailed documentation). +safeguards-dev-balanced = ["godot-ffi/safeguards-dev-balanced"] +safeguards-release-disengaged = ["godot-ffi/safeguards-release-disengaged"] + [dependencies] -godot-ffi = { path = "../godot-ffi", version = "=0.4.0" } +godot-ffi = { path = "../godot-ffi", version = "=0.4.2" } # See https://docs.rs/glam/latest/glam/index.html#feature-gates glam = { workspace = true } serde = { workspace = true, optional = true } -godot-cell = { path = "../godot-cell", version = "=0.4.0" } +godot-cell = { path = "../godot-cell", version = "=0.4.2" } [build-dependencies] -godot-bindings = { path = "../godot-bindings", version = "=0.4.0" } -godot-codegen = { path = "../godot-codegen", version = "=0.4.0" } +godot-bindings = { path = "../godot-bindings", version = "=0.4.2" } +godot-codegen = { path = "../godot-codegen", version = "=0.4.2" } # Reverse dev dependencies so doctests can use `godot::` prefix. [dev-dependencies] diff --git a/godot-core/build.rs b/godot-core/build.rs index 624c73ebf..1cb9dd503 100644 --- a/godot-core/build.rs +++ b/godot-core/build.rs @@ -18,4 +18,5 @@ fn main() { godot_bindings::emit_godot_version_cfg(); godot_bindings::emit_wasm_nothreads_cfg(); + godot_bindings::emit_safeguard_levels(); } diff --git a/godot-core/src/builtin/basis.rs b/godot-core/src/builtin/basis.rs index 23bae4563..e8bdcf702 100644 --- a/godot-core/src/builtin/basis.rs +++ b/godot-core/src/builtin/basis.rs @@ -703,22 +703,22 @@ mod test { Basis::from_euler(EulerOrder::XYZ, euler_xyz_from_rotation); let res = to_rotation.inverse() * rotation_from_xyz_computed_euler; + let col_a = res.col_a(); + let col_b = res.col_b(); + let col_c = res.col_c(); assert!( - (res.col_a() - Vector3::new(1.0, 0.0, 0.0)).length() <= 0.1, - "Double check with XYZ rot order failed, due to X {} with {deg_original_euler} using {rot_order:?}", - res.col_a(), - ); + (col_a - Vector3::new(1.0, 0.0, 0.0)).length() <= 0.1, + "Double check with XYZ rot order failed, due to A {col_a} with {deg_original_euler} using {rot_order:?}", + ); assert!( - (res.col_b() - Vector3::new(0.0, 1.0, 0.0)).length() <= 0.1, - "Double check with XYZ rot order failed, due to Y {} with {deg_original_euler} using {rot_order:?}", - res.col_b(), - ); + (col_b - Vector3::new(0.0, 1.0, 0.0)).length() <= 0.1, + "Double check with XYZ rot order failed, due to B {col_b} with {deg_original_euler} using {rot_order:?}", + ); assert!( - (res.col_c() - Vector3::new(0.0, 0.0, 1.0)).length() <= 0.1, - "Double check with XYZ rot order failed, due to Z {} with {deg_original_euler} using {rot_order:?}", - res.col_c(), - ); + (col_c - Vector3::new(0.0, 0.0, 1.0)).length() <= 0.1, + "Double check with XYZ rot order failed, due to C {col_c} with {deg_original_euler} using {rot_order:?}", + ); } #[test] diff --git a/godot-core/src/builtin/callable.rs b/godot-core/src/builtin/callable.rs index 272c79271..783c6ef93 100644 --- a/godot-core/src/builtin/callable.rs +++ b/godot-core/src/builtin/callable.rs @@ -162,10 +162,12 @@ impl Callable { F: 'static + FnMut(&[&Variant]) -> R, S: meta::AsArg, { + #[cfg(debug_assertions)] meta::arg_into_owned!(name); Self::from_fn_wrapper::(FnWrapper { rust_function, + #[cfg(debug_assertions)] name, thread_id: Some(std::thread::current().id()), linked_obj_id: None, @@ -187,10 +189,12 @@ impl Callable { F: 'static + FnMut(&[&Variant]) -> R, S: meta::AsArg, { + #[cfg(debug_assertions)] meta::arg_into_owned!(name); Self::from_fn_wrapper::(FnWrapper { rust_function, + #[cfg(debug_assertions)] name, thread_id: Some(std::thread::current().id()), linked_obj_id: Some(linked_object.instance_id()), @@ -254,10 +258,12 @@ impl Callable { F: FnMut(&[&Variant]) -> Variant, Fc: FnOnce(&Callable) -> R, { + #[cfg(debug_assertions)] meta::arg_into_owned!(name); let callable = Self::from_fn_wrapper::(FnWrapper { rust_function, + #[cfg(debug_assertions)] name, thread_id: Some(std::thread::current().id()), linked_obj_id: None, @@ -292,10 +298,12 @@ impl Callable { F: 'static + Send + Sync + FnMut(&[&Variant]) -> R, S: meta::AsArg, { + #[cfg(debug_assertions)] meta::arg_into_owned!(name); Self::from_fn_wrapper::(FnWrapper { rust_function, + #[cfg(debug_assertions)] name, thread_id: None, linked_obj_id: None, @@ -340,6 +348,7 @@ impl Callable { callable_userdata: Box::into_raw(Box::new(userdata)) as *mut std::ffi::c_void, call_func: Some(rust_callable_call_fn::), free_func: Some(rust_callable_destroy::>), + #[cfg(debug_assertions)] to_string_func: Some(rust_callable_to_string_named::), is_valid_func: Some(rust_callable_is_valid), ..Self::default_callable_custom_info() @@ -446,9 +455,13 @@ impl Callable { InstanceId::try_from_i64(id) } - /// Returns the 32-bit hash value of this callable's object. - /// - /// _Godot equivalent: `hash`_ + crate::declare_hash_u32_method! { + /// Returns the 32-bit hash value of this callable's object. + /// + /// _Godot equivalent: `hash`_ + } + + #[deprecated = "renamed to `hash_u32`"] pub fn hash(&self) -> u32 { self.as_inner().hash().try_into().unwrap() } @@ -604,6 +617,7 @@ mod custom_callable { pub(crate) struct FnWrapper { pub(super) rust_function: F, + #[cfg(debug_assertions)] pub(super) name: GString, /// `None` if the callable is multi-threaded ([`Callable::from_sync_fn`]). @@ -654,13 +668,16 @@ mod custom_callable { ) { let arg_refs: &[&Variant] = Variant::borrow_ref_slice(p_args, p_argument_count as usize); - let name = { + #[cfg(debug_assertions)] + let name = &{ let c: &C = CallableUserdata::inner_from_raw(callable_userdata); c.to_string() }; - let ctx = meta::CallContext::custom_callable(name.as_str()); + #[cfg(not(debug_assertions))] + let name = ""; + let ctx = meta::CallContext::custom_callable(name); - crate::private::handle_varcall_panic(&ctx, &mut *r_error, move || { + crate::private::handle_fallible_varcall(&ctx, &mut *r_error, move || { // Get the RustCallable again inside closure so it doesn't have to be UnwindSafe. let c: &mut C = CallableUserdata::inner_from_raw(callable_userdata); let result = c.invoke(arg_refs); @@ -681,25 +698,32 @@ mod custom_callable { { let arg_refs: &[&Variant] = Variant::borrow_ref_slice(p_args, p_argument_count as usize); - let name = { + #[cfg(debug_assertions)] + let name = &{ let w: &FnWrapper = CallableUserdata::inner_from_raw(callable_userdata); w.name.to_string() }; - let ctx = meta::CallContext::custom_callable(name.as_str()); + #[cfg(not(debug_assertions))] + let name = ""; + let ctx = meta::CallContext::custom_callable(name); - crate::private::handle_varcall_panic(&ctx, &mut *r_error, move || { + crate::private::handle_fallible_varcall(&ctx, &mut *r_error, move || { // Get the FnWrapper again inside closure so the FnMut doesn't have to be UnwindSafe. let w: &mut FnWrapper = CallableUserdata::inner_from_raw(callable_userdata); if w.thread_id .is_some_and(|tid| tid != std::thread::current().id()) { + #[cfg(debug_assertions)] + let name = &w.name; + #[cfg(not(debug_assertions))] + let name = ""; // NOTE: this panic is currently not propagated to the caller, but results in an error message and Nil return. // See comments in itest callable_call() for details. panic!( "Callable '{}' created with from_fn() must be called from the same thread it was created in.\n\ If you need to call it from any thread, use from_sync_fn() instead (requires `experimental-threads` feature).", - w.name + name ); } @@ -745,6 +769,7 @@ mod custom_callable { *r_is_valid = sys::conv::SYS_TRUE; } + #[cfg(debug_assertions)] pub unsafe extern "C" fn rust_callable_to_string_named( callable_userdata: *mut std::ffi::c_void, r_is_valid: *mut sys::GDExtensionBool, diff --git a/godot-core/src/builtin/collections/array.rs b/godot-core/src/builtin/collections/array.rs index cdbd5ba5d..561b93534 100644 --- a/godot-core/src/builtin/collections/array.rs +++ b/godot-core/src/builtin/collections/array.rs @@ -12,6 +12,7 @@ use std::{cmp, fmt}; use godot_ffi as sys; use sys::{ffi_methods, interface_fn, GodotFfi}; +use crate::builtin::iter::ArrayFunctionalOps; use crate::builtin::*; use crate::meta; use crate::meta::error::{ConvertError, FromGodotError, FromVariantError}; @@ -178,7 +179,7 @@ pub struct Array { } /// Guard that can only call immutable methods on the array. -struct ImmutableInnerArray<'a> { +pub(super) struct ImmutableInnerArray<'a> { inner: inner::InnerArray<'a>, } @@ -285,14 +286,16 @@ impl Array { self.as_inner().is_empty() } - /// Returns a 32-bit integer hash value representing the array and its contents. - /// - /// Note: Arrays with equal content will always produce identical hash values. However, the - /// reverse is not true. Returning identical hash values does not imply the arrays are equal, - /// because different arrays can have identical hash values due to hash collisions. + crate::declare_hash_u32_method! { + /// Returns a 32-bit integer hash value representing the array and its contents. + /// + /// Note: Arrays with equal content will always produce identical hash values. However, the + /// reverse is not true. Returning identical hash values does not imply the arrays are equal, + /// because different arrays can have identical hash values due to hash collisions. + } + + #[deprecated = "renamed to `hash_u32`"] pub fn hash(&self) -> u32 { - // The GDExtension interface only deals in `i64`, but the engine's own `hash()` function - // actually returns `uint32_t`. self.as_inner().hash().try_into().unwrap() } @@ -533,12 +536,11 @@ impl Array { /// To create a deep copy, use [`duplicate_deep()`][Self::duplicate_deep] instead. /// To create a new reference to the same array data, use [`clone()`][Clone::clone]. pub fn duplicate_shallow(&self) -> Self { - // SAFETY: We never write to the duplicated array, and all values read are read as `Variant`. - let duplicate: VariantArray = unsafe { self.as_inner().duplicate(false) }; + // SAFETY: duplicate() returns a typed array with the same type as Self, and all values are taken from `self` so have the right type + let duplicate: Self = unsafe { self.as_inner().duplicate(false) }; - // SAFETY: duplicate() returns a typed array with the same type as Self, and all values are taken from `self` so have the right type. - let result = unsafe { duplicate.assume_type() }; - result.with_cache(self) + // Note: cache is being set while initializing the duplicate as a return value for above call. + duplicate } /// Returns a deep copy of the array. All nested arrays and dictionaries are duplicated and @@ -548,12 +550,11 @@ impl Array { /// To create a shallow copy, use [`duplicate_shallow()`][Self::duplicate_shallow] instead. /// To create a new reference to the same array data, use [`clone()`][Clone::clone]. pub fn duplicate_deep(&self) -> Self { - // SAFETY: We never write to the duplicated array, and all values read are read as `Variant`. - let duplicate: VariantArray = unsafe { self.as_inner().duplicate(true) }; + // SAFETY: duplicate() returns a typed array with the same type as Self, and all values are taken from `self` so have the right type + let duplicate: Self = unsafe { self.as_inner().duplicate(true) }; - // SAFETY: duplicate() returns a typed array with the same type as Self, and all values are taken from `self` so have the right type. - let result = unsafe { duplicate.assume_type() }; - result.with_cache(self) + // Note: cache is being set while initializing the duplicate as a return value for above call. + duplicate } /// Returns a sub-range `begin..end` as a new `Array`. @@ -588,13 +589,10 @@ impl Array { let (begin, end) = range.signed(); let end = end.unwrap_or(i32::MAX as i64); - // SAFETY: The type of the array is `T` and we convert the returned array to an `Array` immediately. - let subarray: VariantArray = - unsafe { self.as_inner().slice(begin, end, step as i64, deep) }; + // SAFETY: slice() returns a typed array with the same type as Self, and all values are taken from `self` so have the right type. + let subarray: Self = unsafe { self.as_inner().slice(begin, end, step as i64, deep) }; - // SAFETY: slice() returns a typed array with the same type as Self. - let result = unsafe { subarray.assume_type() }; - result.with_cache(self) + subarray } /// Returns an iterator over the elements of the `Array`. Note that this takes the array @@ -611,6 +609,13 @@ impl Array { } } + /// Access to Godot's functional-programming APIs based on callables. + /// + /// Exposes Godot array methods such as `filter()`, `map()`, `reduce()` and many more. See return type docs. + pub fn functional_ops(&self) -> ArrayFunctionalOps<'_, T> { + ArrayFunctionalOps::new(self) + } + /// Returns the minimum value contained in the array if all elements are of comparable types. /// /// If the elements can't be compared or the array is empty, `None` is returned. @@ -675,6 +680,8 @@ impl Array { /// /// Calling `bsearch` on an unsorted array results in unspecified behavior. Consider using `sort()` to ensure the sorting /// order is compatible with your callable's ordering. + /// + /// See also: [`bsearch_by()`][Self::bsearch_by], [`functional_ops().bsearch_custom()`][ArrayFunctionalOps::bsearch_custom]. pub fn bsearch(&self, value: impl AsArg) -> usize { meta::arg_into_ref!(value: T); @@ -685,13 +692,15 @@ impl Array { /// /// The comparator function should return an ordering that indicates whether its argument is `Less`, `Equal` or `Greater` the desired value. /// For example, for an ascending-ordered array, a simple predicate searching for a constant value would be `|elem| elem.cmp(&4)`. - /// See also [`slice::binary_search_by()`]. + /// This follows the design of [`slice::binary_search_by()`]. /// /// If the value is found, returns `Ok(index)` with its index. Otherwise, returns `Err(index)`, where `index` is the insertion index /// that would maintain sorting order. /// - /// Calling `bsearch_by` on an unsorted array results in unspecified behavior. Consider using [`sort_by()`] to ensure - /// the sorting order is compatible with your callable's ordering. + /// Calling `bsearch_by` on an unsorted array results in unspecified behavior. Consider using [`sort_unstable_by()`][Self::sort_unstable_by] + /// to ensure the sorting order is compatible with your callable's ordering. + /// + /// See also: [`bsearch()`][Self::bsearch], [`functional_ops().bsearch_custom()`][ArrayFunctionalOps::bsearch_custom]. pub fn bsearch_by(&self, mut func: F) -> Result where F: FnMut(&T) -> cmp::Ordering + 'static, @@ -715,7 +724,7 @@ impl Array { let debug_name = std::any::type_name::(); let index = Callable::with_scoped_fn(debug_name, godot_comparator, |pred| { - self.bsearch_custom(ignored_value, pred) + self.functional_ops().bsearch_custom(ignored_value, pred) }); if let Some(value_at_index) = self.get(index) { @@ -727,22 +736,9 @@ impl Array { Err(index) } - /// Finds the index of a value in a sorted array using binary search, with `Callable` custom predicate. - /// - /// The callable `pred` takes two elements `(a, b)` and should return if `a < b` (strictly less). - /// For a type-safe version, check out [`bsearch_by()`][Self::bsearch_by]. - /// - /// If the value is not present in the array, returns the insertion index that would maintain sorting order. - /// - /// Calling `bsearch_custom` on an unsorted array results in unspecified behavior. Consider using `sort_custom()` to ensure - /// the sorting order is compatible with your callable's ordering. + #[deprecated = "Moved to `functional_ops().bsearch_custom()`"] pub fn bsearch_custom(&self, value: impl AsArg, pred: &Callable) -> usize { - meta::arg_into_ref!(value: T); - - to_usize( - self.as_inner() - .bsearch_custom(&value.to_variant(), pred, true), - ) + self.functional_ops().bsearch_custom(value, pred) } /// Reverses the order of the elements in the array. @@ -756,9 +752,11 @@ impl Array { /// Sorts the array. /// /// The sorting algorithm used is not [stable](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability). - /// This means that values considered equal may have their order changed when using `sort_unstable`. For most variant types, + /// This means that values considered equal may have their order changed when using `sort_unstable()`. For most variant types, /// this distinction should not matter though. /// + /// See also: [`sort_unstable_by()`][Self::sort_unstable_by], [`sort_unstable_custom()`][Self::sort_unstable_custom]. + /// /// _Godot equivalent: `Array.sort()`_ #[doc(alias = "sort")] pub fn sort_unstable(&mut self) { @@ -774,8 +772,10 @@ impl Array { /// elements themselves would be achieved with `|a, b| a.cmp(b)`. /// /// The sorting algorithm used is not [stable](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability). - /// This means that values considered equal may have their order changed when using `sort_unstable_by`. For most variant types, + /// This means that values considered equal may have their order changed when using `sort_unstable_by()`. For most variant types, /// this distinction should not matter though. + /// + /// See also: [`sort_unstable()`][Self::sort_unstable], [`sort_unstable_custom()`][Self::sort_unstable_custom]. pub fn sort_unstable_by(&mut self, mut func: F) where F: FnMut(&T, &T) -> cmp::Ordering, @@ -798,14 +798,14 @@ impl Array { /// Sorts the array, using type-unsafe `Callable` comparator. /// - /// For a type-safe variant of this method, use [`sort_unstable_by()`][Self::sort_unstable_by]. - /// /// The callable expects two parameters `(lhs, rhs)` and should return a bool `lhs < rhs`. /// /// The sorting algorithm used is not [stable](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability). - /// This means that values considered equal may have their order changed when using `sort_unstable_custom`.For most variant types, + /// This means that values considered equal may have their order changed when using `sort_unstable_custom()`. For most variant types, /// this distinction should not matter though. /// + /// Type-safe alternatives: [`sort_unstable()`][Self::sort_unstable] , [`sort_unstable_by()`][Self::sort_unstable_by]. + /// /// _Godot equivalent: `Array.sort_custom()`_ #[doc(alias = "sort_custom")] pub fn sort_unstable_custom(&mut self, func: &Callable) { @@ -943,15 +943,14 @@ impl Array { inner::InnerArray::from_outer_typed(self) } - fn as_inner(&self) -> ImmutableInnerArray<'_> { + pub(super) fn as_inner(&self) -> ImmutableInnerArray<'_> { ImmutableInnerArray { // SAFETY: We can only read from the array. inner: unsafe { self.as_inner_mut() }, } } - /// Changes the generic type on this array, without changing its contents. Needed for API - /// functions that return a variant array even though we know its type, and for API functions + /// Changes the generic type on this array, without changing its contents. Needed for API functions /// that take a variant array even though we want to pass a typed one. /// /// # Safety @@ -966,20 +965,13 @@ impl Array { /// Note also that any `GodotType` can be written to a `Variant` array. /// /// In the current implementation, both cases will produce a panic rather than undefined behavior, but this should not be relied upon. - unsafe fn assume_type(self) -> Array { - // The memory layout of `Array` does not depend on `T`. - std::mem::transmute::, Array>(self) - } - - /// # Safety - /// See [`assume_type`](Self::assume_type). unsafe fn assume_type_ref(&self) -> &Array { // The memory layout of `Array` does not depend on `T`. std::mem::transmute::<&Array, &Array>(self) } /// Validates that all elements in this array can be converted to integers of type `T`. - #[cfg(debug_assertions)] + #[cfg(safeguards_strict)] pub(crate) fn debug_validate_int_elements(&self) -> Result<(), ConvertError> { // SAFETY: every element is internally represented as Variant. let canonical_array = unsafe { self.assume_type_ref::() }; @@ -1001,7 +993,7 @@ impl Array { } // No-op in Release. Avoids O(n) conversion checks, but still panics on access. - #[cfg(not(debug_assertions))] + #[cfg(not(safeguards_strict))] pub(crate) fn debug_validate_int_elements(&self) -> Result<(), ConvertError> { Ok(()) } @@ -1244,7 +1236,7 @@ impl Clone for Array { let copy = unsafe { self.clone_unchecked() }; // Double-check copy's runtime type in Debug mode. - if cfg!(debug_assertions) { + if cfg!(safeguards_strict) { copy.with_checked_type() .expect("copied array should have same type as original array") } else { @@ -1645,21 +1637,25 @@ macro_rules! varray { /// This macro creates a [slice](https://doc.rust-lang.org/std/primitive.slice.html) of `Variant` values. /// /// # Examples -/// Variable number of arguments: +/// ## Variable number of arguments /// ```no_run /// # use godot::prelude::*; /// let slice: &[Variant] = vslice![42, "hello", true]; -/// /// let concat: GString = godot::global::str(slice); /// ``` -/// _(In practice, you might want to use [`godot_str!`][crate::global::godot_str] instead of `str()`.)_ +/// _In practice, you might want to use [`godot_str!`][crate::global::godot_str] instead of `str()`._ /// -/// Dynamic function call via reflection. NIL can still be passed inside `vslice!`, just use `Variant::nil()`. +/// ## Dynamic function call via reflection +/// NIL can still be passed inside `vslice!`, just use `Variant::nil()`. /// ```no_run /// # use godot::prelude::*; /// # fn some_object() -> Gd { unimplemented!() } /// let mut obj: Gd = some_object(); -/// obj.call("some_method", vslice![Vector2i::new(1, 2), Variant::nil()]); +/// +/// obj.call("some_method", vslice![ +/// Vector2i::new(1, 2), +/// Variant::nil(), +/// ]); /// ``` /// /// # See also diff --git a/godot-core/src/builtin/collections/array_functional_ops.rs b/godot-core/src/builtin/collections/array_functional_ops.rs new file mode 100644 index 000000000..12d062e2c --- /dev/null +++ b/godot-core/src/builtin/collections/array_functional_ops.rs @@ -0,0 +1,213 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * 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::builtin::{to_usize, Array, Callable, Variant, VariantArray}; +use crate::meta::{ArrayElement, AsArg}; +use crate::{meta, sys}; + +/// Immutable, functional-programming operations for `Array`, based on Godot callables. +/// +/// Returned by [`Array::functional_ops()`]. +/// +/// These methods exist to provide parity with Godot, e.g. when porting GDScript code to Rust. However, they come with several disadvantages +/// compared to Rust's [iterator adapters](https://doc.rust-lang.org/stable/core/iter/index.html#adapters): +/// - Not type-safe: callables are dynamically typed, so you need to double-check signatures. Godot may misinterpret returned values +/// (e.g. predicates apply to any "truthy" values, not just booleans). +/// - Slower: dispatching through callables is typically more costly than iterating over variants, especially since every call involves multiple +/// variant conversions, too. Combining multiple operations like `filter().map()` is very expensive due to intermediate allocations. +/// - Less composable/flexible: Godot's `map()` always returns an untyped array, even if the input is typed and unchanged by the mapping. +/// Rust's `collect()` on the other hand gives you control over the output type. Chaining iterators can apply multiple transformations lazily. +/// +/// In many cases, it is thus better to use [`Array::iter_shared()`] combined with iterator adapters. Check the individual method docs of +/// this struct for concrete alternatives. +pub struct ArrayFunctionalOps<'a, T: ArrayElement> { + array: &'a Array, +} + +impl<'a, T: ArrayElement> ArrayFunctionalOps<'a, T> { + pub(super) fn new(owner: &'a Array) -> Self { + Self { array: owner } + } + + /// Returns a new array containing only the elements for which the callable returns a truthy value. + /// + /// **Rust alternatives:** [`Iterator::filter()`]. + /// + /// The callable has signature `fn(T) -> bool`. + /// + /// # Example + /// ```no_run + /// # use godot::prelude::*; + /// let array = array![1, 2, 3, 4, 5]; + /// let even = array.functional_ops().filter(&Callable::from_fn("is_even", |args| { + /// args[0].to::() % 2 == 0 + /// })); + /// assert_eq!(even, array![2, 4]); + /// ``` + #[must_use] + pub fn filter(&self, callable: &Callable) -> Array { + // SAFETY: filter() returns array of same type as self. + unsafe { self.array.as_inner().filter(callable) } + } + + /// Returns a new untyped array with each element transformed by the callable. + /// + /// **Rust alternatives:** [`Iterator::map()`]. + /// + /// The callable has signature `fn(T) -> Variant`. Since the transformation can change the element type, this method returns + /// a `VariantArray` (untyped array). + /// + /// # Example + /// ```no_run + /// # use godot::prelude::*; + /// let array = array![1.1, 1.5, 1.9]; + /// let rounded = array.functional_ops().map(&Callable::from_fn("round", |args| { + /// args[0].to::().round() as i64 + /// })); + /// assert_eq!(rounded, varray![1, 2, 2]); + /// ``` + #[must_use] + pub fn map(&self, callable: &Callable) -> VariantArray { + // SAFETY: map() returns an untyped array. + unsafe { self.array.as_inner().map(callable) } + } + + /// Reduces the array to a single value by iteratively applying the callable. + /// + /// **Rust alternatives:** [`Iterator::fold()`] or [`Iterator::reduce()`]. + /// + /// The callable takes two arguments: the accumulator and the current element. + /// It returns the new accumulator value. The process starts with `initial` as the accumulator. + /// + /// # Example + /// ```no_run + /// # use godot::prelude::*; + /// let array = array![1, 2, 3, 4]; + /// let sum = array.functional_ops().reduce( + /// &Callable::from_fn("sum", |args| { + /// args[0].to::() + args[1].to::() + /// }), + /// &0.to_variant() + /// ); + /// assert_eq!(sum, 10.to_variant()); + /// ``` + #[must_use] + pub fn reduce(&self, callable: &Callable, initial: &Variant) -> Variant { + self.array.as_inner().reduce(callable, initial) + } + + /// Returns `true` if the callable returns a truthy value for at least one element. + /// + /// **Rust alternatives:** [`Iterator::any()`]. + /// + /// The callable has signature `fn(element) -> bool`. + /// + /// # Example + /// ```no_run + /// # use godot::prelude::*; + /// let array = array![1, 2, 3, 4]; + /// let any_even = array.functional_ops().any(&Callable::from_fn("is_even", |args| { + /// args[0].to::() % 2 == 0 + /// })); + /// assert!(any_even); + /// ``` + pub fn any(&self, callable: &Callable) -> bool { + self.array.as_inner().any(callable) + } + + /// Returns `true` if the callable returns a truthy value for all elements. + /// + /// **Rust alternatives:** [`Iterator::all()`]. + /// + /// The callable has signature `fn(element) -> bool`. + /// + /// # Example + /// ```no_run + /// # use godot::prelude::*; + /// let array = array![2, 4, 6]; + /// let all_even = array.functional_ops().all(&Callable::from_fn("is_even", |args| { + /// args[0].to::() % 2 == 0 + /// })); + /// assert!(all_even); + /// ``` + pub fn all(&self, callable: &Callable) -> bool { + self.array.as_inner().all(callable) + } + + /// Finds the index of the first element matching a custom predicate. + /// + /// **Rust alternatives:** [`Iterator::position()`]. + /// + /// The callable has signature `fn(element) -> bool`. + /// + /// Returns the index of the first element for which the callable returns a truthy value, starting from `from`. + /// If no element matches, returns `None`. + /// + /// # Example + /// ```no_run + /// # use godot::prelude::*; + /// let array = array![1, 2, 3, 4, 5]; + /// let is_even = Callable::from_fn("is_even", |args| { + /// args[0].to::() % 2 == 0 + /// }); + /// assert_eq!(array.functional_ops().find_custom(&is_even, None), Some(1)); // value 2 + /// assert_eq!(array.functional_ops().find_custom(&is_even, Some(2)), Some(3)); // value 4 + /// ``` + #[cfg(since_api = "4.4")] + pub fn find_custom(&self, callable: &Callable, from: Option) -> Option { + let from = from.map(|i| i as i64).unwrap_or(0); + let found_index = self.array.as_inner().find_custom(callable, from); + + sys::found_to_option(found_index) + } + + /// Finds the index of the last element matching a custom predicate, searching backwards. + /// + /// **Rust alternatives:** [`Iterator::rposition()`]. + /// + /// The callable has signature `fn(element) -> bool`. + /// + /// Returns the index of the last element for which the callable returns a truthy value, searching backwards from `from`. + /// If no element matches, returns `None`. + /// + /// # Example + /// ```no_run + /// # use godot::prelude::*; + /// let array = array![1, 2, 3, 4, 5]; + /// let is_even = Callable::from_fn("is_even", |args| { + /// args[0].to::() % 2 == 0 + /// }); + /// assert_eq!(array.functional_ops().rfind_custom(&is_even, None), Some(3)); // value 4 + /// assert_eq!(array.functional_ops().rfind_custom(&is_even, Some(2)), Some(1)); // value 2 + /// ``` + #[cfg(since_api = "4.4")] + pub fn rfind_custom(&self, callable: &Callable, from: Option) -> Option { + let from = from.map(|i| i as i64).unwrap_or(-1); + let found_index = self.array.as_inner().rfind_custom(callable, from); + + sys::found_to_option(found_index) + } + + /// Finds the index of a value in a sorted array using binary search, with `Callable` custom predicate. + /// + /// The callable `pred` takes two elements `(a, b)` and should return if `a < b` (strictly less). + /// For a type-safe version, check out [`Array::bsearch_by()`]. + /// + /// If the value is not present in the array, returns the insertion index that would maintain sorting order. + /// + /// Calling `bsearch_custom()` on an unsorted array results in unspecified behavior. Consider using [`Array::sort_unstable_custom()`] + /// to ensure the sorting order is compatible with your callable's ordering. + pub fn bsearch_custom(&self, value: impl AsArg, pred: &Callable) -> usize { + meta::arg_into_ref!(value: T); + + to_usize( + self.array + .as_inner() + .bsearch_custom(&value.to_variant(), pred, true), + ) + } +} diff --git a/godot-core/src/builtin/collections/dictionary.rs b/godot-core/src/builtin/collections/dictionary.rs index 830e096e5..166c48bde 100644 --- a/godot-core/src/builtin/collections/dictionary.rs +++ b/godot-core/src/builtin/collections/dictionary.rs @@ -285,7 +285,11 @@ impl Dictionary { old_value } - /// Returns a 32-bit integer hash value representing the dictionary and its contents. + crate::declare_hash_u32_method! { + /// Returns a 32-bit integer hash value representing the dictionary and its contents. + } + + #[deprecated = "renamed to `hash_u32`"] #[must_use] pub fn hash(&self) -> u32 { self.as_inner().hash().try_into().unwrap() diff --git a/godot-core/src/builtin/collections/mod.rs b/godot-core/src/builtin/collections/mod.rs index d28046dc9..5c6fade32 100644 --- a/godot-core/src/builtin/collections/mod.rs +++ b/godot-core/src/builtin/collections/mod.rs @@ -6,6 +6,7 @@ */ mod array; +mod array_functional_ops; mod dictionary; mod extend_buffer; mod packed_array; @@ -21,6 +22,7 @@ pub(crate) mod containers { // Re-export in godot::builtin::iter. #[rustfmt::skip] // Individual lines. pub(crate) mod iterators { + pub use super::array_functional_ops::ArrayFunctionalOps; pub use super::array::Iter as ArrayIter; pub use super::dictionary::Iter as DictIter; pub use super::dictionary::Keys as DictKeys; diff --git a/godot-core/src/builtin/macros.rs b/godot-core/src/builtin/macros.rs index a55e7d9ff..58938c0ef 100644 --- a/godot-core/src/builtin/macros.rs +++ b/godot-core/src/builtin/macros.rs @@ -103,7 +103,9 @@ macro_rules! impl_builtin_traits_inner { ( Hash for $Type:ty ) => { impl std::hash::Hash for $Type { fn hash(&self, state: &mut H) { - self.hash().hash(state) + // The GDExtension interface only deals in `int64_t`, but the engine's own `hash()` function + // actually returns `uint32_t`. + self.hash_u32().hash(state) } } }; diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index be0fa446a..0a97797af 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -65,6 +65,7 @@ pub use __prelude_reexport::*; pub mod math; /// Iterator types for arrays and dictionaries. +// Might rename this to `collections` or so. pub mod iter { pub use super::collections::iterators::*; } @@ -116,6 +117,16 @@ pub mod inner { pub use crate::gen::builtin_classes::*; } +#[macro_export] +macro_rules! declare_hash_u32_method { + ( $( $docs:tt )+ ) => { + $( $docs )+ + pub fn hash_u32(&self) -> u32 { + self.as_inner().hash().try_into().expect("Godot hashes are uint32_t") + } + } +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Conversion functions diff --git a/godot-core/src/builtin/string/gstring.rs b/godot-core/src/builtin/string/gstring.rs index 9bc168716..b001d4ce2 100644 --- a/godot-core/src/builtin/string/gstring.rs +++ b/godot-core/src/builtin/string/gstring.rs @@ -156,7 +156,11 @@ impl GString { self.as_inner().length().try_into().unwrap() } - /// Returns a 32-bit integer hash value representing the string. + crate::declare_hash_u32_method! { + /// Returns a 32-bit integer hash value representing the string. + } + + #[deprecated = "renamed to `hash_u32`"] pub fn hash(&self) -> u32 { self.as_inner() .hash() diff --git a/godot-core/src/builtin/string/mod.rs b/godot-core/src/builtin/string/mod.rs index aaab61279..5c10f5141 100644 --- a/godot-core/src/builtin/string/mod.rs +++ b/godot-core/src/builtin/string/mod.rs @@ -66,6 +66,7 @@ pub enum Encoding { } // ---------------------------------------------------------------------------------------------------------------------------------------------- +// Utilities fn populated_or_none(s: GString) -> Option { if s.is_empty() { @@ -75,19 +76,6 @@ fn populated_or_none(s: GString) -> Option { } } -fn found_to_option(index: i64) -> Option { - if index == -1 { - None - } else { - // If this fails, then likely because we overlooked a negative value. - let index_usize = index - .try_into() - .unwrap_or_else(|_| panic!("unexpected index {index} returned from Godot function")); - - Some(index_usize) - } -} - // ---------------------------------------------------------------------------------------------------------------------------------------------- // Padding, alignment and precision support diff --git a/godot-core/src/builtin/string/node_path.rs b/godot-core/src/builtin/string/node_path.rs index 890bdfe64..c518da209 100644 --- a/godot-core/src/builtin/string/node_path.rs +++ b/godot-core/src/builtin/string/node_path.rs @@ -117,7 +117,11 @@ impl NodePath { self.get_name_count() + self.get_subname_count() } - /// Returns a 32-bit integer hash value representing the string. + crate::declare_hash_u32_method! { + /// Returns a 32-bit integer hash value representing the node path. + } + + #[deprecated = "renamed to `hash_u32`"] pub fn hash(&self) -> u32 { self.as_inner() .hash() diff --git a/godot-core/src/builtin/string/string_macros.rs b/godot-core/src/builtin/string/string_macros.rs index 73035e004..f5fb5f403 100644 --- a/godot-core/src/builtin/string/string_macros.rs +++ b/godot-core/src/builtin/string/string_macros.rs @@ -399,7 +399,7 @@ macro_rules! impl_shared_string_api { } }; - super::found_to_option(godot_found) + sys::found_to_option(godot_found) } } diff --git a/godot-core/src/builtin/string/string_name.rs b/godot-core/src/builtin/string/string_name.rs index c15d082fe..418c29a69 100644 --- a/godot-core/src/builtin/string/string_name.rs +++ b/godot-core/src/builtin/string/string_name.rs @@ -139,7 +139,11 @@ impl StringName { self.as_inner().length() as usize } - /// Returns a 32-bit integer hash value representing the string. + crate::declare_hash_u32_method! { + /// Returns a 32-bit integer hash value representing the string. + } + + #[deprecated = "renamed to `hash_u32`"] pub fn hash(&self) -> u32 { self.as_inner() .hash() diff --git a/godot-core/src/builtin/variant/mod.rs b/godot-core/src/builtin/variant/mod.rs index 26500857b..bec3fa10b 100644 --- a/godot-core/src/builtin/variant/mod.rs +++ b/godot-core/src/builtin/variant/mod.rs @@ -310,8 +310,18 @@ impl Variant { /// Return Godot's hash value for the variant. /// /// _Godot equivalent : `@GlobalScope.hash()`_ - pub fn hash(&self) -> i64 { + pub fn hash_u32(&self) -> u32 { + // @GlobalScope.hash() actually calls the VariantUtilityFunctions::hash(&Variant) function (C++). + // This function calls the passed reference's `hash` method, which returns a uint32_t. + // Therefore, casting this function to u32 is always fine. unsafe { interface_fn!(variant_hash)(self.var_sys()) } + .try_into() + .expect("Godot hashes are uint32_t") + } + + #[deprecated = "renamed to `hash_u32` and type changed to `u32`"] + pub fn hash(&self) -> i64 { + self.hash_u32().into() } /// Interpret the `Variant` as `bool`. diff --git a/godot-core/src/classes/class_runtime.rs b/godot-core/src/classes/class_runtime.rs index f73ecada4..ef7cd0df5 100644 --- a/godot-core/src/classes/class_runtime.rs +++ b/godot-core/src/classes/class_runtime.rs @@ -8,12 +8,15 @@ //! Runtime checks and inspection of Godot classes. use crate::builtin::{GString, StringName, Variant, VariantType}; -#[cfg(debug_assertions)] +#[cfg(safeguards_strict)] use crate::classes::{ClassDb, Object}; +#[cfg(safeguards_balanced)] use crate::meta::CallContext; -#[cfg(debug_assertions)] +#[cfg(safeguards_strict)] use crate::meta::ClassId; -use crate::obj::{bounds, Bounds, Gd, GodotClass, InstanceId, RawGd, Singleton}; +#[cfg(safeguards_strict)] +use crate::obj::Singleton; +use crate::obj::{bounds, Bounds, Gd, GodotClass, InstanceId, RawGd}; use crate::sys; pub(crate) fn debug_string( @@ -191,6 +194,13 @@ where Gd::::from_obj_sys(object_ptr) } +/// Checks that the object with the given instance ID is still alive and that the pointer is valid. +/// +/// This does **not** perform type checking — use `ensure_object_type()` for that. +/// +/// # Panics (balanced+strict safeguards) +/// If the object has been freed or the instance ID points to a different object. +#[cfg(safeguards_balanced)] pub(crate) fn ensure_object_alive( instance_id: InstanceId, old_object_ptr: sys::GDExtensionObjectPtr, @@ -211,7 +221,7 @@ pub(crate) fn ensure_object_alive( ); } -#[cfg(debug_assertions)] +#[cfg(safeguards_strict)] pub(crate) fn ensure_object_inherits(derived: ClassId, base: ClassId, instance_id: InstanceId) { if derived == base || base == Object::class_id() // for Object base, anything inherits by definition @@ -226,7 +236,7 @@ pub(crate) fn ensure_object_inherits(derived: ClassId, base: ClassId, instance_i ) } -#[cfg(debug_assertions)] +#[cfg(safeguards_strict)] pub(crate) fn ensure_binding_not_null(binding: sys::GDExtensionClassInstancePtr) where T: GodotClass + Bounds, @@ -254,7 +264,7 @@ where // Implementation of this file /// Checks if `derived` inherits from `base`, using a cache for _successful_ queries. -#[cfg(debug_assertions)] +#[cfg(safeguards_strict)] fn is_derived_base_cached(derived: ClassId, base: ClassId) -> bool { use std::collections::HashSet; diff --git a/godot-core/src/deprecated.rs b/godot-core/src/deprecated.rs index bde072132..00c88f4f8 100644 --- a/godot-core/src/deprecated.rs +++ b/godot-core/src/deprecated.rs @@ -37,6 +37,12 @@ pub use crate::emit_deprecated_warning; // ---------------------------------------------------------------------------------------------------------------------------------------------- // Library-side deprecations -- see usage description above. +#[deprecated = "\n\ + #[class(no_init, base=EditorPlugin)] will crash when opened in the editor.\n\ + EditorPlugin classes are automatically instantiated by Godot and require a default constructor.\n\ + Use #[class(init)] instead, or provide a custom init() function in the IEditorPlugin impl."] +pub const fn class_no_init_editor_plugin() {} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Godot-side deprecations (we may mark them deprecated but keep support). diff --git a/godot-core/src/docs.rs b/godot-core/src/docs.rs index beaba7219..fc7bcf43b 100644 --- a/godot-core/src/docs.rs +++ b/godot-core/src/docs.rs @@ -8,7 +8,41 @@ use std::collections::HashMap; use crate::meta::ClassId; -use crate::registry::plugin::{ITraitImpl, InherentImpl, PluginItem, Struct}; +use crate::obj::GodotClass; + +/// Piece of information that is gathered by the self-registration ("plugin") system. +/// +/// You should not manually construct this struct, but rather use [`DocsPlugin::new()`]. +#[derive(Debug)] +pub struct DocsPlugin { + /// The name of the class to register docs for. + class_name: ClassId, + + /// The actual item being registered. + item: DocsItem, +} + +impl DocsPlugin { + /// Creates a new `DocsPlugin`, automatically setting the `class_name` to the values defined in [`GodotClass`]. + pub fn new(item: DocsItem) -> Self { + Self { + class_name: T::class_id(), + item, + } + } +} + +type ITraitImplDocs = &'static str; + +#[derive(Debug)] +pub enum DocsItem { + /// Docs for `#[derive(GodotClass)] struct MyClass`. + Struct(StructDocs), + /// Docs for `#[godot_api] impl MyClass`. + InherentImpl(InherentImplDocs), + /// Docs for `#[godot_api] impl ITrait for MyClass`. + ITraitImpl(ITraitImplDocs), +} /// Created for documentation on /// ```ignore @@ -26,10 +60,10 @@ pub struct StructDocs { pub description: &'static str, pub experimental: &'static str, pub deprecated: &'static str, - pub members: &'static str, + pub properties: &'static str, } -/// Keeps documentation for inherent `impl` blocks, such as: +/// Keeps documentation for inherent `impl` blocks (primary and secondary), such as: /// ```ignore /// #[godot_api] /// impl Struct { @@ -46,18 +80,22 @@ pub struct StructDocs { /// } /// ``` /// All fields are XML parts, escaped where necessary. -#[derive(Default, Copy, Clone, Debug)] +#[derive(Default, Clone, Debug)] pub struct InherentImplDocs { - pub methods: &'static str, - pub signals_block: &'static str, - pub constants_block: &'static str, + pub methods_xml: &'static str, + pub signals_xml: &'static str, + pub constants_xml: &'static str, } +/// Godot editor documentation for a class, combined from individual definitions (struct + impls). +/// +/// All fields are collections of XML parts, escaped where necessary. #[derive(Default)] -struct DocPieces { +struct AggregatedDocs { definition: StructDocs, - inherent: InherentImplDocs, - virtual_methods: &'static str, + methods_xmls: Vec<&'static str>, + signals_xmls: Vec<&'static str>, + constants_xmls: Vec<&'static str>, } /// This function scours the registered plugins to find their documentation pieces, @@ -75,65 +113,79 @@ struct DocPieces { /// strings of not-yet-parented XML tags (or empty string if no method has been documented). #[doc(hidden)] pub fn gather_xml_docs() -> impl Iterator { - let mut map = HashMap::::new(); - crate::private::iterate_plugins(|x| { - let class_name = x.class_name; + let mut map = HashMap::::new(); - match x.item { - PluginItem::InherentImpl(InherentImpl { docs, .. }) => { - map.entry(class_name).or_default().inherent = docs + crate::private::iterate_docs_plugins(|shard| { + let class_name = shard.class_name; + match &shard.item { + DocsItem::Struct(struct_docs) => { + map.entry(class_name).or_default().definition = *struct_docs; } - PluginItem::ITraitImpl(ITraitImpl { - virtual_method_docs, - .. - }) => map.entry(class_name).or_default().virtual_methods = virtual_method_docs, + DocsItem::InherentImpl(trait_docs) => { + map.entry(class_name) + .or_default() + .methods_xmls + .push(trait_docs.methods_xml); + + map.entry(class_name) + .and_modify(|pieces| pieces.constants_xmls.push(trait_docs.constants_xml)); - PluginItem::Struct(Struct { docs, .. }) => { - map.entry(class_name).or_default().definition = docs + map.entry(class_name) + .and_modify(|pieces| pieces.signals_xmls.push(trait_docs.signals_xml)); } - _ => (), + DocsItem::ITraitImpl(methods_xml) => { + map.entry(class_name) + .or_default() + .methods_xmls + .push(methods_xml); + } } }); map.into_iter().map(|(class, pieces)| { - let StructDocs { - base, - description, - experimental, - deprecated, - members, - } = pieces.definition; - - let InherentImplDocs { - methods, - signals_block, - constants_block, - } = pieces.inherent; - - let virtual_methods = pieces.virtual_methods; - let methods_block = (virtual_methods.is_empty() && methods.is_empty()) - .then(String::new) - .unwrap_or_else(|| format!("{methods}{virtual_methods}")); - - let (brief, description) = match description - .split_once("[br]") { - Some((brief, description)) => (brief, description.trim_start_matches("[br]")), - None => (description, ""), - }; - - format!(r#" + let StructDocs { + base, + description, + experimental, + deprecated, + properties, + } = pieces.definition; + + let methods_block = wrap_in_xml_block("methods", pieces.methods_xmls); + let signals_block = wrap_in_xml_block("signals", pieces.signals_xmls); + let constants_block = wrap_in_xml_block("constants", pieces.constants_xmls); + + let (brief, description) = match description.split_once("[br]") { + Some((brief, description)) => (brief, description.trim_start_matches("[br]")), + None => (description, ""), + }; + + format!(r#" {brief} {description} {methods_block} {constants_block} {signals_block} -{members} +{properties} "#) - }, - ) + }) +} + +fn wrap_in_xml_block(tag: &str, mut blocks: Vec<&'static str>) -> String { + // We sort the blocks for deterministic output. No need to sort individual methods/signals/constants, this is already done by Godot. + // See https://github.com/godot-rust/gdext/pull/1391 for more information. + blocks.sort(); + + let content = String::from_iter(blocks); + + if content.is_empty() { + String::new() + } else { + format!("<{tag}>{content}") + } } /// # Safety diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index f1ebd978c..e35b65bae 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -16,10 +16,32 @@ use crate::out; mod reexport_pub { #[cfg(not(wasm_nothreads))] pub use super::sys::main_thread_id; - pub use super::sys::{is_main_thread, GdextBuild}; + pub use super::sys::{is_main_thread, GdextBuild, InitStage}; } pub use reexport_pub::*; +#[repr(C)] +struct InitUserData { + library: sys::GDExtensionClassLibraryPtr, + #[cfg(since_api = "4.5")] + main_loop_callbacks: sys::GDExtensionMainLoopCallbacks, +} + +#[cfg(since_api = "4.5")] +unsafe extern "C" fn startup_func() { + E::on_stage_init(InitStage::MainLoop); +} + +#[cfg(since_api = "4.5")] +unsafe extern "C" fn frame_func() { + E::on_main_loop_frame(); +} + +#[cfg(since_api = "4.5")] +unsafe extern "C" fn shutdown_func() { + E::on_stage_deinit(InitStage::MainLoop); +} + #[doc(hidden)] #[deny(unsafe_op_in_unsafe_fn)] pub unsafe fn __gdext_load_library( @@ -60,10 +82,20 @@ pub unsafe fn __gdext_load_library( // Currently no way to express failure; could be exposed to E if necessary. // No early exit, unclear if Godot still requires output parameters to be set. let success = true; + // Leak the userdata. It will be dropped in core level deinitialization. + let userdata = Box::into_raw(Box::new(InitUserData { + library, + #[cfg(since_api = "4.5")] + main_loop_callbacks: sys::GDExtensionMainLoopCallbacks { + startup_func: Some(startup_func::), + frame_func: Some(frame_func::), + shutdown_func: Some(shutdown_func::), + }, + })); let godot_init_params = sys::GDExtensionInitialization { minimum_initialization_level: E::min_level().to_sys(), - userdata: std::ptr::null_mut(), + userdata: userdata.cast::(), initialize: Some(ffi_initialize_layer::), deinitialize: Some(ffi_deinitialize_layer::), }; @@ -88,13 +120,14 @@ pub unsafe fn __gdext_load_library( static LEVEL_SERVERS_CORE_LOADED: AtomicBool = AtomicBool::new(false); unsafe extern "C" fn ffi_initialize_layer( - _userdata: *mut std::ffi::c_void, + userdata: *mut std::ffi::c_void, init_level: sys::GDExtensionInitializationLevel, ) { + let userdata = userdata.cast::().as_ref().unwrap(); let level = InitLevel::from_sys(init_level); let ctx = || format!("failed to initialize GDExtension level `{level:?}`"); - fn try_load(level: InitLevel) { + fn try_load(level: InitLevel, userdata: &InitUserData) { // Workaround for https://github.com/godot-rust/gdext/issues/629: // When using editor plugins, Godot may unload all levels but only reload from Scene upward. // Manually run initialization of lower levels. @@ -102,8 +135,8 @@ unsafe extern "C" fn ffi_initialize_layer( // TODO: Remove this workaround once after the upstream issue is resolved. if level == InitLevel::Scene { if !LEVEL_SERVERS_CORE_LOADED.load(Ordering::Relaxed) { - try_load::(InitLevel::Core); - try_load::(InitLevel::Servers); + try_load::(InitLevel::Core, userdata); + try_load::(InitLevel::Servers, userdata); } } else if level == InitLevel::Core { // When it's normal initialization, the `Servers` level is normally initialized. @@ -112,18 +145,18 @@ unsafe extern "C" fn ffi_initialize_layer( // SAFETY: Godot will call this from the main thread, after `__gdext_load_library` where the library is initialized, // and only once per level. - unsafe { gdext_on_level_init(level) }; - E::on_level_init(level); + unsafe { gdext_on_level_init(level, userdata) }; + E::on_stage_init(level.to_stage()); } // Swallow panics. TODO consider crashing if gdext init fails. let _ = crate::private::handle_panic(ctx, || { - try_load::(level); + try_load::(level, userdata); }); } unsafe extern "C" fn ffi_deinitialize_layer( - _userdata: *mut std::ffi::c_void, + userdata: *mut std::ffi::c_void, init_level: sys::GDExtensionInitializationLevel, ) { let level = InitLevel::from_sys(init_level); @@ -134,9 +167,12 @@ unsafe extern "C" fn ffi_deinitialize_layer( if level == InitLevel::Core { // Once the CORE api is unloaded, reset the flag to initial state. LEVEL_SERVERS_CORE_LOADED.store(false, Ordering::Relaxed); + + // Drop the userdata. + drop(Box::from_raw(userdata.cast::())); } - E::on_level_deinit(level); + E::on_stage_deinit(level.to_stage()); gdext_on_level_deinit(level); }); } @@ -149,7 +185,7 @@ unsafe extern "C" fn ffi_deinitialize_layer( /// - The interface must have been initialized. /// - Must only be called once per level. #[deny(unsafe_op_in_unsafe_fn)] -unsafe fn gdext_on_level_init(level: InitLevel) { +unsafe fn gdext_on_level_init(level: InitLevel, userdata: &InitUserData) { // TODO: in theory, a user could start a thread in one of the early levels, and run concurrent code that messes with the global state // (e.g. class registration). This would break the assumption that the load_class_method_table() calls are exclusive. // We could maybe protect globals with a mutex until initialization is complete, and then move it to a directly-accessible, read-only static. @@ -158,6 +194,15 @@ unsafe fn gdext_on_level_init(level: InitLevel) { unsafe { sys::load_class_method_table(level) }; match level { + InitLevel::Core => { + #[cfg(since_api = "4.5")] + unsafe { + sys::interface_fn!(register_main_loop_callbacks)( + userdata.library, + &raw const userdata.main_loop_callbacks, + ) + }; + } InitLevel::Servers => { // SAFETY: called from the main thread, sys::initialized has already been called. unsafe { sys::discover_main_thread() }; @@ -173,7 +218,6 @@ unsafe fn gdext_on_level_init(level: InitLevel) { crate::docs::register(); } } - _ => (), } crate::registry::class::auto_register_classes(level); @@ -188,6 +232,7 @@ fn gdext_on_level_deinit(level: InitLevel) { // No business logic by itself, but ensures consistency if re-initialization (hot-reload on Linux) occurs. crate::task::cleanup(); + crate::tools::cleanup(); // Garbage-collect various statics. // SAFETY: this is the last time meta APIs are used. @@ -266,8 +311,12 @@ fn gdext_on_level_deinit(level: InitLevel) { /// responsible to uphold them: namely in GDScript code or other GDExtension bindings loaded by the engine. /// Violating this may cause undefined behavior, even when invoking _safe_ functions. /// +/// If you use the `disengaged` [safeguard level], you accept that UB becomes possible even **in safe Rust APIs**, if you use them wrong +/// (e.g. accessing a destroyed object). +/// /// [gdextension]: attr.gdextension.html /// [safety]: https://godot-rust.github.io/book/gdext/advanced/safety.html +/// [safeguard level]: ../index.html#safeguard-levels // FIXME intra-doc link #[doc(alias = "entry_symbol", alias = "entry_point")] pub unsafe trait ExtensionLibrary { @@ -283,26 +332,121 @@ pub unsafe trait ExtensionLibrary { InitLevel::Scene } - /// Custom logic when a certain init-level of Godot is loaded. + /// Custom logic when a certain initialization stage is loaded. + /// + /// This will be invoked for stages >= [`Self::min_level()`], in ascending order. Use `if` or `match` to hook to specific stages. + /// + /// The stages are loaded in order: `Core` → `Servers` → `Scene` → `Editor` (if in editor) → `MainLoop` (4.5+). \ + /// The `MainLoop` stage represents the fully initialized state of Godot, after all initialization levels and classes have been loaded. /// - /// This will only be invoked for levels >= [`Self::min_level()`], in ascending order. Use `if` or `match` to hook to specific levels. + /// See also [`on_main_loop_frame()`][Self::on_main_loop_frame] for per-frame processing. /// + /// # Panics /// If the overridden method panics, an error will be printed, but GDExtension loading is **not** aborted. #[allow(unused_variables)] - fn on_level_init(level: InitLevel) { - // Nothing by default. + #[expect(deprecated)] // Fall back to older API. + fn on_stage_init(stage: InitStage) { + stage + .try_to_level() + .inspect(|&level| Self::on_level_init(level)); + + #[cfg(since_api = "4.5")] // Compat layer. + if stage == InitStage::MainLoop { + Self::on_main_loop_startup(); + } } - /// Custom logic when a certain init-level of Godot is unloaded. + /// Custom logic when a certain initialization stage is unloaded. + /// + /// This will be invoked for stages >= [`Self::min_level()`], in descending order. Use `if` or `match` to hook to specific stages. /// - /// This will only be invoked for levels >= [`Self::min_level()`], in descending order. Use `if` or `match` to hook to specific levels. + /// The stages are unloaded in reverse order: `MainLoop` (4.5+) → `Editor` (if in editor) → `Scene` → `Servers` → `Core`. \ + /// At the time `MainLoop` is deinitialized, all classes are still available. /// + /// # Panics /// If the overridden method panics, an error will be printed, but GDExtension unloading is **not** aborted. #[allow(unused_variables)] + #[expect(deprecated)] // Fall back to older API. + fn on_stage_deinit(stage: InitStage) { + #[cfg(since_api = "4.5")] // Compat layer. + if stage == InitStage::MainLoop { + Self::on_main_loop_shutdown(); + } + + stage + .try_to_level() + .inspect(|&level| Self::on_level_deinit(level)); + } + + /// Old callback before [`on_stage_init()`][Self::on_stage_deinit] was added. Does not support `MainLoop` stage. + #[deprecated = "Use `on_stage_init()` instead, which also includes the MainLoop stage."] + #[allow(unused_variables)] + fn on_level_init(level: InitLevel) { + // Nothing by default. + } + + /// Old callback before [`on_stage_deinit()`][Self::on_stage_deinit] was added. Does not support `MainLoop` stage. + #[deprecated = "Use `on_stage_deinit()` instead, which also includes the MainLoop stage."] + #[allow(unused_variables)] fn on_level_deinit(level: InitLevel) { // Nothing by default. } + #[cfg(since_api = "4.5")] + #[deprecated = "Use `on_stage_init(InitStage::MainLoop)` instead."] + #[doc(hidden)] // Added by mistake -- works but don't advertise. + fn on_main_loop_startup() { + // Nothing by default. + } + + #[cfg(since_api = "4.5")] + #[deprecated = "Use `on_stage_deinit(InitStage::MainLoop)` instead."] + #[doc(hidden)] // Added by mistake -- works but don't advertise. + fn on_main_loop_shutdown() { + // Nothing by default. + } + + /// Callback invoked for every process frame. + /// + /// This is called during the main loop, after Godot is fully initialized. It runs after all + /// [`process()`][crate::classes::INode::process] methods on Node, and before the Godot-internal `ScriptServer::frame()`. + /// This is intended to be the equivalent of [`IScriptLanguageExtension::frame()`][`crate::classes::IScriptLanguageExtension::frame()`] + /// for GDExtension language bindings that don't use the script API. + /// + /// # Example + /// To hook into startup/shutdown of the main loop, use [`on_stage_init()`][Self::on_stage_init] and + /// [`on_stage_deinit()`][Self::on_stage_deinit] and watch for [`InitStage::MainLoop`]. + /// + /// ```no_run + /// # use godot::init::*; + /// # struct MyExtension; + /// #[gdextension] + /// unsafe impl ExtensionLibrary for MyExtension { + /// fn on_stage_init(stage: InitStage) { + /// if stage == InitStage::MainLoop { + /// // Startup code after fully initialized. + /// } + /// } + /// + /// fn on_main_loop_frame() { + /// // Per-frame logic. + /// } + /// + /// fn on_stage_deinit(stage: InitStage) { + /// if stage == InitStage::MainLoop { + /// // Cleanup code before shutdown. + /// } + /// } + /// } + /// ``` + /// + /// # Panics + /// If the overridden method panics, an error will be printed, but execution continues. + #[cfg(since_api = "4.5")] + fn on_main_loop_frame() { + // Nothing by default. + } + /// Whether to override the Wasm binary filename used by your GDExtension which the library should expect at runtime. Return `None` /// to use the default where gdext expects either `{YourCrate}.wasm` (default binary name emitted by Rust) or /// `{YourCrate}.threads.wasm` (for builds producing separate single-threaded and multi-threaded binaries). @@ -330,7 +474,7 @@ pub unsafe trait ExtensionLibrary { /// #[cfg(feature = "nothreads")] /// return None; /// - /// // Tell gdext we add a custom suffix to the binary with thread support. + /// // Tell godot-rust we add a custom suffix to the binary with thread support. /// // Please note that this is not needed if "mycrate.threads.wasm" is used. /// // (You could return `None` as well in that particular case.) /// #[cfg(not(feature = "nothreads"))] @@ -374,15 +518,7 @@ pub enum EditorRunBehavior { // ---------------------------------------------------------------------------------------------------------------------------------------------- -/// Stage of the Godot initialization process. -/// -/// Godot's initialization and deinitialization processes are split into multiple stages, like a stack. At each level, -/// a different amount of engine functionality is available. Deinitialization happens in reverse order. -/// -/// See also: -/// - [`ExtensionLibrary::on_level_init()`] -/// - [`ExtensionLibrary::on_level_deinit()`] -pub type InitLevel = sys::InitLevel; +pub use sys::InitLevel; // ---------------------------------------------------------------------------------------------------------------------------------------------- @@ -398,6 +534,16 @@ unsafe fn ensure_godot_features_compatible() { out!("Check Godot precision setting..."); + #[cfg(feature = "debug-log")] // Display safeguards level in debug log. + let safeguards_level = if cfg!(safeguards_strict) { + "strict" + } else if cfg!(safeguards_balanced) { + "balanced" + } else { + "disengaged" + }; + out!("Safeguards: {safeguards_level}"); + let os_class = StringName::from("OS"); let single = GString::from("single"); let double = GString::from("double"); diff --git a/godot-core/src/meta/args/as_arg.rs b/godot-core/src/meta/args/as_arg.rs index 3de88a4aa..035a6bbdd 100644 --- a/godot-core/src/meta/args/as_arg.rs +++ b/godot-core/src/meta/args/as_arg.rs @@ -9,7 +9,7 @@ use crate::builtin::{GString, NodePath, StringName, Variant}; use crate::meta::sealed::Sealed; use crate::meta::traits::{GodotFfiVariant, GodotNullableFfi}; use crate::meta::{CowArg, FfiArg, GodotType, ObjectArg, ToGodot}; -use crate::obj::{bounds, Bounds, DynGd, Gd, GodotClass, Inherits}; +use crate::obj::{DynGd, Gd, GodotClass, Inherits}; /// Implicit conversions for arguments passed to Godot APIs. /// @@ -263,7 +263,7 @@ where impl AsArg>> for &Gd where T: Inherits, - Base: GodotClass + Bounds, + Base: GodotClass, { fn into_arg<'arg>(self) -> CowArg<'arg, Option>> where @@ -286,7 +286,7 @@ where impl AsArg>> for Option<&Gd> where T: Inherits, - Base: GodotClass + Bounds, + Base: GodotClass, { fn into_arg<'arg>(self) -> CowArg<'arg, Option>> where @@ -313,7 +313,7 @@ impl AsArg>> for &DynGd where T: Inherits, D: ?Sized, - Base: GodotClass + Bounds, + Base: GodotClass, { fn into_arg<'arg>(self) -> CowArg<'arg, Option>> where @@ -337,7 +337,7 @@ impl AsArg>> for &DynGd where T: Inherits, D: ?Sized, - Base: GodotClass + Bounds, + Base: GodotClass, { fn into_arg<'arg>(self) -> CowArg<'arg, Option>> where @@ -361,7 +361,7 @@ impl AsArg>> for Option<&DynGd> where T: Inherits, D: ?Sized, - Base: GodotClass + Bounds, + Base: GodotClass, { fn into_arg<'arg>(self) -> CowArg<'arg, Option>> where @@ -764,3 +764,35 @@ where #[doc(hidden)] // Easier for internal use. pub type ToArg<'r, Via, Pass> = ::Output<'r, Via>; + +/// This type exists only as a place to add doctests for `AsArg`, which do not need to be in the public documentation. +/// +/// `AsArg>` can be used with signals correctly: +/// +/// ```no_run +/// # use godot::prelude::*; +/// #[derive(GodotClass)] +/// #[class(init, base = Node)] +/// struct MyClass { +/// base: Base +/// } +/// +/// #[godot_api] +/// impl MyClass { +/// #[signal] +/// fn signal_optional_user_obj(arg1: Option>); +/// +/// fn foo(&mut self) { +/// let arg = self.to_gd(); +/// // Directly: +/// self.signals().signal_optional_user_obj().emit(&arg); +/// // Via Some: +/// self.signals().signal_optional_user_obj().emit(Some(&arg)); +/// // With None (Note: Gd::null_arg() is restricted to engine classes): +/// self.signals().signal_optional_user_obj().emit(None::>.as_ref()); +/// } +/// } +/// ``` +/// +#[allow(dead_code)] +struct PhantomAsArgDoctests; diff --git a/godot-core/src/meta/class_id.rs b/godot-core/src/meta/class_id.rs index d8df04fab..01108837a 100644 --- a/godot-core/src/meta/class_id.rs +++ b/godot-core/src/meta/class_id.rs @@ -309,8 +309,20 @@ impl ClassIdCache { } fn clear(&mut self) { - self.entries.clear(); + // MACOS-PARTIAL-RELOAD: Previous implementation for when upstream fixes `.gdextension` reload. + // self.entries.clear(); + // self.type_to_index.clear(); + // self.string_to_index.clear(); + + // MACOS-PARTIAL-RELOAD: Preserve existing `ClassId` entries when only the `.gdextension` reloads so indices stay valid. + // There are two types of hot reload: `dylib` reload (`dylib` `mtime` newer) unloads and reloads the library, whereas + // `.gdextension` reload (`.gdextension` `mtime` newer) re-initializes the existing `dylib` without unloading it. To handle + // `.gdextension` reload, keep the backing entries (and thus the `string_to_index` map) but drop cached Godot `StringNames` + // and the `TypeId` lookup so they can be rebuilt. + for entry in &mut self.entries { + entry.godot_str = OnceCell::new(); + } + self.type_to_index.clear(); - self.string_to_index.clear(); } } diff --git a/godot-core/src/meta/error/call_error.rs b/godot-core/src/meta/error/call_error.rs index 75adc654a..c8d8c36cc 100644 --- a/godot-core/src/meta/error/call_error.rs +++ b/godot-core/src/meta/error/call_error.rs @@ -16,6 +16,9 @@ use crate::meta::{CallContext, ToGodot}; use crate::private::PanicPayload; use crate::sys; +/// Result type for function calls that can fail. +pub(crate) type CallResult = Result; + /// Error capable of representing failed function calls. /// /// This type is returned from _varcall_ functions in the Godot API that begin with `try_` prefixes, diff --git a/godot-core/src/meta/error/convert_error.rs b/godot-core/src/meta/error/convert_error.rs index 110cb3aa8..ea5996d5f 100644 --- a/godot-core/src/meta/error/convert_error.rs +++ b/godot-core/src/meta/error/convert_error.rs @@ -171,7 +171,10 @@ impl fmt::Display for ErrorKind { Self::FromGodot(from_godot) => write!(f, "{from_godot}"), Self::FromVariant(from_variant) => write!(f, "{from_variant}"), Self::FromFfi(from_ffi) => write!(f, "{from_ffi}"), - Self::Custom(cause) => write!(f, "{cause:?}"), + Self::Custom(cause) => match cause { + Some(c) => write!(f, "{c}"), + None => write!(f, "custom error"), + }, } } } @@ -186,7 +189,7 @@ pub(crate) enum FromGodotError { }, /// Special case of `BadArrayType` where a custom int type such as `i8` cannot hold a dynamic `i64` value. - #[cfg(debug_assertions)] + #[cfg(safeguards_strict)] BadArrayTypeInt { expected_int_type: &'static str, value: i64, @@ -231,7 +234,7 @@ impl fmt::Display for FromGodotError { write!(f, "expected array of type {exp_class}, got {act_class}") } - #[cfg(debug_assertions)] + #[cfg(safeguards_strict)] Self::BadArrayTypeInt { expected_int_type, value, diff --git a/godot-core/src/meta/param_tuple.rs b/godot-core/src/meta/param_tuple.rs index de1ab88b7..d2901aec7 100644 --- a/godot-core/src/meta/param_tuple.rs +++ b/godot-core/src/meta/param_tuple.rs @@ -7,8 +7,9 @@ use godot_ffi as sys; -use super::{CallContext, CallResult, PropertyInfo}; use crate::builtin::Variant; +use crate::meta::error::CallResult; +use crate::meta::{CallContext, PropertyInfo}; mod impls; @@ -64,7 +65,7 @@ pub trait InParamTuple: ParamTuple { args_ptr: *const sys::GDExtensionConstTypePtr, call_type: sys::PtrcallType, call_ctx: &CallContext, - ) -> Self; + ) -> CallResult; /// Converts `array` to `Self` by calling [`from_variant`](crate::meta::FromGodot::from_variant) on each argument. fn from_variant_array(array: &[&Variant]) -> Self; diff --git a/godot-core/src/meta/param_tuple/impls.rs b/godot-core/src/meta/param_tuple/impls.rs index 4c55ab35d..7d8e6605d 100644 --- a/godot-core/src/meta/param_tuple/impls.rs +++ b/godot-core/src/meta/param_tuple/impls.rs @@ -14,10 +14,10 @@ use godot_ffi as sys; use sys::GodotFfi; use crate::builtin::Variant; -use crate::meta::error::{CallError, ConvertError}; +use crate::meta::error::{CallError, CallResult}; use crate::meta::{ - signature, ArgPassing, CallContext, FromGodot, GodotConvert, GodotType, InParamTuple, - OutParamTuple, ParamTuple, ToGodot, + ArgPassing, CallContext, FromGodot, GodotConvert, GodotType, InParamTuple, OutParamTuple, + ParamTuple, ToGodot, }; macro_rules! count_idents { @@ -57,7 +57,7 @@ macro_rules! unsafe_impl_param_tuple { unsafe fn from_varcall_args( args_ptr: *const sys::GDExtensionConstVariantPtr, call_ctx: &crate::meta::CallContext, - ) -> signature::CallResult { + ) -> CallResult { let args = ( $( // SAFETY: `args_ptr` is an array with length `Self::LEN` and each element is a valid pointer, since they @@ -80,14 +80,16 @@ macro_rules! unsafe_impl_param_tuple { args_ptr: *const sys::GDExtensionConstTypePtr, call_type: sys::PtrcallType, call_ctx: &crate::meta::CallContext, - ) -> Self { - ( + ) -> CallResult { + let tuple = ( $( // SAFETY: `args_ptr` has length `Self::LEN` and `$n` is less than `Self::LEN`, and `args_ptr` must be an array whose // `$n`-th element is of type `$P`. - unsafe { ptrcall_arg::<$P, $n>(args_ptr, call_ctx, call_type) }, + unsafe { ptrcall_arg::<$P, $n>(args_ptr, call_ctx, call_type)? }, )* - ) + ); + + Ok(tuple) // If none of the `?` above were hit. } fn from_variant_array(array: &[&Variant]) -> Self { @@ -196,7 +198,7 @@ pub(super) unsafe fn ptrcall_arg( args_ptr: *const sys::GDExtensionConstTypePtr, call_ctx: &CallContext, call_type: sys::PtrcallType, -) -> P { +) -> CallResult

{ // SAFETY: It is safe to dereference `args_ptr` at `N`. let offset_ptr = unsafe { *args_ptr.offset(N) }; @@ -207,7 +209,7 @@ pub(super) unsafe fn ptrcall_arg( ::try_from_ffi(ffi) .and_then(P::try_from_godot) - .unwrap_or_else(|err| param_error::

(call_ctx, N as i32, err)) + .map_err(|err| CallError::failed_param_conversion::

(call_ctx, N, err)) } /// Converts `arg` into a value of type `P`. @@ -219,7 +221,7 @@ pub(super) unsafe fn varcall_arg( arg: sys::GDExtensionConstVariantPtr, call_ctx: &CallContext, param_index: isize, -) -> Result { +) -> CallResult

{ // SAFETY: It is safe to dereference `args_ptr` at `N` as a `Variant`. let variant_ref = unsafe { Variant::borrow_var_sys(arg) }; @@ -228,11 +230,6 @@ pub(super) unsafe fn varcall_arg( .map_err(|err| CallError::failed_param_conversion::

(call_ctx, param_index, err)) } -fn param_error

(call_ctx: &CallContext, index: i32, err: ConvertError) -> ! { - let param_ty = std::any::type_name::

(); - panic!("in function `{call_ctx}` at parameter [{index}] of type {param_ty}: {err}"); -} - fn assert_array_length(array: &[&Variant]) { assert_eq!( array.len(), diff --git a/godot-core/src/meta/signature.rs b/godot-core/src/meta/signature.rs index 0af81e3eb..00844abf9 100644 --- a/godot-core/src/meta/signature.rs +++ b/godot-core/src/meta/signature.rs @@ -9,17 +9,16 @@ use std::borrow::Cow; use std::fmt; use std::marker::PhantomData; -use godot_ffi::{self as sys, GodotFfi}; +use godot_ffi as sys; +use sys::GodotFfi; use crate::builtin::Variant; -use crate::meta::error::{CallError, ConvertError}; +use crate::meta::error::{CallError, CallResult, ConvertError}; use crate::meta::{ FromGodot, GodotConvert, GodotType, InParamTuple, MethodParamOrReturnInfo, OutParamTuple, ParamTuple, ToGodot, }; -use crate::obj::{GodotClass, InstanceId}; - -pub(super) type CallResult = Result; +use crate::obj::{GodotClass, ValidatedObject}; /// A full signature for a function. /// @@ -62,7 +61,6 @@ where /// Receive a varcall from Godot, and return the value in `ret` as a variant pointer. /// /// # Safety - /// /// A call to this function must be caused by Godot making a varcall with parameters `Params` and return type `Ret`. #[inline] pub unsafe fn in_varcall( @@ -102,19 +100,21 @@ where ret: sys::GDExtensionTypePtr, func: fn(sys::GDExtensionClassInstancePtr, Params) -> Ret, call_type: sys::PtrcallType, - ) { + ) -> CallResult<()> { // $crate::out!("in_ptrcall: {call_ctx}"); #[cfg(feature = "trace")] trace::push(true, true, call_ctx); // SAFETY: TODO. - let args = unsafe { Params::from_ptrcall_args(args_ptr, call_type, call_ctx) }; + let args = unsafe { Params::from_ptrcall_args(args_ptr, call_type, call_ctx)? }; // SAFETY: // `ret` is always a pointer to an initialized value of type $R // TODO: double-check the above - unsafe { ptrcall_return::(func(instance_ptr, args), ret, call_ctx, call_type) } + unsafe { ptrcall_return::(func(instance_ptr, args), ret, call_ctx, call_type) }; + + Ok(()) } } @@ -126,8 +126,6 @@ impl Signature { /// Make a varcall to the Godot engine for a class method. /// /// # Safety - /// - /// - `object_ptr` must be a live instance of a class with the type expected by `method_bind` /// - `method_bind` must expect explicit args `args`, varargs `varargs`, and return a value of type `Ret` #[inline] pub unsafe fn out_class_varcall( @@ -135,19 +133,13 @@ impl Signature { // Separate parameters to reduce tokens in generated class API. class_name: &'static str, method_name: &'static str, - object_ptr: sys::GDExtensionObjectPtr, - maybe_instance_id: Option, // if not static + validated_obj: Option, args: Params, varargs: &[Variant], ) -> CallResult { let call_ctx = CallContext::outbound(class_name, method_name); //$crate::out!("out_class_varcall: {call_ctx}"); - // Note: varcalls are not safe from failing, if they happen through an object pointer -> validity check necessary. - if let Some(instance_id) = maybe_instance_id { - crate::classes::ensure_object_alive(instance_id, object_ptr, &call_ctx); - } - let class_fn = sys::interface_fn!(object_method_bind_call); let variant = args.with_variants(|explicit_args| { @@ -160,7 +152,7 @@ impl Signature { let mut err = sys::default_call_error(); class_fn( method_bind.0, - object_ptr, + ValidatedObject::object_ptr(validated_obj.as_ref()), variant_ptrs.as_ptr(), variant_ptrs.len() as i64, return_ptr, @@ -288,8 +280,6 @@ impl Signature { /// Make a ptrcall to the Godot engine for a class method. /// /// # Safety - /// - /// - `object_ptr` must be a live instance of a class with the type expected by `method_bind` /// - `method_bind` must expect explicit args `args`, and return a value of type `Ret` #[inline] pub unsafe fn out_class_ptrcall( @@ -297,24 +287,19 @@ impl Signature { // Separate parameters to reduce tokens in generated class API. class_name: &'static str, method_name: &'static str, - object_ptr: sys::GDExtensionObjectPtr, - maybe_instance_id: Option, // if not static + validated_obj: Option, args: Params, ) -> Ret { let call_ctx = CallContext::outbound(class_name, method_name); // $crate::out!("out_class_ptrcall: {call_ctx}"); - if let Some(instance_id) = maybe_instance_id { - crate::classes::ensure_object_alive(instance_id, object_ptr, &call_ctx); - } - let class_fn = sys::interface_fn!(object_method_bind_ptrcall); unsafe { Self::raw_ptrcall(args, &call_ctx, |explicit_args, return_ptr| { class_fn( method_bind.0, - object_ptr, + ValidatedObject::object_ptr(validated_obj.as_ref()), explicit_args.as_ptr(), return_ptr, ); diff --git a/godot-core/src/obj/base.rs b/godot-core/src/obj/base.rs index 1e02e038c..05cd82e47 100644 --- a/godot-core/src/obj/base.rs +++ b/godot-core/src/obj/base.rs @@ -5,13 +5,14 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -#[cfg(debug_assertions)] +#[cfg(safeguards_strict)] use std::cell::Cell; use std::cell::RefCell; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; use std::mem::ManuallyDrop; +#[cfg(safeguards_strict)] use std::rc::Rc; use crate::builtin::Callable; @@ -27,7 +28,7 @@ thread_local! { } /// Represents the initialization state of a `Base` object. -#[cfg(debug_assertions)] +#[cfg(safeguards_strict)] #[derive(Debug, Copy, Clone, PartialEq, Eq)] enum InitState { /// Object is being constructed (inside `I*::init()` or `Gd::from_init_fn()`). @@ -38,14 +39,14 @@ enum InitState { Script, } -#[cfg(debug_assertions)] +#[cfg(safeguards_strict)] macro_rules! base_from_obj { ($obj:expr, $state:expr) => { Base::from_obj($obj, $state) }; } -#[cfg(not(debug_assertions))] +#[cfg(not(safeguards_strict))] macro_rules! base_from_obj { ($obj:expr, $state:expr) => { Base::from_obj($obj) @@ -82,7 +83,7 @@ pub struct Base { /// Tracks the initialization state of this `Base` in Debug mode. /// /// Rc allows to "copy-construct" the base from an existing one, while still affecting the user-instance through the original `Base`. - #[cfg(debug_assertions)] + #[cfg(safeguards_strict)] init_state: Rc>, } @@ -95,13 +96,17 @@ impl Base { /// `base` must be alive at the time of invocation, i.e. user `init()` (which could technically destroy it) must not have run yet. /// If `base` is destroyed while the returned `Base` is in use, that constitutes a logic error, not a safety issue. pub(crate) unsafe fn from_base(base: &Base) -> Base { - debug_assert!(base.obj.is_instance_valid()); + #[cfg(safeguards_balanced)] + assert!( + base.obj.is_instance_valid(), + "Cannot construct Base; was object freed during initialization?" + ); let obj = Gd::from_obj_sys_weak(base.obj.obj_sys()); Self { obj: ManuallyDrop::new(obj), - #[cfg(debug_assertions)] + #[cfg(safeguards_strict)] init_state: Rc::clone(&base.init_state), } } @@ -114,7 +119,8 @@ impl Base { /// `gd` must be alive at the time of invocation. If it is destroyed while the returned `Base` is in use, that constitutes a logic /// error, not a safety issue. pub(crate) unsafe fn from_script_gd(gd: &Gd) -> Self { - debug_assert!(gd.is_instance_valid()); + #[cfg(safeguards_strict)] + assert!(gd.is_instance_valid()); let obj = Gd::from_obj_sys_weak(gd.obj_sys()); base_from_obj!(obj, InitState::Script) @@ -141,7 +147,7 @@ impl Base { base_from_obj!(obj, InitState::ObjectConstructing) } - #[cfg(debug_assertions)] + #[cfg(safeguards_strict)] fn from_obj(obj: Gd, init_state: InitState) -> Self { Self { obj: ManuallyDrop::new(obj), @@ -149,7 +155,7 @@ impl Base { } } - #[cfg(not(debug_assertions))] + #[cfg(not(safeguards_strict))] fn from_obj(obj: Gd) -> Self { Self { obj: ManuallyDrop::new(obj), @@ -176,7 +182,7 @@ impl Base { /// # Panics (Debug) /// If called outside an initialization function, or for ref-counted objects on a non-main thread. pub fn to_init_gd(&self) -> Gd { - #[cfg(debug_assertions)] // debug_assert! still checks existence of symbols. + #[cfg(safeguards_strict)] // debug_assert! still checks existence of symbols. assert!( self.is_initializing(), "Base::to_init_gd() can only be called during object initialization, inside I*::init() or Gd::from_init_fn()" @@ -248,7 +254,7 @@ impl Base { /// Finalizes the initialization of this `Base` and returns whether pub(crate) fn mark_initialized(&mut self) { - #[cfg(debug_assertions)] + #[cfg(safeguards_strict)] { assert_eq!( self.init_state.get(), @@ -265,7 +271,7 @@ impl Base { /// Returns a [`Gd`] referencing the base object, assuming the derived object is fully constructed. #[doc(hidden)] pub fn __fully_constructed_gd(&self) -> Gd { - #[cfg(debug_assertions)] // debug_assert! still checks existence of symbols. + #[cfg(safeguards_strict)] // debug_assert! still checks existence of symbols. assert!( !self.is_initializing(), "WithBaseField::to_gd(), base(), base_mut() can only be called on fully-constructed objects, after I*::init() or Gd::from_init_fn()" @@ -296,7 +302,7 @@ impl Base { /// Returns a passive reference to the base object, for use in script contexts only. pub(crate) fn to_script_passive(&self) -> PassiveGd { - #[cfg(debug_assertions)] + #[cfg(safeguards_strict)] assert_eq!( self.init_state.get(), InitState::Script, @@ -308,7 +314,7 @@ impl Base { } /// Returns `true` if this `Base` is currently in the initializing state. - #[cfg(debug_assertions)] + #[cfg(safeguards_strict)] fn is_initializing(&self) -> bool { self.init_state.get() == InitState::ObjectConstructing } @@ -316,7 +322,7 @@ impl Base { /// Returns a [`Gd`] referencing the base object, assuming the derived object is fully constructed. #[doc(hidden)] pub fn __constructed_gd(&self) -> Gd { - #[cfg(debug_assertions)] // debug_assert! still checks existence of symbols. + #[cfg(safeguards_strict)] // debug_assert! still checks existence of symbols. assert!( !self.is_initializing(), "WithBaseField::to_gd(), base(), base_mut() can only be called on fully-constructed objects, after I*::init() or Gd::from_init_fn()" @@ -333,7 +339,7 @@ impl Base { /// # Safety /// Caller must ensure that the underlying object remains valid for the entire lifetime of the returned `PassiveGd`. pub(crate) unsafe fn constructed_passive(&self) -> PassiveGd { - #[cfg(debug_assertions)] // debug_assert! still checks existence of symbols. + #[cfg(safeguards_strict)] // debug_assert! still checks existence of symbols. assert!( !self.is_initializing(), "WithBaseField::base(), base_mut() can only be called on fully-constructed objects, after I*::init() or Gd::from_init_fn()" diff --git a/godot-core/src/obj/casts.rs b/godot-core/src/obj/casts.rs index b0504c68b..9571963e2 100644 --- a/godot-core/src/obj/casts.rs +++ b/godot-core/src/obj/casts.rs @@ -48,7 +48,7 @@ impl CastSuccess { } /// Access shared reference to destination, without consuming object. - #[cfg(debug_assertions)] + #[cfg(safeguards_strict)] pub fn as_dest_ref(&self) -> &RawGd { self.check_validity(); &self.dest @@ -56,6 +56,7 @@ impl CastSuccess { /// Access exclusive reference to destination, without consuming object. pub fn as_dest_mut(&mut self) -> &mut RawGd { + #[cfg(safeguards_strict)] self.check_validity(); &mut self.dest } @@ -70,13 +71,15 @@ impl CastSuccess { self.dest.instance_id_unchecked(), "traded_source must point to the same object as the destination" ); + #[cfg(safeguards_strict)] self.check_validity(); std::mem::forget(traded_source); ManuallyDrop::into_inner(self.dest) } + #[cfg(safeguards_strict)] fn check_validity(&self) { - debug_assert!(self.dest.is_null() || self.dest.is_instance_valid()); + assert!(self.dest.is_null() || self.dest.is_instance_valid()); } } diff --git a/godot-core/src/obj/gd.rs b/godot-core/src/obj/gd.rs index 474511f86..18af4b086 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -15,8 +15,8 @@ use sys::{static_assert_eq_size_align, SysPtr as _}; use crate::builtin::{Callable, GString, NodePath, StringName, Variant}; use crate::meta::error::{ConvertError, FromFfiError}; use crate::meta::{ - ArrayElement, AsArg, CallContext, ClassId, FromGodot, GodotConvert, GodotType, - PropertyHintInfo, RefArg, ToGodot, + ArrayElement, AsArg, ClassId, FromGodot, GodotConvert, GodotType, PropertyHintInfo, RefArg, + ToGodot, }; use crate::obj::{ bounds, cap, Bounds, DynGd, GdDerefTarget, GdMut, GdRef, GodotClass, Inherits, InstanceId, @@ -783,7 +783,7 @@ where } // If ref_counted returned None, that means the instance was destroyed - if ref_counted != Some(false) || !self.is_instance_valid() { + if ref_counted != Some(false) || (cfg!(safeguards_balanced) && !self.is_instance_valid()) { return error_or_panic("called free() on already destroyed object".to_string()); } @@ -791,8 +791,10 @@ where // static type information to be correct. This is a no-op in Release mode. // Skip check during panic unwind; would need to rewrite whole thing to use Result instead. Having BOTH panic-in-panic and bad type is // a very unlikely corner case. + #[cfg(safeguards_strict)] if !is_panic_unwind { - self.raw.check_dynamic_type(&CallContext::gd::("free")); + self.raw + .check_dynamic_type(&crate::meta::CallContext::gd::("free")); } // SAFETY: object must be alive, which was just checked above. No multithreading here. diff --git a/godot-core/src/obj/raw_gd.rs b/godot-core/src/obj/raw_gd.rs index ef7d6a913..e260064f3 100644 --- a/godot-core/src/obj/raw_gd.rs +++ b/godot-core/src/obj/raw_gd.rs @@ -360,7 +360,7 @@ impl RawGd { debug_assert!(!self.is_null(), "cannot upcast null object refs"); // In Debug builds, go the long path via Godot FFI to verify the results are the same. - #[cfg(debug_assertions)] + #[cfg(safeguards_strict)] { // SAFETY: we forget the object below and do not leave the function before. let ffi_dest = self.ffi_cast::().expect("failed FFI upcast"); @@ -381,26 +381,59 @@ impl RawGd { } } - /// Verify that the object is non-null and alive. In Debug mode, additionally verify that it is of type `T` or derived. + /// Validates object for use in `RawGd`/`Gd` methods (not FFI boundary calls). + /// + /// This is used for Rust-side object operations like `bind()`, `clone()`, `ffi_cast()`, etc. + /// For FFI boundary calls (generated engine methods), validation happens in signature.rs instead. + /// + /// # Panics + /// If validation fails. + #[cfg(safeguards_balanced)] pub(crate) fn check_rtti(&self, method_name: &'static str) { let call_ctx = CallContext::gd::(method_name); - let instance_id = self.check_dynamic_type(&call_ctx); + // Type check (strict only). + #[cfg(safeguards_strict)] + self.check_dynamic_type(&call_ctx); + + // SAFETY: check_rtti() is only called for non-null pointers. + let instance_id = unsafe { self.instance_id_unchecked().unwrap_unchecked() }; + + // Liveness check (balanced + strict). classes::ensure_object_alive(instance_id, self.obj_sys(), &call_ctx); } - /// Checks only type, not alive-ness. Used in Gd in case of `free()`. - pub(crate) fn check_dynamic_type(&self, call_ctx: &CallContext<'static>) -> InstanceId { - debug_assert!( + #[cfg(not(safeguards_balanced))] + pub(crate) fn check_rtti(&self, _method_name: &'static str) { + // Disengaged level: no-op. + } + + /// Checks only type, not liveness. + /// + /// Used in specific scenarios like `Gd::free()` where we need type validation + /// but the object may already be dead. This is an internal helper for `check_rtti()`. + #[cfg(safeguards_strict)] + #[inline] + pub(crate) fn check_dynamic_type(&self, call_ctx: &CallContext<'static>) { + assert!( !self.is_null(), - "{call_ctx}: cannot call method on null object", + "internal bug: {call_ctx}: cannot call method on null object", ); let rtti = self.cached_rtti.as_ref(); - // SAFETY: code surrounding RawGd ensures that `self` is non-null; above is just a sanity check against internal bugs. + // SAFETY: RawGd non-null (checked above). let rtti = unsafe { rtti.unwrap_unchecked() }; - rtti.check_type::() + rtti.check_type::(); + } + + /// Creates a validated object for FFI boundary crossing. + /// + /// This is a convenience wrapper around [`ValidatedObject::validate()`]. + #[doc(hidden)] + #[inline] + pub fn validated_object(&self) -> ValidatedObject { + ValidatedObject::validate(self) } // Not pub(super) because used by godot::meta::args::ObjectArg. @@ -422,7 +455,7 @@ where { /// Hands out a guard for a shared borrow, through which the user instance can be read. /// - /// See [`crate::obj::Gd::bind()`] for a more in depth explanation. + /// See [`Gd::bind()`] for a more in depth explanation. // Note: possible names: write/read, hold/hold_mut, r/w, r/rw, ... pub(crate) fn bind(&self) -> GdRef<'_, T> { self.check_rtti("bind"); @@ -431,7 +464,7 @@ where /// Hands out a guard for an exclusive borrow, through which the user instance can be read and written. /// - /// See [`crate::obj::Gd::bind_mut()`] for a more in depth explanation. + /// See [`Gd::bind_mut()`] for a more in depth explanation. pub(crate) fn bind_mut(&mut self) -> GdMut<'_, T> { self.check_rtti("bind_mut"); GdMut::from_guard(self.storage().unwrap().get_mut()) @@ -509,7 +542,7 @@ where let ptr: sys::GDExtensionClassInstancePtr = binding.cast(); - #[cfg(debug_assertions)] + #[cfg(safeguards_strict)] crate::classes::ensure_binding_not_null::(ptr); self.cached_storage_ptr.set(ptr); @@ -752,6 +785,44 @@ impl fmt::Debug for RawGd { } } +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Type-state for already-validated objects before an engine API call + +/// Type-state object pointers that have been validated for engine API calls. +/// +/// Can be passed to [`Signature`](meta::signature::Signature) methods. Performs the following checks depending on safeguard level: +/// - `disengaged`: no validation. +/// - `balanced`: liveness check only. +/// - `strict`: liveness + type inheritance check. +#[doc(hidden)] +pub struct ValidatedObject { + object_ptr: sys::GDExtensionObjectPtr, +} + +impl ValidatedObject { + /// Validates a `RawGd` according to the type's invariants (depending on safeguard level). + /// + /// # Panics + /// If validation fails. + #[doc(hidden)] + #[inline] + pub fn validate(raw_gd: &RawGd) -> Self { + raw_gd.check_rtti("validated_object"); + + Self { + object_ptr: raw_gd.obj_sys(), + } + } + + /// Extracts the object pointer from an `Option`. + /// + /// Returns null pointer for `None` (static methods), or the validated pointer for `Some`. + #[inline] + pub fn object_ptr(opt: Option<&Self>) -> sys::GDExtensionObjectPtr { + opt.map(|v| v.object_ptr).unwrap_or(ptr::null_mut()) + } +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Reusable functions, also shared with Gd, Variant, ObjectArg. diff --git a/godot-core/src/obj/rtti.rs b/godot-core/src/obj/rtti.rs index 114cf8861..1a22b7663 100644 --- a/godot-core/src/obj/rtti.rs +++ b/godot-core/src/obj/rtti.rs @@ -21,7 +21,7 @@ pub struct ObjectRtti { instance_id: InstanceId, /// Only in Debug mode: dynamic class. - #[cfg(debug_assertions)] + #[cfg(safeguards_strict)] class_name: crate::meta::ClassId, // // TODO(bromeon): class_id is not always most-derived class; ObjectRtti is sometimes constructed from a base class, via RawGd::from_obj_sys_weak(). @@ -36,25 +36,29 @@ impl ObjectRtti { Self { instance_id, - #[cfg(debug_assertions)] + #[cfg(safeguards_strict)] class_name: T::class_id(), } } - /// Checks that the object is of type `T` or derived. Returns instance ID. + /// Validates that the object's stored type matches or inherits from `T`. /// - /// # Panics - /// In Debug mode, if the object is not of type `T` or derived. + /// Used internally by `RawGd::check_rtti()` for type validation in strict mode. + /// + /// Only checks the cached type from RTTI construction time. + /// This may not reflect runtime type changes (which shouldn't happen). + /// + /// # Panics (strict safeguards) + /// If the stored type does not inherit from `T`. + #[cfg(safeguards_strict)] #[inline] - pub fn check_type(&self) -> InstanceId { - #[cfg(debug_assertions)] + pub fn check_type(&self) { crate::classes::ensure_object_inherits(self.class_name, T::class_id(), self.instance_id); - - self.instance_id } #[inline] pub fn instance_id(&self) -> InstanceId { + // Do not add logic or validations here, this is passed in every FFI call. self.instance_id } } diff --git a/godot-core/src/private.rs b/godot-core/src/private.rs index 74aaed0ef..d92a2d7c3 100644 --- a/godot-core/src/private.rs +++ b/godot-core/src/private.rs @@ -13,7 +13,7 @@ use std::sync::atomic; use sys::Global; use crate::global::godot_error; -use crate::meta::error::CallError; +use crate::meta::error::{CallError, CallResult}; use crate::meta::CallContext; use crate::obj::Gd; use crate::{classes, sys}; @@ -22,7 +22,10 @@ use crate::{classes, sys}; // Public re-exports mod reexport_pub { + #[cfg(all(since_api = "4.3", feature = "register-docs"))] + pub use crate::docs::{DocsItem, DocsPlugin, InherentImplDocs, StructDocs}; pub use crate::gen::classes::class_macros; + pub use crate::gen::virtuals; // virtual fn names, hashes, signatures #[cfg(feature = "trace")] pub use crate::meta::trace; pub use crate::obj::rtti::ObjectRtti; @@ -52,6 +55,8 @@ static CALL_ERRORS: Global = Global::default(); static ERROR_PRINT_LEVEL: atomic::AtomicU8 = atomic::AtomicU8::new(2); sys::plugin_registry!(pub __GODOT_PLUGIN_REGISTRY: ClassPlugin); +#[cfg(all(since_api = "4.3", feature = "register-docs"))] +sys::plugin_registry!(pub __GODOT_DOCS_REGISTRY: DocsPlugin); // ---------------------------------------------------------------------------------------------------------------------------------------------- // Call error handling @@ -146,6 +151,11 @@ pub(crate) fn iterate_plugins(mut visitor: impl FnMut(&ClassPlugin)) { sys::plugin_foreach!(__GODOT_PLUGIN_REGISTRY; visitor); } +#[cfg(all(since_api = "4.3", feature = "register-docs"))] +pub(crate) fn iterate_docs_plugins(mut visitor: impl FnMut(&DocsPlugin)) { + sys::plugin_foreach!(__GODOT_DOCS_REGISTRY; visitor); +} + #[cfg(feature = "codegen-full")] // Remove if used in other scenarios. pub(crate) fn find_inherent_impl(class_name: crate::meta::ClassId) -> Option { // We do this manually instead of using `iterate_plugins()` because we want to break as soon as we find a match. @@ -407,7 +417,7 @@ impl PanicPayload { /// /// Returns `Err(message)` if a panic occurred, and `Ok(result)` with the result of `code` otherwise. /// -/// In contrast to [`handle_varcall_panic`] and [`handle_ptrcall_panic`], this function is not intended for use in `try_` functions, +/// In contrast to [`handle_fallible_varcall`] and [`handle_fallible_ptrcall`], this function is not intended for use in `try_` functions, /// where the error is propagated as a `CallError` in a global variable. pub fn handle_panic(error_context: E, code: F) -> Result where @@ -430,59 +440,57 @@ where result } -// TODO(bromeon): make call_ctx lazy-evaluated (like error_ctx) everywhere; -// or make it eager everywhere and ensure it's cheaply constructed in the call sites. -pub fn handle_varcall_panic( +/// Invokes a function with the _varcall_ calling convention, handling both expected errors and user panics. +pub fn handle_fallible_varcall( call_ctx: &CallContext, out_err: &mut sys::GDExtensionCallError, code: F, ) where - F: FnOnce() -> Result + std::panic::UnwindSafe, + F: FnOnce() -> CallResult + std::panic::UnwindSafe, { - let outcome: Result, PanicPayload> = - handle_panic(|| call_ctx.to_string(), code); - - let call_error = match outcome { - // All good. - Ok(Ok(_result)) => return, - - // Call error signalled by Godot's or gdext's validation. - Ok(Err(err)) => err, - - // Panic occurred (typically through user): forward message. - Err(panic_msg) => CallError::failed_by_user_panic(call_ctx, panic_msg), - }; - - let error_id = report_call_error(call_error, true); - - // Abuse 'argument' field to store our ID. - *out_err = sys::GDExtensionCallError { - error: sys::GODOT_RUST_CUSTOM_CALL_ERROR, - argument: error_id, - expected: 0, + if let Some(error_id) = handle_fallible_call(call_ctx, code, true) { + // Abuse 'argument' field to store our ID. + *out_err = sys::GDExtensionCallError { + error: sys::GODOT_RUST_CUSTOM_CALL_ERROR, + argument: error_id, + expected: 0, + }; }; //sys::interface_fn!(variant_new_nil)(sys::AsUninit::as_uninit(ret)); } -pub fn handle_ptrcall_panic(call_ctx: &CallContext, code: F) +/// Invokes a function with the _ptrcall_ calling convention, handling both expected errors and user panics. +pub fn handle_fallible_ptrcall(call_ctx: &CallContext, code: F) where - F: FnOnce() -> R + std::panic::UnwindSafe, + F: FnOnce() -> CallResult<()> + std::panic::UnwindSafe, { - let outcome: Result = handle_panic(|| call_ctx.to_string(), code); + handle_fallible_call(call_ctx, code, false); +} + +/// Common error handling for fallible calls, handling detectable errors and user panics. +/// +/// Returns `None` if the call succeeded, or `Some(error_id)` if it failed. +/// +/// `track_globally` indicates whether the error should be stored as an index in the global error database (for varcall calls), to convey +/// out-of-band, godot-rust specific error information to the caller. +fn handle_fallible_call(call_ctx: &CallContext, code: F, track_globally: bool) -> Option +where + F: FnOnce() -> CallResult + std::panic::UnwindSafe, +{ + let outcome: Result, PanicPayload> = handle_panic(|| call_ctx.to_string(), code); let call_error = match outcome { // All good. - Ok(_result) => return, + Ok(Ok(_result)) => return None, - // Panic occurred (typically through user): forward message. - Err(payload) => CallError::failed_by_user_panic(call_ctx, payload), - }; + // Error from Godot or godot-rust validation (e.g. parameter conversion). + Ok(Err(err)) => err, - let _id = report_call_error(call_error, false); -} + // User panic occurred: forward message. + Err(panic_msg) => CallError::failed_by_user_panic(call_ctx, panic_msg), + }; -fn report_call_error(call_error: CallError, track_globally: bool) -> i32 { // Print failed calls to Godot's console. // TODO Level 1 is not yet set, so this will always print if level != 0. Needs better logic to recognize try_* calls and avoid printing. // But a bit tricky with multiple threads and re-entrancy; maybe pass in info in error struct. @@ -491,11 +499,13 @@ fn report_call_error(call_error: CallError, track_globally: bool) -> i32 { } // Once there is a way to auto-remove added errors, this could be always true. - if track_globally { + let error_id = if track_globally { call_error_insert(call_error) } else { 0 - } + }; + + Some(error_id) } // Currently unused; implemented due to temporary need and may come in handy. diff --git a/godot-core/src/registry/class.rs b/godot-core/src/registry/class.rs index 4a13622d5..1715313ec 100644 --- a/godot-core/src/registry/class.rs +++ b/godot-core/src/registry/class.rs @@ -424,8 +424,6 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { is_editor_plugin, is_internal, is_instantiable, - #[cfg(all(since_api = "4.3", feature = "register-docs"))] - docs: _, reference_fn, unreference_fn, }) => { @@ -473,8 +471,6 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { PluginItem::InherentImpl(InherentImpl { register_methods_constants_fn, register_rpcs_fn: _, - #[cfg(all(since_api = "4.3", feature = "register-docs"))] - docs: _, }) => { c.register_methods_constants_fn = Some(register_methods_constants_fn); } @@ -492,8 +488,6 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { user_free_property_list_fn, user_property_can_revert_fn, user_property_get_revert_fn, - #[cfg(all(since_api = "4.3", feature = "register-docs"))] - virtual_method_docs: _, validate_property_fn, }) => { c.user_register_fn = user_register_fn; diff --git a/godot-core/src/registry/plugin.rs b/godot-core/src/registry/plugin.rs index 7d2ad995f..f03da5fae 100644 --- a/godot-core/src/registry/plugin.rs +++ b/godot-core/src/registry/plugin.rs @@ -8,8 +8,6 @@ use std::any::Any; use std::{any, fmt}; -#[cfg(all(since_api = "4.3", feature = "register-docs"))] -use crate::docs::*; use crate::init::InitLevel; use crate::meta::ClassId; use crate::obj::{bounds, cap, Bounds, DynGd, Gd, GodotClass, Inherits, UserClass}; @@ -197,16 +195,10 @@ pub struct Struct { /// Whether the class has a default constructor. pub(crate) is_instantiable: bool, - - /// Documentation extracted from the struct's RustDoc. - #[cfg(all(since_api = "4.3", feature = "register-docs"))] - pub(crate) docs: StructDocs, } impl Struct { - pub fn new( - #[cfg(all(since_api = "4.3", feature = "register-docs"))] docs: StructDocs, - ) -> Self { + pub fn new() -> Self { let refcounted = ::IS_REF_COUNTED; Self { @@ -222,8 +214,6 @@ impl Struct { is_editor_plugin: false, is_internal: false, is_instantiable: false, - #[cfg(all(since_api = "4.3", feature = "register-docs"))] - docs, // While Godot doesn't do anything with these callbacks for non-RefCounted classes, we can avoid instantiating them in Rust. reference_fn: refcounted.then_some(callbacks::reference::), unreference_fn: refcounted.then_some(callbacks::unreference::), @@ -294,15 +284,10 @@ pub struct InherentImpl { // This field is only used during codegen-full. #[cfg_attr(not(feature = "codegen-full"), expect(dead_code))] pub(crate) register_rpcs_fn: Option, - - #[cfg(all(since_api = "4.3", feature = "register-docs"))] - pub docs: InherentImplDocs, } impl InherentImpl { - pub fn new( - #[cfg(all(since_api = "4.3", feature = "register-docs"))] docs: InherentImplDocs, - ) -> Self { + pub fn new() -> Self { Self { register_methods_constants_fn: ErasedRegisterFn { raw: callbacks::register_user_methods_constants::, @@ -310,18 +295,12 @@ impl InherentImpl { register_rpcs_fn: Some(ErasedRegisterRpcsFn { raw: callbacks::register_user_rpcs::, }), - #[cfg(all(since_api = "4.3", feature = "register-docs"))] - docs, } } } #[derive(Default, Clone, Debug)] pub struct ITraitImpl { - #[cfg(all(since_api = "4.3", feature = "register-docs"))] - /// Virtual method documentation. - pub(crate) virtual_method_docs: &'static str, - /// Callback to user-defined `register_class` function. pub(crate) user_register_fn: Option, @@ -436,12 +415,8 @@ pub struct ITraitImpl { } impl ITraitImpl { - pub fn new( - #[cfg(all(since_api = "4.3", feature = "register-docs"))] virtual_method_docs: &'static str, - ) -> Self { + pub fn new() -> Self { Self { - #[cfg(all(since_api = "4.3", feature = "register-docs"))] - virtual_method_docs, get_virtual_fn: Some(callbacks::get_virtual::), ..Default::default() } diff --git a/godot-core/src/storage/mod.rs b/godot-core/src/storage/mod.rs index 3061b6b5d..552fd4df1 100644 --- a/godot-core/src/storage/mod.rs +++ b/godot-core/src/storage/mod.rs @@ -123,14 +123,14 @@ mod log_inactive { // ---------------------------------------------------------------------------------------------------------------------------------------------- // Tracking borrows in Debug mode -#[cfg(debug_assertions)] +#[cfg(safeguards_strict)] use borrow_info::DebugBorrowTracker; -#[cfg(not(debug_assertions))] +#[cfg(not(safeguards_strict))] use borrow_info_noop::DebugBorrowTracker; use crate::obj::{Base, GodotClass}; -#[cfg(debug_assertions)] +#[cfg(safeguards_strict)] mod borrow_info { use std::backtrace::Backtrace; use std::fmt; @@ -195,7 +195,7 @@ mod borrow_info { } } -#[cfg(not(debug_assertions))] +#[cfg(not(safeguards_strict))] mod borrow_info_noop { use std::fmt; diff --git a/godot-core/src/tools/autoload.rs b/godot-core/src/tools/autoload.rs new file mode 100644 index 000000000..c22e3d8ce --- /dev/null +++ b/godot-core/src/tools/autoload.rs @@ -0,0 +1,177 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * 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::cell::RefCell; +use std::collections::HashMap; + +use sys::is_main_thread; + +use crate::builtin::NodePath; +use crate::classes::{Engine, Node, SceneTree}; +use crate::meta::error::ConvertError; +use crate::obj::{Gd, Inherits, Singleton}; +use crate::sys; + +/// Retrieves an autoload by name. +/// +/// See [Godot docs] for an explanation of the autoload concept. Godot sometimes uses the term "autoload" interchangeably with "singleton"; +/// we strictly refer to the former to separate from [`Singleton`][crate::obj::Singleton] objects. +/// +/// If the autoload can be resolved, it will be cached and returned very quickly the second time. +/// +/// [Godot docs]: https://docs.godotengine.org/en/stable/tutorials/scripting/singletons_autoload.html +/// +/// # Panics +/// This is a convenience function that calls [`try_get_autoload_by_name()`]. Panics if that fails, e.g. not found or wrong type. +/// +/// # Example +/// ```no_run +/// use godot::prelude::*; +/// use godot::tools::get_autoload_by_name; +/// +/// #[derive(GodotClass)] +/// #[class(init, base=Node)] +/// struct GlobalStats { +/// base: Base, +/// } +/// +/// // Assuming "Statistics" is registered as an autoload in `project.godot`, +/// // this returns the one instance of type Gd. +/// let stats = get_autoload_by_name::("Statistics"); +/// ``` +pub fn get_autoload_by_name(autoload_name: &str) -> Gd +where + T: Inherits, +{ + try_get_autoload_by_name::(autoload_name) + .unwrap_or_else(|err| panic!("Failed to get autoload `{autoload_name}`: {err}")) +} + +/// Retrieves an autoload by name (fallible). +/// +/// Autoloads are accessed via the `/root/{name}` path in the scene tree. The name is the one you used to register the autoload in +/// `project.godot`. By convention, it often corresponds to the class name, but does not have to. +/// +/// If the autoload can be resolved, it will be cached and returned very quickly the second time. +/// +/// See also [`get_autoload_by_name()`] for simpler function expecting the class name and non-fallible invocation. +/// +/// This function returns `Err` if: +/// - No autoload is registered under `name`. +/// - The autoload cannot be cast to type `T`. +/// - There is an error fetching the scene tree. +/// +/// # Example +/// ```no_run +/// use godot::prelude::*; +/// use godot::tools::try_get_autoload_by_name; +/// +/// #[derive(GodotClass)] +/// #[class(init, base=Node)] +/// struct GlobalStats { +/// base: Base, +/// } +/// +/// let result = try_get_autoload_by_name::("Statistics"); +/// match result { +/// Ok(autoload) => { /* Use the Gd. */ } +/// Err(err) => eprintln!("Failed to get autoload: {err}"), +/// } +/// ``` +pub fn try_get_autoload_by_name(autoload_name: &str) -> Result, ConvertError> +where + T: Inherits, +{ + ensure_main_thread()?; + + // Check cache first. + let cached = AUTOLOAD_CACHE.with(|cache| cache.borrow().get(autoload_name).cloned()); + + if let Some(cached_node) = cached { + return cast_autoload(cached_node, autoload_name); + } + + // Cache miss - fetch from scene tree. + let main_loop = Engine::singleton() + .get_main_loop() + .ok_or_else(|| ConvertError::new("main loop not available"))?; + + let scene_tree = main_loop + .try_cast::() + .map_err(|_| ConvertError::new("main loop is not a SceneTree"))?; + + let autoload_path = NodePath::from(&format!("/root/{autoload_name}")); + + let root = scene_tree + .get_root() + .ok_or_else(|| ConvertError::new("scene tree root not available"))?; + + let autoload_node = root + .try_get_node_as::(&autoload_path) + .ok_or_else(|| ConvertError::new(format!("autoload `{autoload_name}` not found")))?; + + // Store in cache as Gd. + AUTOLOAD_CACHE.with(|cache| { + cache + .borrow_mut() + .insert(autoload_name.to_string(), autoload_node.clone()); + }); + + // Cast to requested type. + cast_autoload(autoload_node, autoload_name) +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Cache implementation + +thread_local! { + /// Cache for autoloads. Maps autoload name to `Gd`. + /// + /// Uses `thread_local!` because `Gd` is not `Send`/`Sync`. Since all Godot objects must be accessed + /// from the main thread, this is safe. We enforce main-thread access via `ensure_main_thread()`. + static AUTOLOAD_CACHE: RefCell>> = RefCell::new(HashMap::new()); +} + +/// Verifies that the current thread is the main thread. +/// +/// Returns an error if called from a thread other than the main thread. This is necessary because `Gd` is not thread-safe. +fn ensure_main_thread() -> Result<(), ConvertError> { + if is_main_thread() { + Ok(()) + } else { + Err(ConvertError::new( + "Autoloads must be fetched from main thread, as Gd is not thread-safe", + )) + } +} + +/// Casts an autoload node to the requested type, with descriptive error message on failure. +fn cast_autoload(node: Gd, autoload_name: &str) -> Result, ConvertError> +where + T: Inherits, +{ + node.try_cast::().map_err(|node| { + let expected = T::class_id(); + let actual = node.get_class(); + + ConvertError::new(format!( + "autoload `{autoload_name}` has wrong type (expected {expected}, got {actual})", + )) + }) +} + +/// Clears the autoload cache (called during shutdown). +/// +/// # Panics +/// Panics if called from a thread other than the main thread. +pub(crate) fn clear_autoload_cache() { + ensure_main_thread().expect("clear_autoload_cache() must be called from the main thread"); + + AUTOLOAD_CACHE.with(|cache| { + cache.borrow_mut().clear(); + }); +} diff --git a/godot-core/src/tools/mod.rs b/godot-core/src/tools/mod.rs index 0b27ec75a..04aafa1cb 100644 --- a/godot-core/src/tools/mod.rs +++ b/godot-core/src/tools/mod.rs @@ -10,10 +10,18 @@ //! Contains functionality that extends existing Godot classes and functions, to make them more versatile //! or better integrated with Rust. +mod autoload; mod gfile; mod save_load; mod translate; +pub use autoload::*; pub use gfile::*; pub use save_load::*; pub use translate::*; + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +pub(crate) fn cleanup() { + clear_autoload_cache(); +} diff --git a/godot-ffi/Cargo.toml b/godot-ffi/Cargo.toml index d5d21f48a..d89f012ee 100644 --- a/godot-ffi/Cargo.toml +++ b/godot-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot-ffi" -version = "0.4.0" +version = "0.4.2" edition = "2021" rust-version = "1.87" license = "MPL-2.0" @@ -30,6 +30,10 @@ api-4-4 = ["godot-bindings/api-4-4"] api-4-5 = ["godot-bindings/api-4-5"] # ]] +# Safeguard levels (see godot/lib.rs for detailed documentation). +safeguards-dev-balanced = ["godot-bindings/safeguards-dev-balanced"] +safeguards-release-disengaged = ["godot-bindings/safeguards-release-disengaged"] + [dependencies] [target.'cfg(target_os = "linux")'.dependencies] @@ -37,11 +41,11 @@ libc = { workspace = true } [target.'cfg(target_family = "wasm")'.dependencies] # Only needed for WASM identifier generation. -godot-macros = { path = "../godot-macros", version = "=0.4.0", features = ["experimental-wasm"] } +godot-macros = { path = "../godot-macros", version = "=0.4.2", features = ["experimental-wasm"] } [build-dependencies] -godot-bindings = { path = "../godot-bindings", version = "=0.4.0" } -godot-codegen = { path = "../godot-codegen", version = "=0.4.0" } +godot-bindings = { path = "../godot-bindings", version = "=0.4.2" } +godot-codegen = { path = "../godot-codegen", version = "=0.4.2" } # https://docs.rs/about/metadata [package.metadata.docs.rs] diff --git a/godot-ffi/build.rs b/godot-ffi/build.rs index aa08ea282..7dfed4d3c 100644 --- a/godot-ffi/build.rs +++ b/godot-ffi/build.rs @@ -29,4 +29,5 @@ fn main() { godot_bindings::emit_godot_version_cfg(); godot_bindings::emit_wasm_nothreads_cfg(); + godot_bindings::emit_safeguard_levels(); } diff --git a/godot-ffi/src/init_level.rs b/godot-ffi/src/init_level.rs new file mode 100644 index 000000000..47d515f93 --- /dev/null +++ b/godot-ffi/src/init_level.rs @@ -0,0 +1,120 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * 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/. + */ + +/// Stage of the Godot initialization process. +/// +/// Godot's initialization and deinitialization processes are split into multiple stages, like a stack. At each level, +/// a different amount of engine functionality is available. Deinitialization happens in reverse order. +/// +/// See also: +// Explicit HTML links because this is re-exported in godot::init, and we can't document a `use` statement. +/// - [`InitStage`](enum.InitStage.html): all levels + main loop. +/// - [`ExtensionLibrary::on_stage_init()`](trait.ExtensionLibrary.html#method.on_stage_init) +/// - [`ExtensionLibrary::on_stage_deinit()`](trait.ExtensionLibrary.html#method.on_stage_deinit) +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub enum InitLevel { + /// First level loaded by Godot. Builtin types are available, classes are not. + Core, + + /// Second level loaded by Godot. Only server classes and builtins are available. + Servers, + + /// Third level loaded by Godot. Most classes are available. + Scene, + + /// Fourth level loaded by Godot, only in the editor. All classes are available. + Editor, +} + +impl InitLevel { + #[doc(hidden)] + pub fn from_sys(level: crate::GDExtensionInitializationLevel) -> Self { + match level { + crate::GDEXTENSION_INITIALIZATION_CORE => Self::Core, + crate::GDEXTENSION_INITIALIZATION_SERVERS => Self::Servers, + crate::GDEXTENSION_INITIALIZATION_SCENE => Self::Scene, + crate::GDEXTENSION_INITIALIZATION_EDITOR => Self::Editor, + _ => { + eprintln!("WARNING: unknown initialization level {level}"); + Self::Scene + } + } + } + + #[doc(hidden)] + pub fn to_sys(self) -> crate::GDExtensionInitializationLevel { + match self { + Self::Core => crate::GDEXTENSION_INITIALIZATION_CORE, + Self::Servers => crate::GDEXTENSION_INITIALIZATION_SERVERS, + Self::Scene => crate::GDEXTENSION_INITIALIZATION_SCENE, + Self::Editor => crate::GDEXTENSION_INITIALIZATION_EDITOR, + } + } + + /// Convert this initialization level to an initialization stage. + pub fn to_stage(self) -> InitStage { + match self { + Self::Core => InitStage::Core, + Self::Servers => InitStage::Servers, + Self::Scene => InitStage::Scene, + Self::Editor => InitStage::Editor, + } + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +/// Extended initialization stage that includes both initialization levels and the main loop. +/// +/// This enum extends [`InitLevel`] with a `MainLoop` variant, representing the fully initialized state of Godot +/// after all initialization levels have been loaded and before any deinitialization begins. +/// +/// During initialization, stages are loaded in order: `Core` → `Servers` → `Scene` → `Editor` (if in editor) → `MainLoop`. \ +/// During deinitialization, stages are unloaded in reverse order. +/// +/// See also: +/// - [`InitLevel`](enum.InitLevel.html): only levels, without `MainLoop`. +/// - [`ExtensionLibrary::on_stage_init()`](trait.ExtensionLibrary.html#method.on_stage_init) +/// - [`ExtensionLibrary::on_stage_deinit()`](trait.ExtensionLibrary.html#method.on_stage_deinit) +/// - [`ExtensionLibrary::on_main_loop_frame()`](trait.ExtensionLibrary.html#method.on_main_loop_frame) +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +#[non_exhaustive] +pub enum InitStage { + /// First level loaded by Godot. Builtin types are available, classes are not. + Core, + + /// Second level loaded by Godot. Only server classes and builtins are available. + Servers, + + /// Third level loaded by Godot. Most classes are available. + Scene, + + /// Fourth level loaded by Godot, only in the editor. All classes are available. + Editor, + + /// The main loop stage, representing the fully initialized state of Godot. + /// + /// This variant is only available in Godot 4.5+. In earlier versions, it will never be passed to callbacks. + #[cfg(since_api = "4.5")] + MainLoop, +} + +impl InitStage { + /// Try to convert this initialization stage to an initialization level. + /// + /// Returns `None` for [`InitStage::MainLoop`], as it doesn't correspond to a Godot initialization level. + pub fn try_to_level(self) -> Option { + match self { + Self::Core => Some(InitLevel::Core), + Self::Servers => Some(InitLevel::Servers), + Self::Scene => Some(InitLevel::Scene), + Self::Editor => Some(InitLevel::Editor), + #[cfg(since_api = "4.5")] + Self::MainLoop => None, + } + } +} diff --git a/godot-ffi/src/interface_init.rs b/godot-ffi/src/interface_init.rs index 6ea170e0e..b7b69ed71 100644 --- a/godot-ffi/src/interface_init.rs +++ b/godot-ffi/src/interface_init.rs @@ -71,7 +71,7 @@ pub fn ensure_static_runtime_compatibility( let minor = unsafe { data_ptr.offset(1).read() }; if minor == 0 { // SAFETY: at this point it's reasonably safe to say that we are indeed dealing with that version struct; read the whole. - let data_ptr = get_proc_address as *const sys::GDExtensionGodotVersion; + let data_ptr = get_proc_address as *const sys::GodotSysVersion; let runtime_version_str = unsafe { read_version_string(&data_ptr.read()) }; panic!( @@ -114,7 +114,7 @@ pub fn ensure_static_runtime_compatibility( pub unsafe fn runtime_version( get_proc_address: sys::GDExtensionInterfaceGetProcAddress, -) -> sys::GDExtensionGodotVersion { +) -> sys::GodotSysVersion { let get_proc_address = get_proc_address.expect("get_proc_address unexpectedly null"); runtime_version_inner(get_proc_address) @@ -125,17 +125,17 @@ unsafe fn runtime_version_inner( get_proc_address: unsafe extern "C" fn( *const std::ffi::c_char, ) -> sys::GDExtensionInterfaceFunctionPtr, -) -> sys::GDExtensionGodotVersion { +) -> sys::GodotSysVersion { // SAFETY: `self.0` is a valid `get_proc_address` pointer. - let get_godot_version = unsafe { get_proc_address(sys::c_str(b"get_godot_version\0")) }; //.expect("get_godot_version unexpectedly null"); + let get_godot_version = unsafe { get_proc_address(sys::c_str(sys::GET_GODOT_VERSION_SYS_STR)) }; //.expect("get_godot_version unexpectedly null"); - // SAFETY: `sys::GDExtensionInterfaceGetGodotVersion` is an `Option` of an `unsafe extern "C"` function pointer. + // SAFETY: `GDExtensionInterfaceGetGodotVersion` is an `Option` of an `unsafe extern "C"` function pointer. let get_godot_version = - crate::unsafe_cast_fn_ptr!(get_godot_version as sys::GDExtensionInterfaceGetGodotVersion); + crate::unsafe_cast_fn_ptr!(get_godot_version as sys::GetGodotSysVersion); - let mut version = std::mem::MaybeUninit::::zeroed(); + let mut version = std::mem::MaybeUninit::::zeroed(); - // SAFETY: `get_proc_address` with "get_godot_version" does return a valid `sys::GDExtensionInterfaceGetGodotVersion` pointer, and since we have a valid + // SAFETY: `get_proc_address` with "get_godot_version" does return a valid `GDExtensionInterfaceGetGodotVersion` pointer, and since we have a valid // `get_proc_address` pointer then it must be callable. unsafe { get_godot_version(version.as_mut_ptr()) }; diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index a1e2e4156..9a866cefa 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -86,8 +86,8 @@ pub use gen::table_editor_classes::*; pub use gen::table_scene_classes::*; pub use gen::table_servers_classes::*; pub use gen::table_utilities::*; -pub use gen::virtual_consts as godot_virtual_consts; pub use global::*; +pub use init_level::*; pub use string_cache::StringCache; pub use toolbox::*; @@ -99,6 +99,7 @@ pub use crate::godot_ffi::{ // API to access Godot via FFI mod binding; +mod init_level; pub use binding::*; use binding::{ @@ -110,52 +111,28 @@ use binding::{ #[cfg(not(wasm_nothreads))] static MAIN_THREAD_ID: ManualInitCell = ManualInitCell::new(); -/// Stage of the Godot initialization process. -/// -/// Godot's initialization and deinitialization processes are split into multiple stages, like a stack. At each level, -/// a different amount of engine functionality is available. Deinitialization happens in reverse order. -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] -pub enum InitLevel { - /// First level loaded by Godot. Builtin types are available, classes are not. - Core, +#[cfg(before_api = "4.5")] +mod version_symbols { - /// Second level loaded by Godot. Only server classes and builtins are available. - Servers, + pub type GodotSysVersion = super::GDExtensionGodotVersion; + pub type GetGodotSysVersion = super::GDExtensionInterfaceGetGodotVersion; + pub const GET_GODOT_VERSION_SYS_STR: &[u8] = b"get_godot_version\0"; +} - /// Third level loaded by Godot. Most classes are available. - Scene, +#[cfg(since_api = "4.5")] +mod version_symbols { + pub type GodotSysVersion = super::GDExtensionGodotVersion2; - /// Fourth level loaded by Godot, only in the editor. All classes are available. - Editor, + pub type GetGodotSysVersion = super::GDExtensionInterfaceGetGodotVersion2; + pub const GET_GODOT_VERSION_SYS_STR: &[u8] = b"get_godot_version2\0"; } -impl InitLevel { - #[doc(hidden)] - pub fn from_sys(level: crate::GDExtensionInitializationLevel) -> Self { - match level { - crate::GDEXTENSION_INITIALIZATION_CORE => Self::Core, - crate::GDEXTENSION_INITIALIZATION_SERVERS => Self::Servers, - crate::GDEXTENSION_INITIALIZATION_SCENE => Self::Scene, - crate::GDEXTENSION_INITIALIZATION_EDITOR => Self::Editor, - _ => { - eprintln!("WARNING: unknown initialization level {level}"); - Self::Scene - } - } - } - #[doc(hidden)] - pub fn to_sys(self) -> crate::GDExtensionInitializationLevel { - match self { - Self::Core => crate::GDEXTENSION_INITIALIZATION_CORE, - Self::Servers => crate::GDEXTENSION_INITIALIZATION_SERVERS, - Self::Scene => crate::GDEXTENSION_INITIALIZATION_SCENE, - Self::Editor => crate::GDEXTENSION_INITIALIZATION_EDITOR, - } - } -} +use version_symbols::*; + +// ---------------------------------------------------------------------------------------------------------------------------------------------- pub struct GdextRuntimeMetadata { - godot_version: GDExtensionGodotVersion, + godot_version: GodotSysVersion, } impl GdextRuntimeMetadata { @@ -163,7 +140,7 @@ impl GdextRuntimeMetadata { /// /// - The `string` field of `godot_version` must not be written to while this struct exists. /// - The `string` field of `godot_version` must be safe to read from while this struct exists. - pub unsafe fn new(godot_version: GDExtensionGodotVersion) -> Self { + pub unsafe fn new(godot_version: GodotSysVersion) -> Self { Self { godot_version } } } @@ -284,14 +261,32 @@ pub unsafe fn initialize( /// # Safety /// See [`initialize`]. pub unsafe fn deinitialize() { - deinitialize_binding() + deinitialize_binding(); + + // MACOS-PARTIAL-RELOAD: Clear the main thread ID to allow re-initialization during hot reload. + #[cfg(not(wasm_nothreads))] + { + if MAIN_THREAD_ID.is_initialized() { + MAIN_THREAD_ID.clear(); + } + } } -fn print_preamble(version: GDExtensionGodotVersion) { +fn safeguards_level_string() -> &'static str { + if cfg!(safeguards_strict) { + "strict" + } else if cfg!(safeguards_balanced) { + "balanced" + } else { + "disengaged" + } +} + +fn print_preamble(version: GodotSysVersion) { let api_version: &'static str = GdextBuild::godot_static_version_string(); let runtime_version = read_version_string(&version); - - println!("Initialize godot-rust (API {api_version}, runtime {runtime_version})"); + let safeguards_level = safeguards_level_string(); + println!("Initialize godot-rust (API {api_version}, runtime {runtime_version}, safeguards {safeguards_level})"); } /// # Safety diff --git a/godot-ffi/src/toolbox.rs b/godot-ffi/src/toolbox.rs index 0b8c11f2a..65dfecce3 100644 --- a/godot-ffi/src/toolbox.rs +++ b/godot-ffi/src/toolbox.rs @@ -195,6 +195,20 @@ pub fn i64_to_ordering(value: i64) -> std::cmp::Ordering { } } +/// Converts a Godot "found" index `Option`, where -1 is mapped to `None`. +pub fn found_to_option(index: i64) -> Option { + if index == -1 { + None + } else { + // If this fails, then likely because we overlooked a negative value. + let index_usize = index + .try_into() + .unwrap_or_else(|_| panic!("unexpected index {index} returned from Godot function")); + + Some(index_usize) + } +} + /* pub fn unqualified_type_name() -> &'static str { let type_name = std::any::type_name::(); @@ -402,7 +416,7 @@ pub(crate) fn load_utility_function( }) } -pub(crate) fn read_version_string(version_ptr: &sys::GDExtensionGodotVersion) -> String { +pub(crate) fn read_version_string(version_ptr: &sys::GodotSysVersion) -> String { let char_ptr = version_ptr.string; // SAFETY: GDExtensionGodotVersion has the (manually upheld) invariant of a valid string field. diff --git a/godot-macros/Cargo.toml b/godot-macros/Cargo.toml index 2110bde96..43b600966 100644 --- a/godot-macros/Cargo.toml +++ b/godot-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot-macros" -version = "0.4.0" +version = "0.4.2" edition = "2021" rust-version = "1.87" license = "MPL-2.0" @@ -29,7 +29,7 @@ litrs = { workspace = true, optional = true } venial = { workspace = true } [build-dependencies] -godot-bindings = { path = "../godot-bindings", version = "=0.4.0" } # emit_godot_version_cfg +godot-bindings = { path = "../godot-bindings", version = "=0.4.2" } # emit_godot_version_cfg # Reverse dev dependencies so doctests can use `godot::` prefix. [dev-dependencies] diff --git a/godot-macros/src/bench.rs b/godot-macros/src/bench.rs index 29a2b43c9..e729956be 100644 --- a/godot-macros/src/bench.rs +++ b/godot-macros/src/bench.rs @@ -19,23 +19,30 @@ pub fn attribute_bench(input_decl: venial::Item) -> ParseResult { _ => return bail!(&input_decl, "#[bench] can only be applied to functions"), }; - // Note: allow attributes for things like #[rustfmt] or #[clippy] - if func.generic_params.is_some() || !func.params.is_empty() || func.where_clause.is_some() { + // Disallow generics, but allow attributes for things like #[rustfmt] or #[clippy]. + if func.generic_params.is_some() || func.where_clause.is_some() { return bad_signature(&func); } - // Ignore -> (), as no one does that by accident. - // We need `ret` to make sure the type is correct and to avoid unused imports (by IDEs). - let Some(ret) = func.return_ty else { + let mut attr = KvParser::parse_required(&func.attributes, "bench", &func.name)?; + let manual = attr.handle_alone("manual")?; + let repetitions = attr.handle_usize("repeat")?; + attr.finish()?; + + // Validate attribute combinations. + if manual && repetitions.is_some() { return bail!( func, - "#[bench] function must return a value from its computation, to prevent optimizing the operation away" + "#[bench(manual)] cannot be combined with `repeat` -- pass repetitions to bench_measure() instead" ); - }; + } - let mut attr = KvParser::parse_required(&func.attributes, "bench", &func.name)?; - let repetitions = attr.handle_usize("repeat")?.unwrap_or(DEFAULT_REPETITIONS); - attr.finish()?; + let repetitions = repetitions.unwrap_or(DEFAULT_REPETITIONS); + + // Validate parameter count. + if !func.params.is_empty() { + return bad_signature(&func); + } let bench_name = &func.name; let bench_name_str = func.name.to_string(); @@ -43,23 +50,48 @@ pub fn attribute_bench(input_decl: venial::Item) -> ParseResult { let body = &func.body; // Filter out #[bench] itself, but preserve other attributes like #[allow], #[expect], etc. - let other_attributes = retain_attributes_except(&func.attributes, "bench"); + let other_attributes: Vec<_> = retain_attributes_except(&func.attributes, "bench").collect(); - Ok(quote! { - #(#other_attributes)* - pub fn #bench_name() { - for _ in 0..#repetitions { - let __ret: #ret = #body; - crate::common::bench_used(__ret); + let generated_fn = if manual { + // Manual mode: user calls bench_measure() directly. + let ret = func.return_ty; + quote! { + #(#other_attributes)* + // Don't return crate::framework::BenchResult here, to keep user imports working naturally. + pub fn #bench_name() -> #ret { + #body + } + } + } else { + // Automatic mode: framework controls timing. + // Ignore `-> ()`, as no one does that by accident. + // We need `ret` to make sure the type is correct and to avoid unused imports (by IDEs). + let Some(ret) = func.return_ty else { + return bail!( + func, + "#[bench] function must return a value from its computation, to prevent optimizing the operation away" + ); + }; + + quote! { + #(#other_attributes)* + pub fn #bench_name() -> crate::framework::BenchResult { + crate::framework::bench_measure(#repetitions, || { + let __ret: #ret = #body; + __ret // passed onto bench_used() by caller. + }) } } + }; + + Ok(quote! { + #generated_fn ::godot::sys::plugin_add!(crate::framework::__GODOT_BENCH; crate::framework::RustBenchmark { name: #bench_name_str, file: std::file!(), line: std::line!(), function: #bench_name, - repetitions: #repetitions, }); }) } @@ -68,7 +100,12 @@ fn bad_signature(func: &venial::Function) -> Result bail!( func, "#[bench] function must have one of these signatures:\ - \n fn {f}() {{ ... }}", + \n\ + \n(1) #[bench]\ + \n fn {f}() -> T {{ ... }}\ + \n\ + \n(2) #[bench(manual)]\ + \n fn {f}() -> BenchResult {{ ... /* call to bench_measure() */ }}", f = func.name, ) } diff --git a/godot-macros/src/class/data_models/field_var.rs b/godot-macros/src/class/data_models/field_var.rs index 173017a2e..9ec2271bd 100644 --- a/godot-macros/src/class/data_models/field_var.rs +++ b/godot-macros/src/class/data_models/field_var.rs @@ -18,6 +18,8 @@ use crate::{util, ParseResult}; /// Store info from `#[var]` attribute. #[derive(Clone, Debug)] pub struct FieldVar { + /// What name this variable should have in Godot, if `None` then the Rust name should be used. + pub rename: Option, pub getter: GetterSetter, pub setter: GetterSetter, pub hint: FieldHint, @@ -29,6 +31,7 @@ impl FieldVar { /// Parse a `#[var]` attribute to a `FieldVar` struct. /// /// Possible keys: + /// - `rename = ident` /// - `get = expr` /// - `set = expr` /// - `hint = ident` @@ -36,6 +39,7 @@ impl FieldVar { /// - `usage_flags = pub(crate) fn new_from_kv(parser: &mut KvParser) -> ParseResult { let span = parser.span(); + let rename = parser.handle_ident("rename")?; let getter = GetterSetter::parse(parser, "get")?; let setter = GetterSetter::parse(parser, "set")?; @@ -64,6 +68,7 @@ impl FieldVar { }; Ok(FieldVar { + rename, getter, setter, hint, @@ -84,6 +89,7 @@ impl FieldVar { impl Default for FieldVar { fn default() -> Self { Self { + rename: Default::default(), getter: Default::default(), setter: Default::default(), hint: Default::default(), @@ -131,11 +137,12 @@ impl GetterSetter { class_name: &Ident, kind: GetSet, field: &Field, + rename: &Option, ) -> Option { match self { GetterSetter::Omitted => None, GetterSetter::Generated => Some(GetterSetterImpl::from_generated_impl( - class_name, kind, field, + class_name, kind, field, rename, )), GetterSetter::Custom(function_name) => { Some(GetterSetterImpl::from_custom_impl(function_name)) @@ -173,14 +180,21 @@ pub struct GetterSetterImpl { } impl GetterSetterImpl { - fn from_generated_impl(class_name: &Ident, kind: GetSet, field: &Field) -> Self { + fn from_generated_impl( + class_name: &Ident, + kind: GetSet, + field: &Field, + rename: &Option, + ) -> Self { let Field { name: field_name, ty: field_type, .. } = field; - let function_name = format_ident!("{}{field_name}", kind.prefix()); + let var_name = rename.as_ref().unwrap_or(field_name); + + let function_name = format_ident!("{}{var_name}", kind.prefix()); let signature; let function_body; diff --git a/godot-macros/src/class/data_models/func.rs b/godot-macros/src/class/data_models/func.rs index 4c31d799c..9d8601fee 100644 --- a/godot-macros/src/class/data_models/func.rs +++ b/godot-macros/src/class/data_models/func.rs @@ -5,11 +5,11 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use proc_macro2::{Group, Ident, TokenStream, TokenTree}; +use proc_macro2::{Group, Ident, Span, TokenStream, TokenTree}; use quote::{format_ident, quote}; use crate::class::RpcAttr; -use crate::util::{bail, bail_fn, ident, safe_ident}; +use crate::util::{bail, bail_fn, ident, safe_ident, to_spanned_tuple}; use crate::{util, ParseResult}; /// Information used for registering a Rust function with Godot. @@ -51,14 +51,20 @@ impl FuncDefinition { // Virtual methods are non-static by their nature; so there's no support for static ones. pub fn make_virtual_callback( class_name: &Ident, + trait_base_class: &Ident, signature_info: &SignatureInfo, before_kind: BeforeKind, interface_trait: Option<&venial::TypeExpr>, ) -> TokenStream { let method_name = &signature_info.method_name; - let wrapped_method = - make_forwarding_closure(class_name, signature_info, before_kind, interface_trait); + let wrapped_method = make_forwarding_closure( + class_name, + trait_base_class, + signature_info, + before_kind, + interface_trait, + ); let sig_params = signature_info.params_type(); let sig_ret = &signature_info.return_type; @@ -80,7 +86,7 @@ pub fn make_virtual_callback( ret: sys::GDExtensionTypePtr, ) { let call_ctx = #call_ctx; - let _success = ::godot::private::handle_ptrcall_panic( + ::godot::private::handle_fallible_ptrcall( &call_ctx, || #invocation ); @@ -108,6 +114,7 @@ pub fn make_method_registration( let forwarding_closure = make_forwarding_closure( class_name, + class_name, // Not used in this case. signature_info, BeforeKind::Without, interface_trait, @@ -191,6 +198,7 @@ pub enum ReceiverType { pub struct SignatureInfo { pub method_name: Ident, pub receiver_type: ReceiverType, + pub params_span: Span, pub param_idents: Vec, /// Parameter types *without* receiver. pub param_types: Vec, @@ -207,6 +215,7 @@ impl SignatureInfo { Self { method_name: ident("ready"), receiver_type: ReceiverType::Mut, + params_span: Span::call_site(), param_idents: vec![], param_types: vec![], return_type: quote! { () }, @@ -214,9 +223,14 @@ impl SignatureInfo { } } - pub fn params_type(&self) -> TokenStream { - let param_types = &self.param_types; - quote! { (#(#param_types,)*) } + /// Returns params (e.g. `(v1, v2, v3...)`) of this signature as a properly spanned group. + pub fn params_tuple(&self) -> Group { + to_spanned_tuple(&self.param_idents, self.params_span) + } + + /// Returns param types (e.g. `(f32, f64, GString...)`) of this signature as a properly spanned group. + pub fn params_type(&self) -> Group { + to_spanned_tuple(&self.param_types, self.params_span) } } @@ -235,12 +249,15 @@ pub enum BeforeKind { /// Returns a closure expression that forwards the parameters to the Rust instance. fn make_forwarding_closure( class_name: &Ident, + trait_base_class: &Ident, signature_info: &SignatureInfo, before_kind: BeforeKind, interface_trait: Option<&venial::TypeExpr>, ) -> TokenStream { let method_name = &signature_info.method_name; let params = &signature_info.param_idents; + let params_tuple = signature_info.params_tuple(); + let param_ident = Ident::new("params", signature_info.params_span); let instance_decl = match &signature_info.receiver_type { ReceiverType::Ref => quote! { @@ -269,29 +286,49 @@ fn make_forwarding_closure( ReceiverType::Ref | ReceiverType::Mut => { // Generated default virtual methods (e.g. for ready) may not have an actual implementation (user code), so // all they need to do is call the __before_ready() method. This means the actual method call may be optional. - let method_call = if matches!(before_kind, BeforeKind::OnlyBefore) { - TokenStream::new() + let method_call; + let sig_tuple_annotation; + + if matches!(before_kind, BeforeKind::OnlyBefore) { + sig_tuple_annotation = TokenStream::new(); + method_call = TokenStream::new() + } else if let Some(interface_trait) = interface_trait { + // impl ITrait for Class {...} + // Virtual methods. + + let instance_ref = match signature_info.receiver_type { + ReceiverType::Ref => quote! { &instance }, + ReceiverType::Mut => quote! { &mut instance }, + _ => unreachable!("unexpected receiver type"), // checked above. + }; + + sig_tuple_annotation = make_sig_tuple_annotation(trait_base_class, method_name); + + let method_invocation = TokenStream::from_iter( + quote! {<#class_name as #interface_trait>::#method_name} + .into_iter() + .map(|mut token| { + token.set_span(signature_info.params_span); + token + }), + ); + + method_call = quote! { + #method_invocation( #instance_ref, #(#params),* ) + }; } else { - match interface_trait { - // impl ITrait for Class {...} - Some(interface_trait) => { - let instance_ref = match signature_info.receiver_type { - ReceiverType::Ref => quote! { &instance }, - ReceiverType::Mut => quote! { &mut instance }, - _ => unreachable!("unexpected receiver type"), // checked above. - }; - - quote! { <#class_name as #interface_trait>::#method_name( #instance_ref, #(#params),* ) } - } + // impl Class {...} + // Methods are non-virtual. - // impl Class {...} - None => quote! { instance.#method_name( #(#params),* ) }, - } + sig_tuple_annotation = TokenStream::new(); + method_call = quote! { + instance.#method_name( #(#params),* ) + }; }; quote! { |instance_ptr, params| { - let ( #(#params,)* ) = params; + let #params_tuple #sig_tuple_annotation = #param_ident; let storage = unsafe { ::godot::private::as_storage::<#class_name>(instance_ptr) }; @@ -305,9 +342,17 @@ fn make_forwarding_closure( ReceiverType::GdSelf => { // Method call is always present, since GdSelf implies that the user declares the method. // (Absent method is only used in the case of a generated default virtual method, e.g. for ready()). + + let sig_tuple_annotation = if interface_trait.is_some() { + make_sig_tuple_annotation(trait_base_class, method_name) + } else { + TokenStream::new() + }; + quote! { |instance_ptr, params| { - let ( #(#params,)* ) = params; + // Not using `virtual_sig`, since virtual methods with `#[func(gd_self)]` are being moved out of the trait to inherent impl. + let #params_tuple #sig_tuple_annotation = #param_ident; let storage = unsafe { ::godot::private::as_storage::<#class_name>(instance_ptr) }; @@ -321,7 +366,7 @@ fn make_forwarding_closure( // No before-call needed, since static methods are not virtual. quote! { |_, params| { - let ( #(#params,)* ) = params; + let #params_tuple = #param_ident; #class_name::#method_name(#(#params),*) } } @@ -365,6 +410,7 @@ pub(crate) fn into_signature_info( }; let num_params = signature.params.inner.len(); + let params_span = signature.span(); let mut param_idents = Vec::with_capacity(num_params); let mut param_types = Vec::with_capacity(num_params); let ret_type = match signature.return_ty { @@ -420,6 +466,7 @@ pub(crate) fn into_signature_info( SignatureInfo { method_name, receiver_type, + params_span, param_idents, param_types, return_type: ret_type, @@ -440,8 +487,12 @@ fn maybe_change_parameter_type( && param_ty.tokens.len() == 1 && param_ty.tokens[0].to_string() == "f32" { + // Retain span of input parameter -> for error messages, IDE support, etc. + let mut f64_ident = ident("f64"); + f64_ident.set_span(param_ty.span()); + Ok(venial::TypeExpr { - tokens: vec![TokenTree::Ident(ident("f64"))], + tokens: vec![TokenTree::Ident(f64_ident)], }) } else { Err(param_ty) @@ -515,7 +566,7 @@ fn make_varcall_fn(call_ctx: &TokenStream, wrapped_method: &TokenStream) -> Toke err: *mut sys::GDExtensionCallError, ) { let call_ctx = #call_ctx; - ::godot::private::handle_varcall_panic( + ::godot::private::handle_fallible_varcall( &call_ctx, &mut *err, || #invocation @@ -536,14 +587,10 @@ fn make_ptrcall_fn(call_ctx: &TokenStream, wrapped_method: &TokenStream) -> Toke ret: sys::GDExtensionTypePtr, ) { let call_ctx = #call_ctx; - let _success = ::godot::private::handle_panic( - || format!("{call_ctx}"), + ::godot::private::handle_fallible_ptrcall( + &call_ctx, || #invocation ); - - // if success.is_err() { - // // TODO set return value to T::default()? - // } } } } @@ -589,6 +636,19 @@ fn make_call_context(class_name_str: &str, method_name_str: &str) -> TokenStream } } +/// Returns a type annotation for the tuple corresponding to the signature declared on given ITrait method, +/// allowing to validate params for a generated method call at compile time. +/// +/// For example `::godot::private::virtuals::Node::Sig_physics_process` is `(f64, )`, +/// thus `let params: ::godot::private::virtuals::Node::Sig_physics_process = ();` +/// will not compile. +fn make_sig_tuple_annotation(trait_base_class: &Ident, method_name: &Ident) -> TokenStream { + let rust_sig_name = format_ident!("Sig_{method_name}"); + quote! { + : ::godot::private::virtuals::#trait_base_class::#rust_sig_name + } +} + pub fn bail_attr(attr_name: &Ident, msg: &str, method_name: &Ident) -> ParseResult { bail!(method_name, "#[{attr_name}]: {msg}") } diff --git a/godot-macros/src/class/data_models/inherent_impl.rs b/godot-macros/src/class/data_models/inherent_impl.rs index 63f988ad5..e40202538 100644 --- a/godot-macros/src/class/data_models/inherent_impl.rs +++ b/godot-macros/src/class/data_models/inherent_impl.rs @@ -91,10 +91,8 @@ pub fn transform_inherent_impl( let (funcs, signals) = process_godot_fns(&class_name, &mut impl_block, meta.secondary)?; let consts = process_godot_constants(&mut impl_block)?; - #[cfg(all(feature = "register-docs", since_api = "4.3"))] - let docs = crate::docs::document_inherent_impl(&funcs, &consts, &signals); - #[cfg(not(all(feature = "register-docs", since_api = "4.3")))] - let docs = quote! {}; + let inherent_impl_docs = + crate::docs::make_trait_docs_registration(&funcs, &consts, &signals, &class_name, &prv); // Container struct holding names of all registered #[func]s. // The struct is declared by #[derive(GodotClass)]. @@ -127,17 +125,20 @@ pub fn transform_inherent_impl( let method_storage_name = format_ident!("__registration_methods_{class_name}"); let constants_storage_name = format_ident!("__registration_constants_{class_name}"); - let fill_storage = quote! { - ::godot::sys::plugin_execute_pre_main!({ - #method_storage_name.lock().unwrap().push(|| { - #( #method_registrations )* - #( #signal_registrations )* - }); + let fill_storage = { + quote! { + ::godot::sys::plugin_execute_pre_main!({ + #method_storage_name.lock().unwrap().push(|| { + #( #method_registrations )* + #( #signal_registrations )* + }); + + #constants_storage_name.lock().unwrap().push(|| { + #constant_registration + }); - #constants_storage_name.lock().unwrap().push(|| { - #constant_registration }); - }); + } }; if !meta.secondary { @@ -175,7 +176,7 @@ pub fn transform_inherent_impl( let class_registration = quote! { ::godot::sys::plugin_add!(#prv::__GODOT_PLUGIN_REGISTRY; #prv::ClassPlugin::new::<#class_name>( - #prv::PluginItem::InherentImpl(#prv::InherentImpl::new::<#class_name>(#docs)) + #prv::PluginItem::InherentImpl(#prv::InherentImpl::new::<#class_name>()) )); }; @@ -189,6 +190,7 @@ pub fn transform_inherent_impl( #( #func_name_constants )* } #signal_symbol_types + #inherent_impl_docs }; Ok(result) @@ -202,6 +204,7 @@ pub fn transform_inherent_impl( impl #funcs_collection { #( #func_name_constants )* } + #inherent_impl_docs }; Ok(result) @@ -422,7 +425,11 @@ fn add_virtual_script_call( rename: &Option, gd_self_parameter: Option, ) -> String { - assert!(cfg!(since_api = "4.3")); + #[allow(clippy::assertions_on_constants)] + { + // Without braces, clippy removes the #[allow] for some reason... + assert!(cfg!(since_api = "4.3")); + } // Update parameter names, so they can be forwarded (e.g. a "_" declared by the user cannot). let is_params = function.params.iter_mut().skip(1); // skip receiver. diff --git a/godot-macros/src/class/data_models/interface_trait_impl.rs b/godot-macros/src/class/data_models/interface_trait_impl.rs index 5d78ef3ca..ebcd7525d 100644 --- a/godot-macros/src/class/data_models/interface_trait_impl.rs +++ b/godot-macros/src/class/data_models/interface_trait_impl.rs @@ -20,10 +20,11 @@ pub fn transform_trait_impl(mut original_impl: venial::Impl) -> ParseResult ParseResult ParseResult(#docs); + let mut item = #prv::ITraitImpl::new::<#class_name>(); #(#modifications)* item } @@ -177,7 +178,7 @@ pub fn transform_trait_impl(mut original_impl: venial::Impl) -> ParseResult ParseResult ::godot::sys::GDExtensionClassCallVirtual { //println!("virtual_call: {}.{}", std::any::type_name::(), name); use ::godot::obj::UserClass as _; - use ::godot::sys::godot_virtual_consts::#trait_base_class as virtuals; + use ::godot::private::virtuals::#trait_base_class as virtuals; #tool_check match #match_expr { @@ -202,6 +203,8 @@ pub fn transform_trait_impl(mut original_impl: venial::Impl) -> ParseResult( #prv::PluginItem::ITraitImpl(#item_constructor) )); + + #register_docs }; // #decls still holds a mutable borrow to `original_impl`, so we mutate && append it afterwards. @@ -607,7 +610,7 @@ fn handle_regular_virtual_fn<'a>( cfg_attrs, rust_method_name: virtual_method_name, // If ever the `I*` verbatim validation is relaxed (it won't work with use-renames or other weird edge cases), the approach - // with godot_virtual_consts module could be changed to something like the following (GodotBase = nearest Godot base class): + // with godot::private::virtuals module could be changed to something like the following (GodotBase = nearest Godot base class): // __get_virtual_hash::("method") godot_name_hash_constant: quote! { virtuals::#method_name_ident }, signature_info, @@ -716,13 +719,14 @@ struct OverriddenVirtualFn<'a> { } impl OverriddenVirtualFn<'_> { - fn make_match_arm(&self, class_name: &Ident) -> TokenStream { + fn make_match_arm(&self, class_name: &Ident, trait_base_class: &Ident) -> TokenStream { let cfg_attrs = self.cfg_attrs.iter(); let godot_name_hash_constant = &self.godot_name_hash_constant; // Lazily generate code for the actual work (calling user function). let method_callback = make_virtual_callback( class_name, + trait_base_class, &self.signature_info, self.before_kind, self.interface_trait.as_ref(), diff --git a/godot-macros/src/class/data_models/property.rs b/godot-macros/src/class/data_models/property.rs index 75671575e..b98bed71c 100644 --- a/godot-macros/src/class/data_models/property.rs +++ b/godot-macros/src/class/data_models/property.rs @@ -76,10 +76,8 @@ pub fn make_property_impl(class_name: &Ident, fields: &Fields) -> TokenStream { }; make_groups_registrations(group, subgroup, &mut export_tokens, class_name); - - let field_name = field_ident.to_string(); - let FieldVar { + rename, getter, setter, hint, @@ -87,6 +85,8 @@ pub fn make_property_impl(class_name: &Ident, fields: &Fields) -> TokenStream { .. } = var; + let field_name = rename.as_ref().unwrap_or(field_ident).to_string(); + let export_hint; let registration_fn; @@ -151,14 +151,14 @@ pub fn make_property_impl(class_name: &Ident, fields: &Fields) -> TokenStream { // Note: {getter,setter}_tokens can be either a path `Class_Functions::constant_name` or an empty string `""`. let getter_tokens = make_getter_setter( - getter.to_impl(class_name, GetSet::Get, field), + getter.to_impl(class_name, GetSet::Get, field, &rename), &mut getter_setter_impls, &mut func_name_consts, &mut export_tokens, class_name, ); let setter_tokens = make_getter_setter( - setter.to_impl(class_name, GetSet::Set, field), + setter.to_impl(class_name, GetSet::Set, field, &rename), &mut getter_setter_impls, &mut func_name_consts, &mut export_tokens, diff --git a/godot-macros/src/class/derive_godot_class.rs b/godot-macros/src/class/derive_godot_class.rs index d487f402b..532fe7660 100644 --- a/godot-macros/src/class/derive_godot_class.rs +++ b/godot-macros/src/class/derive_godot_class.rs @@ -52,7 +52,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { let class_name = &class.name; let class_name_str: String = struct_cfg .rename - .map_or_else(|| class.name.clone(), |rename| rename) + .unwrap_or_else(|| class.name.clone()) .to_string(); let class_name_allocation = quote! { ClassId::__alloc_next_unicode(#class_name_str) }; @@ -61,18 +61,21 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { modifiers.push(quote! { with_internal }) } let base_ty = &struct_cfg.base_ty; - #[cfg(all(feature = "register-docs", since_api = "4.3"))] - let docs = - crate::docs::document_struct(base_ty.to_string(), &class.attributes, &fields.all_fields); - #[cfg(not(all(feature = "register-docs", since_api = "4.3")))] - let docs = quote! {}; + let prv = quote! { ::godot::private }; + + let struct_docs_registration = crate::docs::make_struct_docs_registration( + base_ty.to_string(), + &class.attributes, + &fields.all_fields, + class_name, + &prv, + ); let base_class = quote! { ::godot::classes::#base_ty }; // Use this name because when typing a non-existent class, users will be met with the following error: // could not find `inherit_from_OS__ensure_class_exists` in `class_macros`. let inherits_macro_ident = format_ident!("inherit_from_{}__ensure_class_exists", base_ty); - let prv = quote! { ::godot::private }; let godot_exports_impl = make_property_impl(class_name, &fields); let godot_withbase_impl = if let Some(Field { name, ty, .. }) = &fields.base_field { @@ -96,8 +99,12 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { TokenStream::new() }; - let (user_class_impl, has_default_virtual) = - make_user_class_impl(class_name, struct_cfg.is_tool, &fields.all_fields); + let (user_class_impl, has_default_virtual) = make_user_class_impl( + class_name, + &struct_cfg.base_ty, + struct_cfg.is_tool, + &fields.all_fields, + ); let mut init_expecter = TokenStream::new(); let mut godot_init_impl = TokenStream::new(); @@ -188,9 +195,10 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { #( #deprecations )* #( #errors )* + #struct_docs_registration ::godot::sys::plugin_add!(#prv::__GODOT_PLUGIN_REGISTRY; #prv::ClassPlugin::new::<#class_name>( #prv::PluginItem::Struct( - #prv::Struct::new::<#class_name>(#docs)#(.#modifiers())* + #prv::Struct::new::<#class_name>()#(.#modifiers())* ) )); @@ -282,7 +290,7 @@ pub fn make_existence_check(ident: &Ident) -> TokenStream { // ---------------------------------------------------------------------------------------------------------------------------------------------- // Implementation -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Eq, PartialEq)] enum InitStrategy { Generated, UserDefined, @@ -411,6 +419,7 @@ fn make_oneditor_panic_inits(class_name: &Ident, all_fields: &[Field]) -> TokenS fn make_user_class_impl( class_name: &Ident, + trait_base_class: &Ident, is_tool: bool, all_fields: &[Field], ) -> (TokenStream, bool) { @@ -421,7 +430,6 @@ fn make_user_class_impl( let rpc_registrations = TokenStream::new(); let onready_inits = make_onready_init(all_fields); - let oneditor_panic_inits = make_oneditor_panic_inits(class_name, all_fields); let run_before_ready = !onready_inits.is_empty() || !oneditor_panic_inits.is_empty(); @@ -430,8 +438,13 @@ fn make_user_class_impl( let tool_check = util::make_virtual_tool_check(); let signature_info = SignatureInfo::fn_ready(); - let callback = - make_virtual_callback(class_name, &signature_info, BeforeKind::OnlyBefore, None); + let callback = make_virtual_callback( + class_name, + trait_base_class, + &signature_info, + BeforeKind::OnlyBefore, + None, + ); // See also __virtual_call() codegen. // This doesn't explicitly check if the base class inherits from Node (and thus has `_ready`), but the derive-macro already does @@ -440,7 +453,7 @@ fn make_user_class_impl( if cfg!(since_api = "4.4") { hash_param = quote! { hash: u32, }; matches_ready_hash = quote! { - (name, hash) == ::godot::sys::godot_virtual_consts::Node::ready + (name, hash) == ::godot::private::virtuals::Node::ready }; } else { hash_param = TokenStream::new(); @@ -518,11 +531,12 @@ fn parse_struct_attributes(class: &venial::Struct) -> ParseResult - ::godot::__deprecated::emit_deprecated_warning!(class_editor_plugin); - }); + // Removed #[class(editor_plugin)] + if let Some(key) = parser.handle_alone_with_span("editor_plugin")? { + return bail!( + key, + "#[class(editor_plugin)] has been removed in favor of #[class(tool, base=EditorPlugin)]", + ); } // #[class(rename = NewName)] @@ -543,18 +557,24 @@ fn parse_struct_attributes(class: &venial::Struct) -> ParseResult - ::godot::__deprecated::emit_deprecated_warning!(class_hidden); - }); + // Removed #[class(hidden)] + if let Some(key) = parser.handle_alone_with_span("hidden")? { + return bail!( + key, + "#[class(hidden)] has been renamed to #[class(internal)]", + ); } parser.finish()?; } + // Deprecated: #[class(no_init)] with base=EditorPlugin + if matches!(init_strategy, InitStrategy::Absent) && base_ty == ident("EditorPlugin") { + deprecations.push(quote! { + ::godot::__deprecated::emit_deprecated_warning!(class_no_init_editor_plugin); + }); + } + post_validate(&base_ty, is_tool)?; Ok(ClassAttributes { @@ -574,6 +594,7 @@ fn parse_fields( ) -> ParseResult { let mut all_fields = vec![]; let mut base_field = Option::::None; + #[allow(unused_mut)] // Less chore when adding/removing deprecations. let mut deprecations = vec![]; let mut errors = vec![]; @@ -620,21 +641,12 @@ fn parse_fields( }); } - // Deprecated #[init(default = expr)] - if let Some((key, default)) = parser.handle_expr_with_key("default")? { - if field.default_val.is_some() { - return bail!( - key, - "Cannot use both `val` and `default` keys in #[init]; prefer using `val`" - ); - } - field.default_val = Some(FieldDefault { - default_val: default, - span: parser.span(), - }); - deprecations.push(quote_spanned! { parser.span()=> - ::godot::__deprecated::emit_deprecated_warning!(init_default); - }) + // Removed #[init(default = ...)] + if let Some((key, _default)) = parser.handle_expr_with_key("default")? { + return bail!( + key, + "#[init(default = ...)] has been renamed to #[init(val = ...)]", + ); } // #[init(node = "PATH")] diff --git a/godot-macros/src/docs.rs b/godot-macros/src/docs/extract_docs.rs similarity index 91% rename from godot-macros/src/docs.rs rename to godot-macros/src/docs/extract_docs.rs index de57c209b..36070330f 100644 --- a/godot-macros/src/docs.rs +++ b/godot-macros/src/docs/extract_docs.rs @@ -4,13 +4,11 @@ * 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 markdown_converter; - use proc_macro2::{Ident, TokenStream}; use quote::{quote, ToTokens}; use crate::class::{ConstDefinition, Field, FuncDefinition, SignalDefinition}; +use crate::docs::markdown_converter; #[derive(Default)] struct XmlParagraphs { @@ -24,33 +22,40 @@ struct XmlParagraphs { deprecated_attr: String, } +pub struct InherentImplXmlDocs { + pub method_xml_elems: String, + pub constant_xml_elems: String, + pub signal_xml_elems: String, +} + /// Returns code containing the doc information of a `#[derive(GodotClass)] struct MyClass` declaration iff class or any of its members is documented. pub fn document_struct( base: String, description: &[venial::Attribute], fields: &[Field], ) -> TokenStream { - let base_escaped = xml_escape(base); let XmlParagraphs { description_content, deprecated_attr, experimental_attr, } = attribute_docs_to_xml_paragraphs(description).unwrap_or_default(); - let members = fields + let properties = fields .iter() .filter(|field| field.var.is_some() || field.export.is_some()) .filter_map(format_member_xml) .collect::(); + let base_escaped = xml_escape(base); + quote! { - ::godot::docs::StructDocs { - base: #base_escaped, - description: #description_content, - experimental: #experimental_attr, - deprecated: #deprecated_attr, - members: #members, - } + ::godot::docs::StructDocs { + base: #base_escaped, + description: #description_content, + experimental: #experimental_attr, + deprecated: #deprecated_attr, + properties: #properties, + } } } @@ -59,39 +64,27 @@ pub fn document_inherent_impl( functions: &[FuncDefinition], constants: &[ConstDefinition], signals: &[SignalDefinition], -) -> TokenStream { - let group_xml_block = |s: String, tag: &str| -> String { - if s.is_empty() { - s - } else { - format!("<{tag}>{s}") - } - }; - +) -> InherentImplXmlDocs { let signal_xml_elems = signals .iter() .filter_map(format_signal_xml) .collect::(); - let signals_block = group_xml_block(signal_xml_elems, "signals"); let constant_xml_elems = constants .iter() .map(|ConstDefinition { raw_constant }| raw_constant) .filter_map(format_constant_xml) .collect::(); - let constants_block = group_xml_block(constant_xml_elems, "constants"); let method_xml_elems = functions .iter() .filter_map(format_method_xml) .collect::(); - quote! { - ::godot::docs::InherentImplDocs { - methods: #method_xml_elems, - signals_block: #signals_block, - constants_block: #constants_block, - } + InherentImplXmlDocs { + method_xml_elems, + constant_xml_elems, + signal_xml_elems, } } @@ -123,9 +116,8 @@ fn extract_docs_from_attributes(doc: &[venial::Attribute]) -> impl Iterator TokenStream { + let struct_docs = extract_docs::document_struct(base, description, fields); + quote! { + ::godot::sys::plugin_add!(#prv::__GODOT_DOCS_REGISTRY; #prv::DocsPlugin::new::<#class_name>( + #prv::DocsItem::Struct( + #struct_docs + ) + )); + } + } + + pub fn make_trait_docs_registration( + functions: &[FuncDefinition], + constants: &[ConstDefinition], + signals: &[SignalDefinition], + class_name: &Ident, + prv: &TokenStream, + ) -> TokenStream { + let extract_docs::InherentImplXmlDocs { + method_xml_elems, + constant_xml_elems, + signal_xml_elems, + } = extract_docs::document_inherent_impl(functions, constants, signals); + + quote! { + ::godot::sys::plugin_add!(#prv::__GODOT_DOCS_REGISTRY; #prv::DocsPlugin::new::<#class_name>( + #prv::DocsItem::InherentImpl(#prv::InherentImplDocs { + methods_xml: #method_xml_elems, + signals_xml: #signal_xml_elems, + constants_xml: #constant_xml_elems + }) + )); + } + } + + pub fn make_interface_impl_docs_registration( + impl_members: &[venial::ImplMember], + class_name: &Ident, + prv: &TokenStream, + ) -> TokenStream { + let virtual_methods = extract_docs::document_interface_trait_impl(impl_members); + + quote! { + ::godot::sys::plugin_add!(#prv::__GODOT_DOCS_REGISTRY; #prv::DocsPlugin::new::<#class_name>( + #prv::DocsItem::ITraitImpl(#virtual_methods) + )); + } + } +} + +#[cfg(all(feature = "register-docs", since_api = "4.3"))] +pub use docs_generators::*; + +#[cfg(not(all(feature = "register-docs", since_api = "4.3")))] +mod placeholders { + use super::*; + + pub fn make_struct_docs_registration( + _base: String, + _description: &[venial::Attribute], + _fields: &[Field], + _class_name: &Ident, + _prv: &TokenStream, + ) -> TokenStream { + TokenStream::new() + } + + pub fn make_trait_docs_registration( + _functions: &[FuncDefinition], + _constants: &[ConstDefinition], + _signals: &[SignalDefinition], + _class_name: &Ident, + _prv: &proc_macro2::TokenStream, + ) -> TokenStream { + TokenStream::new() + } + + pub fn make_interface_impl_docs_registration( + _impl_members: &[venial::ImplMember], + _class_name: &Ident, + _prv: &TokenStream, + ) -> TokenStream { + TokenStream::new() + } +} + +#[cfg(not(all(feature = "register-docs", since_api = "4.3")))] +pub use placeholders::*; diff --git a/godot-macros/src/lib.rs b/godot-macros/src/lib.rs index 8ba7d92ae..3970094f5 100644 --- a/godot-macros/src/lib.rs +++ b/godot-macros/src/lib.rs @@ -13,7 +13,6 @@ mod bench; mod class; mod derive; -#[cfg(all(feature = "register-docs", since_api = "4.3"))] mod docs; mod ffi_macros; mod gdextension; @@ -207,7 +206,23 @@ use crate::util::{bail, ident, KvParser}; /// } /// ``` /// -/// To create a property without a backing field to store data, you can use [`PhantomVar`](../obj/struct.PhantomVar.html). +/// If you want the field to have a different name in Godot and Rust, you can use `rename`: +/// +/// ``` +/// # use godot::prelude::*; +/// #[derive(GodotClass)] +/// # #[class(init)] +/// struct MyStruct { +/// #[var(rename = my_godot_field)] +/// my_rust_field: i64, +/// } +/// ``` +/// +/// With this, you can access this field as `my_rust_field` in Rust code, however when accessing it from Godot you have to +/// use `my_godot_field` instead (including when using methods such as [`Object::get`](../classes/struct.Object.html#method.get)). +/// The generated getters and setters will also be named `get/set_my_godot_field`, instead of `get/set_my_rust_field`. +/// +/// To create a property without a backing field to store data, you can use [`PhantomVar`](../prelude/struct.PhantomVar.html). /// This disables autogenerated getters and setters for that field. /// /// ## Export properties -- `#[export]` diff --git a/godot-macros/src/util/mod.rs b/godot-macros/src/util/mod.rs index 082dedd71..cfe292034 100644 --- a/godot-macros/src/util/mod.rs +++ b/godot-macros/src/util/mod.rs @@ -269,6 +269,16 @@ pub fn is_cfg_or_cfg_attr(attr: &venial::Attribute) -> bool { false } +/// Returns group representing properly spanned tuple (e.g. `(arg1, arg2, arg3)`). +/// +/// Use it to preserve span in case if tuple in question is empty (will create properly spanned `()` in such a case). +pub fn to_spanned_tuple(items: &[impl ToTokens], span: Span) -> Group { + let mut group = Group::new(Delimiter::Parenthesis, quote! { #(#items,)* }); + group.set_span(span); + + group +} + pub(crate) fn extract_cfg_attrs( attrs: &[venial::Attribute], ) -> impl IntoIterator { diff --git a/godot/Cargo.toml b/godot/Cargo.toml index 4fb405634..456d48be8 100644 --- a/godot/Cargo.toml +++ b/godot/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "godot" -version = "0.4.0" +version = "0.4.2" edition = "2021" rust-version = "1.87" license = "MPL-2.0" @@ -10,7 +10,7 @@ description = "Rust bindings for Godot 4" authors = ["Bromeon", "godot-rust contributors"] repository = "https://github.com/godot-rust/gdext" homepage = "https://godot-rust.github.io" -documentation = "https://docs.rs/godot/0.4.0" +documentation = "https://docs.rs/godot/0.4.2" readme = "crate-readme.md" [features] @@ -19,6 +19,7 @@ custom-json = ["api-custom-json"] double-precision = ["godot-core/double-precision"] experimental-godot-api = ["godot-core/experimental-godot-api"] experimental-threads = ["godot-core/experimental-threads"] +experimental-required-objs = ["godot-core/experimental-required-objs"] experimental-wasm = [] experimental-wasm-nothreads = ["godot-core/experimental-wasm-nothreads"] codegen-rustfmt = ["godot-core/codegen-rustfmt"] @@ -39,6 +40,10 @@ api-4-4 = ["godot-core/api-4-4"] api-4-5 = ["godot-core/api-4-5"] # ]] +# Safeguard levels (see godot/lib.rs for detailed documentation). +safeguards-dev-balanced = ["godot-core/safeguards-dev-balanced"] +safeguards-release-disengaged = ["godot-core/safeguards-release-disengaged"] + default = ["__codegen-full"] # Private features, they are under no stability guarantee @@ -47,8 +52,8 @@ __debug-log = ["godot-core/debug-log"] __trace = ["godot-core/trace"] [dependencies] -godot-core = { path = "../godot-core", version = "=0.4.0" } -godot-macros = { path = "../godot-macros", version = "=0.4.0" } +godot-core = { path = "../godot-core", version = "=0.4.2" } +godot-macros = { path = "../godot-macros", version = "=0.4.2" } # https://docs.rs/about/metadata [package.metadata.docs.rs] diff --git a/godot/src/lib.rs b/godot/src/lib.rs index ab149d85c..37beda33e 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -58,6 +58,47 @@ //!

//! //! +//! ## Safeguard levels +//! +//! godot-rust uses three tiers that differ in the amount of runtime checks and validations that are performed. \ +//! They can be configured via [Cargo features](#cargo-features). +//! +//! - 🛡️ **Strict** (default for dev builds) +//! +//! Lots of additional, sometimes expensive checks. Detects many bugs during development. +//! - `Gd::bind/bind_mut()` provides extensive diagnostics to locate runtime borrow errors. +//! - `Array` safe conversion checks (for types like `Array`). +//! - RTTI checks on object access (protect against type mismatch edge cases). +//! - Geometric invariants (e.g. normalized quaternions). +//! - Access to engine APIs outside valid scope.

+//! +//! - ⚖️ **Balanced** (default for release builds) +//! +//! Basic validity and invariant checks, reasonably fast. Within this level, you should not be able to encounter undefined behavior (UB) +//! in safe Rust code. Invariant violations may however cause panics and logic errors. +//! - Object liveness checks. +//! - `Gd::bind/bind_mut()` cause panics on borrow errors.

+//! +//! - ☣️ **Disengaged** +//! +//! Most checks disabled, sacrifices safety for raw speed. This renders a large part of the godot-rust API `unsafe` without polluting the +//! code; you opt in via `unsafe impl ExtensionLibrary`. +//! +//! Before using this, measure to ensure you truly need the last bit of performance (balanced should be fast enough for most cases; if not, +//! consider bringing it up). Also test your code thoroughly using the other levels first. Undefined behavior and crashes arising +//! from using this level are your full responsibility. When reporting a bug, make sure you can reproduce it under the balanced level. +//! - Unchecked object access -> instant UB if an object is dead. +//! - `Gd::bind/bind_mut()` are unchecked -> UB if mutable aliasing occurs. +//! +//!

+//!

Safeguards are a recent addition to godot-rust and need calibrating over time. If you are unhappy with how the balanced level +//! performs in basic operations, consider bringing it up for discussion. We'd like to offer the disengaged level for power users who +//! really need it, but it shouldn't be the only choice for decent runtime performance, as it comes with heavy trade-offs.

+//! +//!

As of v0.4, the above checks are not fully implemented yet. Neither are they guarantees; categorization may change over time.

+//!
+//! +//! //! ## Cargo features //! //! The following features can be enabled for this crate. All of them are off by default. @@ -88,7 +129,13 @@ //! * **`experimental-godot-api`** //! //! Access to `godot::classes` APIs that Godot marks "experimental". These are under heavy development and may change at any time. -//! If you opt in to this feature, expect breaking changes at compile and runtime. +//! If you opt in to this feature, expect breaking changes at compile and runtime.

+//! +//! * **`experimental-required-objs`** +//! +//! Enables _required_ objects in Godot function signatures. When GDExtension advertises parameters or return value as required (non-null), the +//! generated code will use `Gd` instead of `Option>` for type safety. This will undergo many breaking changes as the API evolves; +//! we are explicitly excluding this from any SemVer guarantees. Needs Godot 4.6-dev. See . //! //! _Rust functionality toggles:_ //! @@ -114,9 +161,9 @@ //! //! By default, Wasm threads are enabled and require the flag `"-C", "link-args=-pthread"` in the `wasm32-unknown-unknown` target. //! This must be kept in sync with Godot's Web export settings (threading support enabled). To disable it, use **additionally* the feature -//! `experimental-wasm-nothreads`.

+//! `experimental-wasm-nothreads`. //! -//! It is recommended to use this feature in combination with `lazy-function-tables` to reduce the size of the generated Wasm binary. +//! It is recommended to use this feature in combination with `lazy-function-tables` to reduce the size of the generated Wasm binary.

//! //! * **`experimental-wasm-nothreads`** //! @@ -136,7 +183,19 @@ //! This feature requires at least Godot 4.3. //! See also: [`#[derive(GodotClass)]`](register/derive.GodotClass.html#documentation) //! -//! _Integrations:_ +//! _Safeguards:_ +//! +//! See [Safeguard levels](#safeguard-levels). +//! +//! * **`safeguards-dev-balanced`** +//! +//! For the `dev` Cargo profile, use the **balanced** safeguard level instead of the default strict level.

+//! +//! * **`safeguards-release-disengaged`** +//! +//! For the `release` Cargo profile, use the **disengaged** safeguard level instead of the default balanced level. +//! +//! _Third-party integrations:_ //! //! * **`serde`** //! @@ -220,3 +279,19 @@ pub use godot_core::private; /// Often-imported symbols. pub mod prelude; + +/// Tests for code that must not compile. +// Do not add #[cfg(test)], it seems to break `cargo test -p godot --features godot/api-custom,godot/experimental-required-objs`. +mod no_compile_tests { + /// ```compile_fail + /// use godot::prelude::*; + /// let mut node: Gd = todo!(); + /// let option = Some(node.clone()); + /// let option: Option<&Gd> = option.as_ref(); + /// + /// // Following must not compile since `add_child` accepts only required (non-null) arguments. Comment-out for sanity check. + /// node.add_child(option); + /// ``` + #[cfg(feature = "experimental-required-objs")] + fn __test_invalid_patterns() {} +} diff --git a/godot/src/prelude.rs b/godot/src/prelude.rs index 9d479b075..1188b155b 100644 --- a/godot/src/prelude.rs +++ b/godot/src/prelude.rs @@ -13,7 +13,7 @@ pub use super::classes::{ pub use super::global::{ godot_error, godot_print, godot_print_rich, godot_script_error, godot_warn, }; -pub use super::init::{gdextension, ExtensionLibrary, InitLevel}; +pub use super::init::{gdextension, ExtensionLibrary, InitLevel, InitStage}; pub use super::meta::error::{ConvertError, IoError}; pub use super::meta::{FromGodot, GodotConvert, ToGodot}; pub use super::obj::{ diff --git a/itest/godot/SpecialTests.gd b/itest/godot/SpecialTests.gd index e787a45b0..0f752b24a 100644 --- a/itest/godot/SpecialTests.gd +++ b/itest/godot/SpecialTests.gd @@ -57,3 +57,17 @@ func test_collision_object_2d_input_event(): window.queue_free() +func test_autoload(): + var fetched = Engine.get_main_loop().get_root().get_node_or_null("/root/MyAutoload") + assert_that(fetched != null, "MyAutoload should be loaded") + + var by_class: AutoloadClass = fetched + assert_eq(by_class.verify_works(), 787, "Autoload typed by class") + + var by_class_symbol: AutoloadClass = MyAutoload + assert_eq(by_class_symbol.verify_works(), 787, "Autoload typed by class") + + # Autoload in GDScript can be referenced by class name or autoload name, however autoload as a type is only available in Godot 4.3+. + # See https://github.com/godot-rust/gdext/pull/1381#issuecomment-3446111511. + # var by_name: MyAutoload = fetched + # assert_eq(by_name.verify_works(), 787, "Autoload typed by name") diff --git a/itest/godot/TestRunner.gd b/itest/godot/TestRunner.gd index a6f1bc503..21b43dafb 100644 --- a/itest/godot/TestRunner.gd +++ b/itest/godot/TestRunner.gd @@ -9,11 +9,11 @@ extends Node class_name GDScriptTestRunner func _ready(): - # Check that tests are invoked from the command line. Loading the editor may break some parts (e.g. generated test code). - # Both checks are needed (it's possible to invoke `godot -e --headless`). - if Engine.is_editor_hint() || DisplayServer.get_name() != 'headless': - push_error("Integration tests must be run in headless mode (without editor).") - get_tree().quit(2) + # Don't run tests when opened in the editor. + if Engine.is_editor_hint(): + if DisplayServer.get_name() == 'headless': + push_error("Opening itest in editor in headless mode is not supported.") + get_tree().quit(2) return # Ensure physics is initialized, for tests that require it. diff --git a/itest/godot/TestRunner.tscn b/itest/godot/TestRunner.tscn index dbd5cd740..cf2dc9b53 100644 --- a/itest/godot/TestRunner.tscn +++ b/itest/godot/TestRunner.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=3 uid="uid://dgcj68l8n6wpb"] -[ext_resource type="Script" path="res://TestRunner.gd" id="1_wdbrf"] +[ext_resource type="Script" uid="uid://dcsm6ho05dipr" path="res://TestRunner.gd" id="1_wdbrf"] [node name="TestRunner" type="Node"] script = ExtResource("1_wdbrf") diff --git a/itest/godot/gdscript_tests/AutoloadScene.tscn b/itest/godot/gdscript_tests/AutoloadScene.tscn new file mode 100644 index 000000000..f1404c188 --- /dev/null +++ b/itest/godot/gdscript_tests/AutoloadScene.tscn @@ -0,0 +1,3 @@ +[gd_scene format=3 uid="uid://csf04mj3dj8bn"] + +[node name="AutoloadNode" type="AutoloadClass"] diff --git a/itest/godot/input/GenFfiTests.template.gd b/itest/godot/input/GenFfiTests.template.gd index c01966ecd..a732a3e97 100644 --- a/itest/godot/input/GenFfiTests.template.gd +++ b/itest/godot/input/GenFfiTests.template.gd @@ -67,6 +67,21 @@ func test_ptrcall_IDENT(): mark_test_succeeded() #) +# Functions that are invoked via ptrcall do not have an API to propagate the error back to the caller, but Godot pre-initializes their +# return value to the default value of that type. This test verifies that in case of panic, the default value (per Godot) is returned. +#( +func test_ptrcall_panic_IDENT(): + mark_test_pending() + var ffi := GenFfi.new() + + var from_rust: TYPE = ffi.panic_IDENT() + _check_callconv("panic_IDENT", "ptrcall") + + var expected_default: TYPE # initialize to default (null for objects) + assert_eq(from_rust, expected_default, "return value from panicked ptrcall fn == default value") + mark_test_succeeded() +#) + #( func test_ptrcall_static_IDENT(): mark_test_pending() diff --git a/itest/godot/project.godot b/itest/godot/project.godot index 049e2ba0a..fdccb81bb 100644 --- a/itest/godot/project.godot +++ b/itest/godot/project.godot @@ -15,6 +15,10 @@ run/main_scene="res://TestRunner.tscn" config/features=PackedStringArray("4.2") run/flush_stdout_on_print=true +[autoload] + +MyAutoload="*res://gdscript_tests/AutoloadScene.tscn" + [debug] gdscript/warnings/shadowed_variable=0 diff --git a/itest/hot-reload/godot/run-test.sh b/itest/hot-reload/godot/run-test.sh index 828f8e583..98eda93fe 100755 --- a/itest/hot-reload/godot/run-test.sh +++ b/itest/hot-reload/godot/run-test.sh @@ -37,6 +37,39 @@ cleanup() { set -euo pipefail trap cleanup EXIT +godotAwait() { + if [[ $godotPid -ne 0 ]]; then + echo "[Bash] Error: godotAwait called while Godot (PID $godotPid) is still running." + exit 1 + fi + + $GODOT4_BIN -e --headless --path $rel & + godotPid=$! + echo "[Bash] Wait for Godot ready (PID $godotPid)..." + + $GODOT4_BIN --headless --no-header --script ReloadOrchestrator.gd -- await +} + +godotNotify() { + if [[ $godotPid -eq 0 ]]; then + echo "[Bash] Error: godotNotify called but Godot is not running." + exit 1 + fi + + $GODOT4_BIN --headless --no-header --script ReloadOrchestrator.gd -- notify + + echo "[Bash] Wait for Godot exit..." + local status=0 + wait $godotPid + status=$? + echo "[Bash] Godot (PID $godotPid) has completed with status $status." + godotPid=0 + + if [[ $status -ne 0 ]]; then + exit $status + fi +} + echo "[Bash] Start hot-reload integration test..." # Restore un-reloaded file (for local testing). @@ -54,22 +87,25 @@ cargo build -p hot-reload $cargoArgs # Wait briefly so artifacts are present on file system. sleep 0.5 -$GODOT4_BIN -e --headless --path $rel & -godotPid=$! -echo "[Bash] Wait for Godot ready (PID $godotPid)..." +# ---------------------------------------------------------------- +# Test Case 1: Update Rust source and compile to trigger reload. +# ---------------------------------------------------------------- -$GODOT4_BIN --headless --no-header --script ReloadOrchestrator.gd -- await -$GODOT4_BIN --headless --no-header --script ReloadOrchestrator.gd -- replace +echo "[Bash] Scenario 1: Reload after updating Rust source..." +godotAwait +$GODOT4_BIN --headless --no-header --script ReloadOrchestrator.gd -- replace # Compile updated Rust source. cargo build -p hot-reload $cargoArgs +godotNotify -$GODOT4_BIN --headless --no-header --script ReloadOrchestrator.gd -- notify - -echo "[Bash] Wait for Godot exit..." -wait $godotPid -status=$? -echo "[Bash] Godot (PID $godotPid) has completed with status $status." - +# ---------------------------------------------------------------- +# Test Case 2: Touch the .gdextension file to trigger reload. +# ---------------------------------------------------------------- +echo "[Bash] Scenario 2: Reload after touching rust.gdextension..." +godotAwait +# Update timestamp to trigger reload. +touch "$rel/rust.gdextension" +godotNotify diff --git a/itest/hot-reload/rust/src/lib.rs b/itest/hot-reload/rust/src/lib.rs index 4b74b7bfc..69d3c9016 100644 --- a/itest/hot-reload/rust/src/lib.rs +++ b/itest/hot-reload/rust/src/lib.rs @@ -11,12 +11,12 @@ struct HotReload; #[gdextension] unsafe impl ExtensionLibrary for HotReload { - fn on_level_init(level: InitLevel) { - println!("[Rust] Init level {level:?}"); + fn on_stage_init(stage: InitStage) { + println!("[Rust] Init stage {stage:?}"); } - fn on_level_deinit(level: InitLevel) { - println!("[Rust] Deinit level {level:?}"); + fn on_stage_deinit(stage: InitStage) { + println!("[Rust] Deinit stage {stage:?}"); } } diff --git a/itest/rust/Cargo.toml b/itest/rust/Cargo.toml index 0694779fb..cd7c79ebc 100644 --- a/itest/rust/Cargo.toml +++ b/itest/rust/Cargo.toml @@ -13,7 +13,7 @@ crate-type = ["cdylib"] # Default feature MUST be empty for workflow reasons, even if it differs from the default feature set in upstream `godot` crate. default = [] codegen-full = ["godot/__codegen-full"] -codegen-full-experimental = ["codegen-full", "godot/experimental-godot-api"] +codegen-full-experimental = ["codegen-full", "godot/experimental-godot-api", "godot/experimental-required-objs"] experimental-threads = ["godot/experimental-threads"] register-docs = ["godot/register-docs"] serde = ["dep:serde", "dep:serde_json", "godot/serde"] diff --git a/itest/rust/build.rs b/itest/rust/build.rs index fd16833bd..d664baaaa 100644 --- a/itest/rust/build.rs +++ b/itest/rust/build.rs @@ -272,6 +272,7 @@ fn main() { rustfmt_if_needed(vec![rust_file]); godot_bindings::emit_godot_version_cfg(); + godot_bindings::emit_safeguard_levels(); // The godot crate has a __codegen-full default feature that enables the godot-codegen/codegen-full feature. When compiling the entire // workspace itest also gets compiled with full codegen due to feature unification. This causes compiler errors since the @@ -319,12 +320,14 @@ fn generate_rust_methods(inputs: &[Input]) -> Vec { .. } = input; - let return_method = format_ident!("return_{}", ident); - let accept_method = format_ident!("accept_{}", ident); - let mirror_method = format_ident!("mirror_{}", ident); - let return_static_method = format_ident!("return_static_{}", ident); - let accept_static_method = format_ident!("accept_static_{}", ident); - let mirror_static_method = format_ident!("mirror_static_{}", ident); + let return_method = format_ident!("return_{ident}"); + let accept_method = format_ident!("accept_{ident}"); + let mirror_method = format_ident!("mirror_{ident}"); + let panic_method = format_ident!("panic_{ident}"); + + let return_static_method = format_ident!("return_static_{ident}"); + let accept_static_method = format_ident!("accept_static_{ident}"); + let mirror_static_method = format_ident!("mirror_static_{ident}"); quote! { #[func] @@ -342,6 +345,11 @@ fn generate_rust_methods(inputs: &[Input]) -> Vec { i } + #[func] + fn #panic_method(&self) -> #rust_ty { + panic!("intentional panic in `{}`", stringify!(#panic_method)); + } + #[func] fn #return_static_method() -> #rust_ty { #rust_val diff --git a/itest/rust/src/benchmarks/mod.rs b/itest/rust/src/benchmarks/mod.rs index 77312090b..f6fb5d00a 100644 --- a/itest/rust/src/benchmarks/mod.rs +++ b/itest/rust/src/benchmarks/mod.rs @@ -13,9 +13,10 @@ use godot::builtin::inner::InnerRect2i; use godot::builtin::{GString, PackedInt32Array, Rect2i, StringName, Vector2i}; use godot::classes::{Node3D, Os, RefCounted}; use godot::obj::{Gd, InstanceId, NewAlloc, NewGd, Singleton}; +use godot::prelude::{varray, Callable, RustCallable, Variant}; use godot::register::GodotClass; -use crate::framework::bench; +use crate::framework::{bench, bench_measure, BenchResult}; mod color; @@ -113,9 +114,38 @@ fn packed_array_from_iter_unknown_size() -> PackedInt32Array { })) } +#[bench(manual)] +fn call_callv_rust_fn() -> BenchResult { + let callable = Callable::from_fn("RustFunction", |_| ()); + + bench_measure(25, || callable.callv(&varray![])) +} + +#[bench(manual)] +fn call_callv_custom() -> BenchResult { + let callable = Callable::from_custom(MyRustCallable {}); + + bench_measure(25, || callable.callv(&varray![])) +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Helpers for benchmarks above #[derive(GodotClass)] #[class(init)] struct MyBenchType {} + +#[derive(PartialEq, Hash)] +struct MyRustCallable {} + +impl RustCallable for MyRustCallable { + fn invoke(&mut self, _args: &[&Variant]) -> Variant { + Variant::nil() + } +} + +impl std::fmt::Display for MyRustCallable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "MyRustCallable") + } +} diff --git a/itest/rust/src/builtin_tests/containers/array_test.rs b/itest/rust/src/builtin_tests/containers/array_test.rs index ea8e3de98..94c55bc8e 100644 --- a/itest/rust/src/builtin_tests/containers/array_test.rs +++ b/itest/rust/src/builtin_tests/containers/array_test.rs @@ -102,7 +102,7 @@ fn array_iter_shared() { fn array_hash() { let array = array![1, 2]; // Just testing that it converts successfully from i64 to u32. - array.hash(); + array.hash_u32(); } #[itest] @@ -554,18 +554,11 @@ fn array_bsearch_by() { } #[itest] -fn array_bsearch_custom() { +fn array_fops_bsearch_custom() { let a = array![5, 4, 2, 1]; let func = backwards_sort_callable(); - assert_eq!(a.bsearch_custom(1, &func), 3); - assert_eq!(a.bsearch_custom(3, &func), 2); -} - -fn backwards_sort_callable() -> Callable { - // No &[&Variant] explicit type in arguments. - Callable::from_fn("sort backwards", |args| { - args[0].to::() > args[1].to::() - }) + assert_eq!(a.functional_ops().bsearch_custom(1, &func), 3); + assert_eq!(a.functional_ops().bsearch_custom(3, &func), 2); } #[itest] @@ -677,6 +670,107 @@ func make_array() -> Array[CustomScriptForArrays]: assert_eq!(script.get_global_name(), "CustomScriptForArrays".into()); } +// Test that proper type has been set&cached while creating new Array. +// https://github.com/godot-rust/gdext/pull/1357 +#[itest] +fn array_inner_type() { + let primary = Array::::new(); + + let secondary = primary.duplicate_shallow(); + assert_eq!(secondary.element_type(), primary.element_type()); + + let secondary = primary.duplicate_deep(); + assert_eq!(secondary.element_type(), primary.element_type()); + + let subarray = primary.subarray_deep(.., None); + assert_eq!(subarray.element_type(), primary.element_type()); + + let subarray = primary.subarray_shallow(.., None); + assert_eq!(subarray.element_type(), primary.element_type()); +} + +#[itest] +fn array_fops_filter() { + let is_even = is_even_callable(); + + let array = array![1, 2, 3, 4, 5, 6]; + assert_eq!(array.functional_ops().filter(&is_even), array![2, 4, 6]); +} + +#[itest] +fn array_fops_map() { + let f = Callable::from_fn("round", |args| args[0].to::().round() as i64); + + let array = array![0.7, 1.0, 1.3, 1.6]; + let result = array.functional_ops().map(&f); + + assert_eq!(result, varray![1, 1, 1, 2]); +} + +#[itest] +fn array_fops_reduce() { + let f = Callable::from_fn("sum", |args| args[0].to::() + args[1].to::()); + + let array = array![1, 2, 3, 4]; + let result = array.functional_ops().reduce(&f, &0.to_variant()); + + assert_eq!(result.to::(), 10); +} + +#[itest] +fn array_fops_any() { + let is_even = is_even_callable(); + + assert!(array![1, 2, 3].functional_ops().any(&is_even)); + assert!(!array![1, 3, 5].functional_ops().any(&is_even)); +} + +#[itest] +fn array_fops_all() { + let is_even = is_even_callable(); + + assert!(!array![1, 2, 3].functional_ops().all(&is_even)); + assert!(array![2, 4, 6].functional_ops().all(&is_even)); +} + +#[itest] +#[cfg(since_api = "4.4")] +fn array_fops_find_custom() { + let is_even = is_even_callable(); + + let array = array![1, 2, 3, 4, 5]; + assert_eq!(array.functional_ops().find_custom(&is_even, None), Some(1)); + + let array = array![1, 3, 5]; + assert_eq!(array.functional_ops().find_custom(&is_even, None), None); +} + +#[itest] +#[cfg(since_api = "4.4")] +fn array_fops_rfind_custom() { + let is_even = is_even_callable(); + + let array = array![1, 2, 3, 4, 5]; + assert_eq!(array.functional_ops().rfind_custom(&is_even, None), Some(3)); + + let array = array![1, 3, 5]; + assert_eq!(array.functional_ops().rfind_custom(&is_even, None), None); +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Helper functions for creating callables. + +fn backwards_sort_callable() -> Callable { + // No &[&Variant] explicit type in arguments. + Callable::from_fn("sort backwards", |args| { + args[0].to::() > args[1].to::() + }) +} + +fn is_even_callable() -> Callable { + Callable::from_fn("is even", |args| args[0].to::() % 2 == 0) +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Class definitions diff --git a/itest/rust/src/builtin_tests/containers/callable_test.rs b/itest/rust/src/builtin_tests/containers/callable_test.rs index 5d3ad1ef2..e2c055a79 100644 --- a/itest/rust/src/builtin_tests/containers/callable_test.rs +++ b/itest/rust/src/builtin_tests/containers/callable_test.rs @@ -73,14 +73,14 @@ fn callable_validity() { fn callable_hash() { let obj = CallableTestObj::new_gd(); assert_eq!( - obj.callable("assign_int").hash(), - obj.callable("assign_int").hash() + obj.callable("assign_int").hash_u32(), + obj.callable("assign_int").hash_u32() ); // Not guaranteed, but unlikely. assert_ne!( - obj.callable("assign_int").hash(), - obj.callable("stringify_int").hash() + obj.callable("assign_int").hash_u32(), + obj.callable("stringify_int").hash_u32() ); } diff --git a/itest/rust/src/builtin_tests/containers/dictionary_test.rs b/itest/rust/src/builtin_tests/containers/dictionary_test.rs index dc0cb4c8c..6bb5f838a 100644 --- a/itest/rust/src/builtin_tests/containers/dictionary_test.rs +++ b/itest/rust/src/builtin_tests/containers/dictionary_test.rs @@ -126,15 +126,22 @@ fn dictionary_hash() { "bar": true, }; - assert_eq!(a.hash(), b.hash(), "equal dictionaries have same hash"); + assert_eq!( + a.hash_u32(), + b.hash_u32(), + "equal dictionaries have same hash" + ); assert_ne!( - a.hash(), - c.hash(), + a.hash_u32(), + c.hash_u32(), "dictionaries with reordered content have different hash" ); // NaNs are not equal (since Godot 4.2) but share same hash. - assert_eq!(vdict! {772: f32::NAN}.hash(), vdict! {772: f32::NAN}.hash()); + assert_eq!( + vdict! {772: f32::NAN}.hash_u32(), + vdict! {772: f32::NAN}.hash_u32() + ); } #[itest] diff --git a/itest/rust/src/builtin_tests/containers/signal_test.rs b/itest/rust/src/builtin_tests/containers/signal_test.rs index 37ccd38b4..6ada19106 100644 --- a/itest/rust/src/builtin_tests/containers/signal_test.rs +++ b/itest/rust/src/builtin_tests/containers/signal_test.rs @@ -15,7 +15,6 @@ use godot::meta::{FromGodot, GodotConvert, ToGodot}; use godot::obj::{Base, Gd, InstanceId, NewAlloc, NewGd}; use godot::prelude::ConvertError; use godot::register::{godot_api, GodotClass}; -use godot::sys; use godot::sys::Global; use crate::framework::itest; diff --git a/itest/rust/src/builtin_tests/containers/variant_test.rs b/itest/rust/src/builtin_tests/containers/variant_test.rs index c73b97e43..18e620157 100644 --- a/itest/rust/src/builtin_tests/containers/variant_test.rs +++ b/itest/rust/src/builtin_tests/containers/variant_test.rs @@ -21,7 +21,7 @@ use godot::obj::{Gd, InstanceId, NewAlloc, NewGd}; use godot::sys::GodotFfi; use crate::common::roundtrip; -use crate::framework::{expect_panic, itest, runs_release}; +use crate::framework::{expect_panic, expect_panic_or_ub, itest, runs_release}; const TEST_BASIS: Basis = Basis::from_rows( Vector3::new(1.0, 2.0, 3.0), @@ -272,7 +272,7 @@ fn variant_dead_object_conversions() { ); // Variant::to(). - expect_panic("Variant::to() with dead object should panic", || { + expect_panic_or_ub("Variant::to() with dead object should panic", || { let _: Gd = variant.to(); }); @@ -361,15 +361,15 @@ fn variant_array_from_untyped_conversions() { ) } -// Same builtin type INT, but incompatible integers (Debug-only). +// Same builtin type INT, but incompatible integers (strict-safeguards only). #[itest] fn variant_array_bad_integer_conversions() { let i32_array: Array = array![1, 2, 160, -40]; let i32_variant = i32_array.to_variant(); let i8_back = i32_variant.try_to::>(); - // In Debug mode, we expect an error upon conversion. - #[cfg(debug_assertions)] + // In strict safeguard mode, we expect an error upon conversion. + #[cfg(safeguards_strict)] { let err = i8_back.expect_err("Array -> Array conversion should fail"); assert_eq!( @@ -378,8 +378,8 @@ fn variant_array_bad_integer_conversions() { ) } - // In Release mode, we expect the conversion to succeed, but a panic to occur on element access. - #[cfg(not(debug_assertions))] + // In balanced/disengaged modes, we expect the conversion to succeed, but a panic to occur on element access. + #[cfg(not(safeguards_strict))] { let i8_array = i8_back.expect("Array -> Array conversion should succeed"); expect_panic("accessing element 160 as i8 should panic", || { @@ -713,13 +713,13 @@ fn variant_hash() { ]; for variant in hash_is_not_0 { - assert_ne!(variant.hash(), 0) + assert_ne!(variant.hash_u32(), 0) } for variant in self_equal { - assert_eq!(variant.hash(), variant.hash()) + assert_eq!(variant.hash_u32(), variant.hash_u32()) } - assert_eq!(Variant::nil().hash(), 0); + assert_eq!(Variant::nil().hash_u32(), 0); // It's not guaranteed that different object will have different hash, but it is // extremely unlikely for a collision to happen. diff --git a/itest/rust/src/common.rs b/itest/rust/src/common.rs index eb211efe2..ebfb1061e 100644 --- a/itest/rust/src/common.rs +++ b/itest/rust/src/common.rs @@ -21,13 +21,3 @@ where assert_eq!(value, back); } - -/// Signal to the compiler that a value is used (to avoid optimization). -pub fn bench_used(value: T) { - // The following check would be used to prevent `()` arguments, ensuring that a value from the bench is actually going into the blackbox. - // However, we run into this issue, despite no array being used: https://github.com/rust-lang/rust/issues/43408. - // error[E0401]: can't use generic parameters from outer function - // sys::static_assert!(std::mem::size_of::() != 0, "returned unit value in benchmark; make sure to use a real value"); - - std::hint::black_box(value); -} diff --git a/itest/rust/src/engine_tests/async_test.rs b/itest/rust/src/engine_tests/async_test.rs index cfbb201f1..e2b80b50f 100644 --- a/itest/rust/src/engine_tests/async_test.rs +++ b/itest/rust/src/engine_tests/async_test.rs @@ -11,7 +11,6 @@ use godot::builtin::{array, vslice, Array, Callable, Signal}; use godot::classes::{Object, RefCounted}; use godot::obj::{Base, Gd, NewAlloc, NewGd}; use godot::prelude::{godot_api, GodotClass}; -use godot::sys; use godot::task::{self, create_test_signal_future_resolver, SignalFuture, TaskHandle}; use crate::framework::{expect_async_panic, itest, TestContext}; @@ -132,6 +131,8 @@ fn async_task_signal_future_panic() -> TaskHandle { #[cfg(feature = "experimental-threads")] #[itest(async)] fn signal_future_non_send_arg_panic() -> TaskHandle { + use godot::sys; + use crate::framework::ThreadCrosser; let mut object = RefCounted::new_gd(); diff --git a/itest/rust/src/engine_tests/autoload_test.rs b/itest/rust/src/engine_tests/autoload_test.rs new file mode 100644 index 000000000..cc3a7c7c2 --- /dev/null +++ b/itest/rust/src/engine_tests/autoload_test.rs @@ -0,0 +1,92 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * 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::classes::Node; +use godot::prelude::*; +use godot::tools::{get_autoload_by_name, try_get_autoload_by_name}; + +use crate::framework::{itest, quick_thread}; + +#[derive(GodotClass)] +#[class(init, base=Node)] +struct AutoloadClass { + base: Base, + #[var] + property: i32, +} + +#[godot_api] +impl AutoloadClass { + #[func] + fn verify_works(&self) -> i32 { + 787 + } +} + +#[itest] +fn autoload_get() { + let mut autoload = get_autoload_by_name::("MyAutoload"); + { + let mut guard = autoload.bind_mut(); + assert_eq!(guard.verify_works(), 787); + assert_eq!(guard.property, 0, "still has default value"); + + guard.property = 42; + } + + // Fetch same autoload anew. + let autoload2 = get_autoload_by_name::("MyAutoload"); + assert_eq!(autoload2.bind().property, 42); + + // Reset for other tests. + autoload.bind_mut().property = 0; +} + +#[itest] +fn autoload_try_get_named() { + let autoload = try_get_autoload_by_name::("MyAutoload").expect("fetch autoload"); + + assert_eq!(autoload.bind().verify_works(), 787); + assert_eq!(autoload.bind().property, 0, "still has default value"); +} + +#[itest] +fn autoload_try_get_named_inexistent() { + let result = try_get_autoload_by_name::("InexistentAutoload"); + result.expect_err("non-existent autoload"); +} + +#[itest] +fn autoload_try_get_named_bad_type() { + let result = try_get_autoload_by_name::("MyAutoload"); + result.expect_err("autoload of incompatible node type"); +} + +#[itest] +fn autoload_from_other_thread() { + use std::sync::{Arc, Mutex}; + + // We can't return the Result from the thread because Gd is not Send, so we extract the error message instead. + let outer_error = Arc::new(Mutex::new(String::new())); + let inner_error = Arc::clone(&outer_error); + + quick_thread(move || { + let result = try_get_autoload_by_name::("MyAutoload"); + match result { + Ok(_) => panic!("autoload access from non-main thread should fail"), + Err(err) => { + *inner_error.lock().unwrap() = err.to_string(); + } + } + }); + + let msg = outer_error.lock().unwrap(); + assert_eq!( + *msg, + "Autoloads must be fetched from main thread, as Gd is not thread-safe" + ); +} diff --git a/itest/rust/src/engine_tests/mod.rs b/itest/rust/src/engine_tests/mod.rs index 7a7c5503d..b5edaefd7 100644 --- a/itest/rust/src/engine_tests/mod.rs +++ b/itest/rust/src/engine_tests/mod.rs @@ -6,6 +6,7 @@ */ mod async_test; +mod autoload_test; mod codegen_enums_test; mod codegen_test; mod engine_enum_test; diff --git a/itest/rust/src/engine_tests/native_structures_test.rs b/itest/rust/src/engine_tests/native_structures_test.rs index cf2e36e71..10aec961b 100644 --- a/itest/rust/src/engine_tests/native_structures_test.rs +++ b/itest/rust/src/engine_tests/native_structures_test.rs @@ -26,6 +26,7 @@ pub fn sample_glyph(start: i32) -> Glyph { font_rid: Rid::new(1024), font_size: 1025, index: 1026, + span_index: -1, } } @@ -48,6 +49,7 @@ fn native_structure_codegen() { font_rid: Rid::new(0), font_size: 0, index: 0, + span_index: -1, }; } diff --git a/itest/rust/src/engine_tests/node_test.rs b/itest/rust/src/engine_tests/node_test.rs index badabfe6c..d4fc4f721 100644 --- a/itest/rust/src/engine_tests/node_test.rs +++ b/itest/rust/src/engine_tests/node_test.rs @@ -74,3 +74,27 @@ fn node_call_group(ctx: &TestContext) { tree.call_group("group", "remove_meta", vslice!["something"]); assert!(!node.has_meta("something")); } + +// Experimental required parameter/return value. +/* TODO(v0.5): enable once https://github.com/godot-rust/gdext/pull/1383 is merged. +#[cfg(all(feature = "codegen-full-experimental", since_api = "4.6"))] +#[itest] +fn node_required_param_return() { + use godot::classes::Tween; + use godot::obj::Gd; + + let mut parent = Node::new_alloc(); + let child = Node::new_alloc(); + + // add_child() takes required arg, so this still works. + // (Test for Option *not* working anymore is in godot > no_compile_tests.) + parent.add_child(&child); + + // create_tween() returns now non-null instance. + let tween: Gd = parent.create_tween(); + assert!(tween.is_instance_valid()); + assert!(tween.to_string().contains("Tween")); + + parent.free(); +} +*/ diff --git a/itest/rust/src/framework/bencher.rs b/itest/rust/src/framework/bencher.rs index 49e428e04..4e9c6fac4 100644 --- a/itest/rust/src/framework/bencher.rs +++ b/itest/rust/src/framework/bencher.rs @@ -17,13 +17,16 @@ // Instead, we focus on min (fastest run) and median -- even median may vary quite a bit between runs; but it gives an idea of the distribution. // See also https://easyperf.net/blog/2019/12/30/Comparing-performance-measurements#average-median-minimum. +use std::any::TypeId; use std::time::{Duration, Instant}; const WARMUP_RUNS: usize = 200; const TEST_RUNS: usize = 501; // uneven, so median need not be interpolated. const METRIC_COUNT: usize = 2; -pub struct BenchResult { +pub type BenchResult = Result; + +pub struct BenchMeasurement { pub stats: [Duration; METRIC_COUNT], } @@ -31,25 +34,42 @@ pub fn metrics() -> [&'static str; METRIC_COUNT] { ["min", "median"] } -pub fn run_benchmark(code: fn(), inner_repetitions: usize) -> BenchResult { +/// Measures the timing of the passed closure (repeated `inner_repetitions` times). +/// +/// Used by both `#[bench]` automatic mode (generated by macro) and `#[bench(manual)]` (called explicitly). +/// +/// Returns `Err(String)` if there is something wrong with the benchmark setup. +pub fn bench_measure(inner_repetitions: usize, work: impl Fn() -> R) -> BenchResult { + // Runtime check: ensure the closure doesn't return (). + if TypeId::of::() == TypeId::of::<()>() { + return Err("bench_measure(): closure must return non-unit type to prevent the computation from being optimized away".to_string()); + } + + // Warmup phase. for _ in 0..WARMUP_RUNS { - code(); + for _ in 0..inner_repetitions { + let _ = work(); + } } + // Measurement phase. let mut times = Vec::with_capacity(TEST_RUNS); for _ in 0..TEST_RUNS { let start = Instant::now(); - code(); + for _ in 0..inner_repetitions { + let fruits_of_labor = work(); + bench_used(fruits_of_labor); + } let duration = start.elapsed(); times.push(duration / inner_repetitions as u32); } times.sort(); - calculate_stats(times) + Ok(calculate_stats(times)) } -fn calculate_stats(times: Vec) -> BenchResult { +fn calculate_stats(times: Vec) -> BenchMeasurement { // See top of file for rationale. /*let mean = { @@ -72,7 +92,17 @@ fn calculate_stats(times: Vec) -> BenchResult { let min = times[0]; let median = times[TEST_RUNS / 2]; - BenchResult { + BenchMeasurement { stats: [min, median], } } + +/// Signal to the compiler that a value is used (to avoid optimization). +fn bench_used(value: T) { + // The following check would be used to prevent `()` arguments, ensuring that a value from the bench is actually going into the blackbox. + // However, we run into this issue, despite no array being used: https://github.com/rust-lang/rust/issues/43408. + // error[E0401]: can't use generic parameters from outer function + // sys::static_assert!(std::mem::size_of::() != 0, "returned unit value in benchmark; make sure to use a real value"); + + std::hint::black_box(value); +} diff --git a/itest/rust/src/framework/mod.rs b/itest/rust/src/framework/mod.rs index e5ced4764..52684eeb3 100644 --- a/itest/rust/src/framework/mod.rs +++ b/itest/rust/src/framework/mod.rs @@ -158,8 +158,7 @@ pub struct RustBenchmark { pub file: &'static str, #[allow(dead_code)] pub line: u32, - pub function: fn(), - pub repetitions: usize, + pub function: fn() -> BenchResult, } pub fn passes_filter(filters: &[String], test_name: &str) -> bool { @@ -200,6 +199,14 @@ pub fn expect_panic(context: &str, code: impl FnOnce()) { ); } +/// Run for code that panics in *strict* and *balanced* safeguard levels, but cause UB in *disengaged* level. +/// +/// The code is not executed for the latter. +pub fn expect_panic_or_ub(_context: &str, _code: impl FnOnce()) { + #[cfg(safeguards_balanced)] + expect_panic(_context, _code); +} + pin_project_lite::pin_project! { pub struct ExpectPanicFuture { context: &'static str, @@ -313,6 +320,14 @@ pub fn runs_release() -> bool { !Os::singleton().is_debug_build() } +/// Whether we are running in GitHub Actions CI. +/// +/// Must not be used to influence test logic. Only for logging and diagnostics. +#[allow(dead_code)] +pub fn runs_github_ci() -> bool { + std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true") +} + /// Create a `GDScript` script from code, compiles and returns it. pub fn create_gdscript(code: &str) -> Gd { let mut script = GDScript::new_gd(); diff --git a/itest/rust/src/framework/runner.rs b/itest/rust/src/framework/runner.rs index 7c4e894ff..54f07857e 100644 --- a/itest/rust/src/framework/runner.rs +++ b/itest/rust/src/framework/runner.rs @@ -376,7 +376,7 @@ impl IntegrationTests { print_bench_pre(bench.name, bench.file, last_file.as_deref()); last_file = Some(bench.file.to_string()); - let result = bencher::run_benchmark(bench.function, bench.repetitions); + let result = (bench.function)(); print_bench_post(result); } } @@ -546,9 +546,17 @@ fn print_bench_pre(benchmark: &str, bench_file: &str, last_file: Option<&str>) { } fn print_bench_post(result: BenchResult) { - for stat in result.stats.iter() { - print!(" {:>10.3}μs", stat.as_nanos() as f64 / 1000.0); + match result { + Ok(measured) => { + for stat in measured.stats.iter() { + print!(" {:>10.3}μs", stat.as_nanos() as f64 / 1000.0); + } + } + Err(msg) => { + print!(" {FMT_RED}ERROR: {msg}{FMT_END}"); + } } + println!(); } diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index 9ed9c0460..20aab16af 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -5,7 +5,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use godot::init::{gdextension, ExtensionLibrary, InitLevel}; +use godot::init::{gdextension, ExtensionLibrary, InitLevel, InitStage}; mod benchmarks; mod builtin_tests; @@ -24,7 +24,16 @@ unsafe impl ExtensionLibrary for framework::IntegrationTests { InitLevel::Core } - fn on_level_init(level: InitLevel) { - object_tests::on_level_init(level); + fn on_stage_init(stage: InitStage) { + object_tests::on_stage_init(stage); + } + + fn on_stage_deinit(stage: InitStage) { + object_tests::on_stage_deinit(stage); + } + + #[cfg(since_api = "4.5")] + fn on_main_loop_frame() { + object_tests::on_main_loop_frame(); } } diff --git a/itest/rust/src/object_tests/base_init_test.rs b/itest/rust/src/object_tests/base_init_test.rs index d78e58ade..43a7d9497 100644 --- a/itest/rust/src/object_tests/base_init_test.rs +++ b/itest/rust/src/object_tests/base_init_test.rs @@ -10,7 +10,7 @@ use godot::builtin::Vector2; use godot::classes::notify::ObjectNotification; use godot::classes::{ClassDb, IRefCounted, RefCounted}; use godot::meta::ToGodot; -use godot::obj::{Base, Gd, InstanceId, NewAlloc, NewGd, Singleton, WithBaseField}; +use godot::obj::{Base, Gd, InstanceId, NewGd, Singleton, WithBaseField}; use godot::register::{godot_api, GodotClass}; use godot::task::TaskHandle; @@ -65,13 +65,14 @@ fn base_init_extracted_gd() { // Checks bad practice of rug-pulling the base pointer. #[itest] +#[cfg(safeguards_balanced)] fn base_init_freed_gd() { let mut free_executed = false; expect_panic("base object is destroyed", || { let _obj = Gd::::from_init_fn(|base| { let obj = base.to_init_gd(); - obj.free(); // Causes the problem, but doesn't panic yet. + obj.free(); // Causes the problem, but doesn't panic yet. UB in safeguards-disengaged. free_executed = true; Based { base, i: 456 } @@ -144,27 +145,28 @@ fn verify_complex_init((obj, base): (Gd, Gd)) -> Instance id } -#[cfg(debug_assertions)] +#[cfg(safeguards_strict)] #[itest] fn base_init_outside_init() { + use godot::obj::NewAlloc; let mut obj = Based::new_alloc(); expect_panic("to_init_gd() outside init() function", || { let guard = obj.bind_mut(); - let _gd = guard.base.to_init_gd(); // Panics in Debug builds. + let _gd = guard.base.to_init_gd(); // Panics in strict safeguard mode. }); obj.free(); } -#[cfg(debug_assertions)] +#[cfg(safeguards_strict)] #[itest] fn base_init_to_gd() { expect_panic("WithBaseField::to_gd() inside init() function", || { let _obj = Gd::::from_init_fn(|base| { let temp_obj = Based { base, i: 999 }; - // Call to self.to_gd() during initialization should panic in Debug builds. + // Call to self.to_gd() during initialization should panic in strict safeguard mode. let _gd = godot::obj::WithBaseField::to_gd(&temp_obj); temp_obj diff --git a/itest/rust/src/object_tests/base_test.rs b/itest/rust/src/object_tests/base_test.rs index 7278cc32e..52ca2b2bc 100644 --- a/itest/rust/src/object_tests/base_test.rs +++ b/itest/rust/src/object_tests/base_test.rs @@ -7,7 +7,7 @@ use godot::prelude::*; -use crate::framework::{expect_panic, itest}; +use crate::framework::{expect_panic_or_ub, itest}; #[itest(skip)] fn base_test_is_weak() { @@ -82,6 +82,8 @@ fn base_gd_self() { } // Hardening against https://github.com/godot-rust/gdext/issues/711. +// Furthermore, this now keeps expect_panic_or_ub() instead of expect_panic(), although it's questionable if much remains with all these checks +// disabled. There is still some logic remaining, and an alternative would be to #[cfg(safeguards_balanced)] this. #[itest] fn base_smuggling() { let (mut obj, extracted_base) = create_object_with_extracted_base(); @@ -98,19 +100,19 @@ fn base_smuggling() { extracted_base_obj.free(); // Access to object should now fail. - expect_panic("object with dead base: calling base methods", || { + expect_panic_or_ub("object with dead base: calling base methods", || { obj.get_position(); }); - expect_panic("object with dead base: bind()", || { + expect_panic_or_ub("object with dead base: bind()", || { obj.bind(); }); - expect_panic("object with dead base: instance_id()", || { + expect_panic_or_ub("object with dead base: instance_id()", || { obj.instance_id(); }); - expect_panic("object with dead base: clone()", || { + expect_panic_or_ub("object with dead base: clone()", || { let _ = obj.clone(); }); - expect_panic("object with dead base: upcast()", || { + expect_panic_or_ub("object with dead base: upcast()", || { obj.upcast::(); }); @@ -118,7 +120,7 @@ fn base_smuggling() { let (obj, extracted_base) = create_object_with_extracted_base(); obj.free(); - expect_panic("accessing extracted base of dead object", || { + expect_panic_or_ub("accessing extracted base of dead object", || { extracted_base.__constructed_gd().get_position(); }); } diff --git a/itest/rust/src/object_tests/init_level_test.rs b/itest/rust/src/object_tests/init_level_test.rs deleted file mode 100644 index fecb05308..000000000 --- a/itest/rust/src/object_tests/init_level_test.rs +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (c) godot-rust; Bromeon and contributors. - * 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::sync::atomic::{AtomicBool, Ordering}; - -use godot::init::InitLevel; -use godot::obj::{NewAlloc, Singleton}; -use godot::register::{godot_api, GodotClass}; -use godot::sys::Global; - -use crate::framework::{expect_panic, itest, runs_release, suppress_godot_print}; - -static OBJECT_CALL_HAS_RUN: AtomicBool = AtomicBool::new(false); -static LEVELS_SEEN: Global> = Global::default(); - -#[derive(GodotClass)] -#[class(base = Object, init)] -struct SomeObject {} - -#[godot_api] -impl SomeObject { - #[func] - pub fn set_has_run_true(&self) { - OBJECT_CALL_HAS_RUN.store(true, Ordering::Release); - } - - pub fn test() { - assert!(!OBJECT_CALL_HAS_RUN.load(Ordering::Acquire)); - let mut some_object = SomeObject::new_alloc(); - // Need to go through Godot here as otherwise we bypass the failure. - some_object.call("set_has_run_true", &[]); - some_object.free(); - } -} - -// Ensure that the above function has actually run and succeeded. -#[itest] -fn init_level_all_initialized() { - assert!( - OBJECT_CALL_HAS_RUN.load(Ordering::Relaxed), - "Object call function did not run during Core init level" - ); -} - -// Ensure that we saw all the init levels expected. -#[itest] -fn init_level_observed_all() { - let levels_seen = LEVELS_SEEN.lock().clone(); - - assert_eq!(levels_seen[0], InitLevel::Core); - assert_eq!(levels_seen[1], InitLevel::Servers); - assert_eq!(levels_seen[2], InitLevel::Scene); - - // In Debug/Editor builds, Editor level is loaded; otherwise not. - let level_3 = levels_seen.get(3); - if runs_release() { - assert_eq!(level_3, None); - } else { - assert_eq!(level_3, Some(&InitLevel::Editor)); - } -} - -// ---------------------------------------------------------------------------------------------------------------------------------------------- -// Level-specific callbacks - -pub fn on_level_init(level: InitLevel) { - LEVELS_SEEN.lock().push(level); - - match level { - InitLevel::Core => on_init_core(), - InitLevel::Servers => on_init_servers(), - InitLevel::Scene => on_init_scene(), - InitLevel::Editor => on_init_editor(), - } -} - -// Runs during core init level to ensure we can access core singletons. -fn on_init_core() { - // Ensure we can create and use an Object-derived class during Core init level. - SomeObject::test(); - - // Check the early core singletons we can access here. - #[cfg(feature = "codegen-full")] - { - let project_settings = godot::classes::ProjectSettings::singleton(); - assert_eq!( - project_settings.get("application/config/name").get_type(), - godot::builtin::VariantType::STRING - ); - } - - let engine = godot::classes::Engine::singleton(); - assert!(engine.get_physics_ticks_per_second() > 0); - - let os = godot::classes::Os::singleton(); - assert!(!os.get_name().is_empty()); - - let time = godot::classes::Time::singleton(); - assert!(time.get_ticks_usec() <= time.get_ticks_usec()); -} - -fn on_init_servers() { - // Nothing yet. -} - -fn on_init_scene() { - // Known limitation that singletons only become available later: - // https://github.com/godotengine/godot-cpp/issues/1180#issuecomment-3074351805 - suppress_godot_print(|| { - expect_panic("Singletons not loaded during Scene init level", || { - let _ = godot::classes::RenderingServer::singleton(); - }); - }); -} - -pub fn on_init_editor() { - // Nothing yet. -} diff --git a/itest/rust/src/object_tests/init_stage_test.rs b/itest/rust/src/object_tests/init_stage_test.rs new file mode 100644 index 000000000..f919a74f4 --- /dev/null +++ b/itest/rust/src/object_tests/init_stage_test.rs @@ -0,0 +1,187 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * 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::builtin::{Rid, Variant}; +use godot::classes::{Engine, IObject, RenderingServer}; +use godot::init::InitStage; +use godot::obj::{Base, GodotClass, NewAlloc, Singleton}; +use godot::register::{godot_api, GodotClass}; +use godot::sys::Global; + +use crate::framework::{expect_panic, itest, runs_release, suppress_godot_print}; + +static STAGES_SEEN: Global> = Global::default(); + +#[derive(GodotClass)] +#[class(base = Object, init)] +struct SomeObject {} + +#[godot_api] +impl SomeObject { + #[func] + pub fn method(&self) -> i32 { + 356 + } + + pub fn test() { + let mut some_object = SomeObject::new_alloc(); + // Need to go through Godot here as otherwise we bypass the failure. + let result = some_object.call("method", &[]); + assert_eq!(result, Variant::from(356)); + + some_object.free(); + } +} + +// Ensure that we saw all the init levels expected. +#[itest] +fn init_level_observed_all() { + let actual_stages = STAGES_SEEN.lock().clone(); + + let mut expected_stages = vec![InitStage::Core, InitStage::Servers, InitStage::Scene]; + + // In Debug/Editor builds, Editor level is loaded; otherwise not. + if !runs_release() { + expected_stages.push(InitStage::Editor); + } + + // From Godot 4.5, MainLoop level is added. + #[cfg(since_api = "4.5")] + expected_stages.push(InitStage::MainLoop); + + assert_eq!(actual_stages, expected_stages); +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Stage-specific callbacks + +pub fn on_stage_init(stage: InitStage) { + STAGES_SEEN.lock().push(stage); + + match stage { + InitStage::Core => on_init_core(), + InitStage::Servers => on_init_servers(), + InitStage::Scene => on_init_scene(), + InitStage::Editor => on_init_editor(), + #[cfg(since_api = "4.5")] + InitStage::MainLoop => on_init_main_loop(), + _ => { /* Needed due to #[non_exhaustive] */ } + } +} + +// Runs during core init level to ensure we can access core singletons. +fn on_init_core() { + // Ensure we can create and use an Object-derived class during Core init level. + SomeObject::test(); + + // Check the early core singletons we can access here. + #[cfg(feature = "codegen-full")] + { + let project_settings = godot::classes::ProjectSettings::singleton(); + assert_eq!( + project_settings.get("application/config/name").get_type(), + godot::builtin::VariantType::STRING + ); + } + + let engine = godot::classes::Engine::singleton(); + assert!(engine.get_physics_ticks_per_second() > 0); + + let os = godot::classes::Os::singleton(); + assert!(!os.get_name().is_empty()); + + let time = godot::classes::Time::singleton(); + assert!(time.get_ticks_usec() <= time.get_ticks_usec()); +} + +fn on_init_servers() { + // Nothing yet. +} + +fn on_init_scene() { + // Known limitation that singletons only become available later: + // https://github.com/godotengine/godot-cpp/issues/1180#issuecomment-3074351805 + suppress_godot_print(|| { + expect_panic("Singletons not loaded during Scene init level", || { + let _ = godot::classes::RenderingServer::singleton(); + }); + }); +} + +pub fn on_init_editor() { + // Nothing yet. +} + +#[derive(GodotClass)] +#[class(base=Object)] +struct MainLoopCallbackSingleton { + tex: Rid, +} + +#[godot_api] +impl IObject for MainLoopCallbackSingleton { + fn init(_: Base) -> Self { + Self { + tex: RenderingServer::singleton().texture_2d_placeholder_create(), + } + } +} + +#[cfg(since_api = "4.5")] +fn on_init_main_loop() { + // RenderingServer should be accessible in MainLoop init and deinit. + let singleton = MainLoopCallbackSingleton::new_alloc(); + assert!(singleton.bind().tex.is_valid()); + Engine::singleton().register_singleton( + &MainLoopCallbackSingleton::class_id().to_string_name(), + &singleton, + ); +} + +#[cfg(not(since_api = "4.5"))] +fn on_init_main_loop() { + // Nothing on older API versions. +} + +pub fn on_stage_deinit(stage: InitStage) { + match stage { + #[cfg(since_api = "4.5")] + InitStage::MainLoop => on_deinit_main_loop(), + _ => { + // Nothing for other stages yet. + } + } +} + +#[cfg(since_api = "4.5")] +fn on_deinit_main_loop() { + let singleton = Engine::singleton() + .get_singleton(&MainLoopCallbackSingleton::class_id().to_string_name()) + .unwrap() + .cast::(); + Engine::singleton() + .unregister_singleton(&MainLoopCallbackSingleton::class_id().to_string_name()); + let tex = singleton.bind().tex; + assert!(tex.is_valid()); + RenderingServer::singleton().free_rid(tex); + singleton.free(); +} + +#[cfg(not(since_api = "4.5"))] +fn on_deinit_main_loop() { + // Nothing on older API versions. +} + +#[cfg(since_api = "4.5")] +pub fn on_main_loop_frame() { + // Nothing yet. +} + +#[cfg(not(since_api = "4.5"))] +pub fn on_main_loop_frame() { + // Nothing on older API versions. +} diff --git a/itest/rust/src/object_tests/mod.rs b/itest/rust/src/object_tests/mod.rs index 01270a86c..319d8a8dd 100644 --- a/itest/rust/src/object_tests/mod.rs +++ b/itest/rust/src/object_tests/mod.rs @@ -15,7 +15,7 @@ mod enum_test; // `get_property_list` is only supported in Godot 4.3+ #[cfg(since_api = "4.3")] mod get_property_list_test; -mod init_level_test; +mod init_stage_test; mod object_arg_test; mod object_swap_test; mod object_test; @@ -33,4 +33,4 @@ mod virtual_methods_niche_test; mod virtual_methods_test; // Need to test this in the init level method. -pub use init_level_test::*; +pub use init_stage_test::*; diff --git a/itest/rust/src/object_tests/object_swap_test.rs b/itest/rust/src/object_tests/object_swap_test.rs index b1cea92ea..d636bd01e 100644 --- a/itest/rust/src/object_tests/object_swap_test.rs +++ b/itest/rust/src/object_tests/object_swap_test.rs @@ -10,8 +10,8 @@ // A lot these tests also exist in the `object_test` module, where they test object lifetime rather than type swapping. // TODO consolidate them, so that it's less likely to forget edge cases. -// Disabled in Release mode, since we don't perform the subtype check there. -#![cfg(debug_assertions)] +// Disabled in balanced/disengaged levels, since we don't perform the subtype check there. +#![cfg(safeguards_strict)] use godot::builtin::GString; use godot::classes::{Node, Node3D, Object}; diff --git a/itest/rust/src/object_tests/object_test.rs b/itest/rust/src/object_tests/object_test.rs index 6fd31ae30..07a24afde 100644 --- a/itest/rust/src/object_tests/object_test.rs +++ b/itest/rust/src/object_tests/object_test.rs @@ -11,18 +11,17 @@ use std::cell::{Cell, RefCell}; use std::rc::Rc; -use godot::builtin::{GString, StringName, Variant, Vector3}; +use godot::builtin::{Array, GString, StringName, Variant, Vector3}; use godot::classes::{ file_access, Engine, FileAccess, IRefCounted, Node, Node2D, Node3D, Object, RefCounted, }; use godot::global::godot_str; -#[allow(deprecated)] use godot::meta::{FromGodot, GodotType, ToGodot}; use godot::obj::{Base, Gd, Inherits, InstanceId, NewAlloc, NewGd, RawGd, Singleton}; use godot::register::{godot_api, GodotClass}; use godot::sys::{self, interface_fn, GodotFfi}; -use crate::framework::{expect_panic, itest, TestContext}; +use crate::framework::{expect_panic, expect_panic_or_ub, itest, TestContext}; // TODO: // * make sure that ptrcalls are used when possible (i.e. when type info available; maybe GDScript integration test) @@ -171,7 +170,7 @@ fn object_instance_id_when_freed() { node.clone().free(); // destroys object without moving out of reference assert!(!node.is_instance_valid()); - expect_panic("instance_id() on dead object", move || { + expect_panic_or_ub("instance_id() on dead object", move || { node.instance_id(); }); } @@ -235,7 +234,7 @@ fn object_user_bind_after_free() { let copy = obj.clone(); obj.free(); - expect_panic("bind() on dead user object", move || { + expect_panic_or_ub("bind() on dead user object", move || { let _ = copy.bind(); }); } @@ -247,7 +246,7 @@ fn object_user_free_during_bind() { let copy = obj.clone(); // TODO clone allowed while bound? - expect_panic("direct free() on user while it's bound", move || { + expect_panic_or_ub("direct free() on user while it's bound", move || { copy.free(); }); @@ -275,7 +274,7 @@ fn object_engine_freed_argument_passing(ctx: &TestContext) { // Destroy object and then pass it to a Godot engine API. node.free(); - expect_panic("pass freed Gd to Godot engine API (T=Node)", || { + expect_panic_or_ub("pass freed Gd to Godot engine API (T=Node)", || { tree.add_child(&node2); }); } @@ -288,10 +287,10 @@ fn object_user_freed_casts() { // Destroy object and then pass it to a Godot engine API (upcast itself works, see other tests). obj.free(); - expect_panic("Gd::upcast() on dead object (T=user)", || { + expect_panic_or_ub("Gd::upcast() on dead object (T=user)", || { let _ = obj2.upcast::(); }); - expect_panic("Gd::cast() on dead object (T=user)", || { + expect_panic_or_ub("Gd::cast() on dead object (T=user)", || { let _ = base_obj.cast::(); }); } @@ -306,7 +305,7 @@ fn object_user_freed_argument_passing() { // Destroy object and then pass it to a Godot engine API (upcast itself works, see other tests). obj.free(); - expect_panic("pass freed Gd to Godot engine API (T=user)", || { + expect_panic_or_ub("pass freed Gd to Godot engine API (T=user)", || { engine.register_singleton("NeverRegistered", &obj2); }); } @@ -344,7 +343,7 @@ fn object_user_call_after_free() { let mut copy = obj.clone(); obj.free(); - expect_panic("call() on dead user object", move || { + expect_panic_or_ub("call() on dead user object", move || { let _ = copy.call("get_instance_id", &[]); }); } @@ -355,7 +354,7 @@ fn object_engine_use_after_free() { let copy = node.clone(); node.free(); - expect_panic("call method on dead engine object", move || { + expect_panic_or_ub("call method on dead engine object", move || { copy.get_position(); }); } @@ -366,7 +365,7 @@ fn object_engine_use_after_free_varcall() { let mut copy = node.clone(); node.free(); - expect_panic("call method on dead engine object", move || { + expect_panic_or_ub("call method on dead engine object", move || { copy.call_deferred("get_position", &[]); }); } @@ -411,13 +410,13 @@ fn object_dead_eq() { { let lhs = a.clone(); - expect_panic("Gd::eq() panics when one operand is dead", move || { + expect_panic_or_ub("Gd::eq() panics when one operand is dead", move || { let _ = lhs == b; }); } { let rhs = a.clone(); - expect_panic("Gd::ne() panics when one operand is dead", move || { + expect_panic_or_ub("Gd::ne() panics when one operand is dead", move || { let _ = b2 != rhs; }); } @@ -826,7 +825,7 @@ fn object_engine_manual_double_free() { let node2 = node.clone(); node.free(); - expect_panic("double free()", move || { + expect_panic_or_ub("double free()", move || { node2.free(); }); } @@ -845,7 +844,7 @@ fn object_user_double_free() { let obj2 = obj.clone(); obj.call("free", &[]); - expect_panic("double free()", move || { + expect_panic_or_ub("double free()", move || { obj2.free(); }); } @@ -878,6 +877,10 @@ fn object_get_scene_tree(ctx: &TestContext) { let count = tree.get_child_count(); assert_eq!(count, 1); + + // Explicit type as regression test: https://github.com/godot-rust/gdext/pull/1385 + let nodes: Array> = tree.get_children(); + assert_eq!(nodes.len(), 1); } // implicitly tested: node does not leak #[itest] diff --git a/itest/rust/src/object_tests/property_test.rs b/itest/rust/src/object_tests/property_test.rs index 1ef54c87a..e13b18848 100644 --- a/itest/rust/src/object_tests/property_test.rs +++ b/itest/rust/src/object_tests/property_test.rs @@ -53,6 +53,9 @@ struct HasProperty { #[var] packed_int_array: PackedInt32Array, + + #[var(rename = renamed_variable)] + unused_name: GString, } #[godot_api] @@ -147,10 +150,48 @@ impl INode for HasProperty { texture_val: OnEditor::default(), texture_val_rw: None, packed_int_array: PackedInt32Array::new(), + unused_name: GString::new(), } } } +#[itest] +fn test_renamed_var() { + let mut obj = HasProperty::new_alloc(); + + let prop_list = obj.get_property_list(); + assert!(prop_list + .iter_shared() + .any(|d| d.get("name") == Some("renamed_variable".to_variant()))); + assert!(!prop_list + .iter_shared() + .any(|d| d.get("name") == Some("unused_name".to_variant()))); + + assert_eq!(obj.get("renamed_variable"), GString::new().to_variant()); + assert_eq!(obj.get("unused_name"), Variant::nil()); + + let new_value = "variable changed".to_variant(); + obj.set("renamed_variable", &new_value); + obj.set("unused_name", &"something different".to_variant()); + assert_eq!(obj.get("renamed_variable"), new_value); + assert_eq!(obj.get("unused_name"), Variant::nil()); + + obj.free(); +} + +#[itest] +fn test_renamed_var_getter_setter() { + let obj = HasProperty::new_alloc(); + + assert!(obj.has_method("get_renamed_variable")); + assert!(obj.has_method("set_renamed_variable")); + assert!(!obj.has_method("get_unused_name")); + assert!(!obj.has_method("get_unused_name")); + assert_eq!(obj.bind().get_renamed_variable(), GString::new()); + + obj.free(); +} + #[derive(Default, Copy, Clone)] #[repr(i64)] enum SomeCStyleEnum { diff --git a/itest/rust/src/object_tests/virtual_methods_test.rs b/itest/rust/src/object_tests/virtual_methods_test.rs index 27ea6e5f1..17fd5e610 100644 --- a/itest/rust/src/object_tests/virtual_methods_test.rs +++ b/itest/rust/src/object_tests/virtual_methods_test.rs @@ -738,7 +738,7 @@ impl GetSetTest { // There isn't a good way to test editor plugins, but we can at least declare one to ensure that the macro // compiles. #[derive(GodotClass)] -#[class(no_init, base = EditorPlugin, tool)] +#[class(init, base = EditorPlugin, tool)] struct CustomEditorPlugin; // Just override EditorPlugin::edit() to verify method is declared with Option. diff --git a/itest/rust/src/register_tests/constant_test.rs b/itest/rust/src/register_tests/constant_test.rs index ae8806403..9ab6f0066 100644 --- a/itest/rust/src/register_tests/constant_test.rs +++ b/itest/rust/src/register_tests/constant_test.rs @@ -172,8 +172,6 @@ godot::sys::plugin_add!( godot::private::ClassPlugin::new::( godot::private::PluginItem::InherentImpl( godot::private::InherentImpl::new::( - #[cfg(feature = "register-docs")] - Default::default() ) ) ) diff --git a/itest/rust/src/register_tests/register_docs_test.rs b/itest/rust/src/register_tests/register_docs_test.rs index a7951c656..9504e4359 100644 --- a/itest/rust/src/register_tests/register_docs_test.rs +++ b/itest/rust/src/register_tests/register_docs_test.rs @@ -305,15 +305,60 @@ impl FairlyDocumented { fn other_signal(x: i64); } +#[godot_api(secondary)] +impl FairlyDocumented { + /// Documented method in godot_api secondary block + #[func] + fn secondary_but_documented(&self, _smth: i64) {} +} + +#[godot_api(secondary)] +impl FairlyDocumented { + /// Documented method in other godot_api secondary block + #[func] + fn tertiary_but_documented(&self, _smth: i64) {} +} + #[itest] fn test_register_docs() { - let xml = find_class_docs("FairlyDocumented"); + let actual_xml = find_class_docs("FairlyDocumented"); // Uncomment if implementation changes and expected output file should be rewritten. // std::fs::write("../rust/src/register_tests/res/registered_docs.xml", &xml) // .expect("failed to write docs XML file"); - assert_eq!(include_str!("res/registered_docs.xml"), xml); + let expected_xml = include_str!("res/registered_docs.xml"); + + if actual_xml == expected_xml { + return; // All good. + } + + // In GitHub Actions, print output of expected vs. actual. + if crate::framework::runs_github_ci() { + panic!( + "Registered docs XML does not match expected output.\ + \n============================================================\ + \nExpected:\n\n{expected_xml}\n\ + \n------------------------------------------------------------\ + \nActual:\n\n{actual_xml}\n\ + \n============================================================\n" + ); + } + + // Locally, write to a file for manual inspection/diffing. + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("src") + .join("register_tests") + .join("res") + .join("actual_registered_docs.xml"); + + std::fs::write(&path, &actual_xml).expect("write `actual_registered_docs.xml` failed"); + + panic!( + "Registered docs XML does not match expected output.\n\ + Actual output has been written to following file:\n {path}\n", + path = path.display() + ); } fn find_class_docs(class_name: &str) -> String { diff --git a/itest/rust/src/register_tests/res/registered_docs.xml b/itest/rust/src/register_tests/res/registered_docs.xml index a0551a2ba..6ef6f7f29 100644 --- a/itest/rust/src/register_tests/res/registered_docs.xml +++ b/itest/rust/src/register_tests/res/registered_docs.xml @@ -17,6 +17,30 @@ public class Player : Node2D code&nbsp;block </div>[/codeblock][br][br][url=https://www.google.com/search?q=2+%2B+2+<+5]Google: 2 + 2 < 5[/url][br][br]connect these[br][br]¹ [url=https://example.org]https://example.org[/url][br]² because the glorb doesn't flibble.[br]³ This is the third footnote in order of definition.[br]⁴ Fourth footnote in order of definition.[br]⁵ This is the fifth footnote in order of definition.[br]⁶ sixth footnote in order of definition. + + + + + initialize this + + + + + + + + Documented method in godot_api secondary block + + + + + + + + Documented method in other godot_api secondary block + + + @@ -64,14 +88,6 @@ public class Player : Node2D ????? probably - - - - - - initialize this - - Documentation.HmmmmWho would know that!this docstring has < a special character