From 0c1ea724b3e6fd90f89a957b9df200664804883c Mon Sep 17 00:00:00 2001 From: goatfryed Date: Sun, 15 Jun 2025 18:22:56 +0200 Subject: [PATCH 1/6] test(core): test Object::call_deferred --- .../src/object_tests/call_deferred_test.rs | 75 +++++++++++++++++++ itest/rust/src/object_tests/mod.rs | 3 + 2 files changed, 78 insertions(+) create mode 100644 itest/rust/src/object_tests/call_deferred_test.rs diff --git a/itest/rust/src/object_tests/call_deferred_test.rs b/itest/rust/src/object_tests/call_deferred_test.rs new file mode 100644 index 000000000..456987fd0 --- /dev/null +++ b/itest/rust/src/object_tests/call_deferred_test.rs @@ -0,0 +1,75 @@ +/* + * 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::framework::itest; +use crate::object_tests::call_deferred_test::TestState::{Accepted, Initial}; +use godot::prelude::*; +use godot::task::{SignalFuture, TaskHandle}; + +#[derive(GodotConvert, Var, Export, Clone, PartialEq, Debug)] +#[godot(via = GString)] +enum TestState { + Initial, + Accepted, +} + +#[derive(GodotClass)] +#[class(base=Node)] +struct DeferredTestNode { + base: Base, + state: TestState, +} + +#[godot_api] +impl DeferredTestNode { + #[signal] + fn test_completed(state: TestState); + + #[func] + fn accept(&mut self) { + self.state = Accepted; + } + + fn as_expectation_task(&self) -> TaskHandle { + assert_eq!(Initial, self.state, "accept evaluated synchronously"); + + let test_will_succeed: SignalFuture<(Variant,)> = + Signal::from_object_signal(&self.to_gd(), "test_completed").to_future(); + godot::task::spawn(async move { + let (final_state,) = test_will_succeed.await; + let final_state: TestState = final_state.to(); + + assert_eq!(Accepted, final_state); + }) + } +} + +#[godot_api] +impl INode for DeferredTestNode { + fn init(base: Base) -> Self { + Self { + base, + state: Initial, + } + } + + fn process(&mut self, _delta: f64) { + let args = vslice![self.state]; + self.base_mut().emit_signal("test_completed", args); + } +} + +#[itest(async)] +fn calls_method_names_deferred(ctx: &crate::framework::TestContext) -> TaskHandle { + let mut test_node = DeferredTestNode::new_alloc(); + ctx.scene_tree.clone().add_child(&test_node); + + test_node.call_deferred("accept", &[]); + + let handle = test_node.bind().as_expectation_task(); + handle +} diff --git a/itest/rust/src/object_tests/mod.rs b/itest/rust/src/object_tests/mod.rs index 0c7f5486e..7924aebb1 100644 --- a/itest/rust/src/object_tests/mod.rs +++ b/itest/rust/src/object_tests/mod.rs @@ -8,6 +8,9 @@ mod base_test; mod class_name_test; mod class_rename_test; +// Test code depends on task API, Godot 4.2+. +#[cfg(since_api = "4.2")] +mod call_deferred_test; mod dyn_gd_test; mod dynamic_call_test; mod enum_test; From 0fa23b2e2238daf21f23885e70f2a51c45dfc7c8 Mon Sep 17 00:00:00 2001 From: goatfryed Date: Tue, 24 Jun 2025 19:10:51 +0200 Subject: [PATCH 2/6] feat(godot-ffi): stub is_main_thread() for wasm_nothread --- godot-core/src/task/async_runtime.rs | 1 - godot-ffi/src/lib.rs | 12 ++++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/godot-core/src/task/async_runtime.rs b/godot-core/src/task/async_runtime.rs index 133b1bf6d..e97035742 100644 --- a/godot-core/src/task/async_runtime.rs +++ b/godot-core/src/task/async_runtime.rs @@ -95,7 +95,6 @@ pub fn spawn(future: impl Future + 'static) -> TaskHandle { // By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`. // // Once thread-safe futures are possible the restriction can be lifted. - #[cfg(not(wasm_nothreads))] assert!( crate::init::is_main_thread(), "godot_task() can only be used on the main thread" diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index 858c53371..abe96182c 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -435,9 +435,17 @@ pub fn main_thread_id() -> std::thread::ThreadId { /// /// # Panics /// - If it is called before the engine bindings have been initialized. -#[cfg(not(wasm_nothreads))] +/// pub fn is_main_thread() -> bool { - std::thread::current().id() == main_thread_id() + #[cfg(not(wasm_nothreads))] + { + std::thread::current().id() == main_thread_id() + } + + #[cfg(wasm_nothreads)] + { + true + } } // ---------------------------------------------------------------------------------------------------------------------------------------------- From e10faf729df5c10d0336cef745bb35c848c52e87 Mon Sep 17 00:00:00 2001 From: goatfryed Date: Wed, 25 Jun 2025 19:03:56 +0200 Subject: [PATCH 3/6] chore(core): add todo --- godot-core/src/registry/signal/typed_signal.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/godot-core/src/registry/signal/typed_signal.rs b/godot-core/src/registry/signal/typed_signal.rs index 87277b132..a7563f71f 100644 --- a/godot-core/src/registry/signal/typed_signal.rs +++ b/godot-core/src/registry/signal/typed_signal.rs @@ -16,6 +16,7 @@ use std::borrow::Cow; use std::marker::PhantomData; use std::ops::DerefMut; +// TODO(v0.4): find more general name for trait. /// Object part of the signal receiver (handler). /// /// Functionality overlaps partly with [`meta::AsObjectArg`] and [`meta::AsArg`]. Can however not directly be replaced From e85e5e8d36f56451f9666db5c820087d03dca6df Mon Sep 17 00:00:00 2001 From: goatfryed Date: Sun, 15 Jun 2025 19:19:44 +0200 Subject: [PATCH 4/6] feat(core): support easier, type-safe deferred calls --- godot-core/src/obj/call_deferred.rs | 67 +++++++++++++ godot-core/src/obj/mod.rs | 2 + godot-ffi/src/lib.rs | 1 - godot/src/prelude.rs | 1 + .../src/object_tests/call_deferred_test.rs | 97 +++++++++++++------ 5 files changed, 136 insertions(+), 32 deletions(-) create mode 100644 godot-core/src/obj/call_deferred.rs diff --git a/godot-core/src/obj/call_deferred.rs b/godot-core/src/obj/call_deferred.rs new file mode 100644 index 000000000..64708a9b1 --- /dev/null +++ b/godot-core/src/obj/call_deferred.rs @@ -0,0 +1,67 @@ +/* + * 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::{Callable, Variant}; +use crate::meta::UniformObjectDeref; +use crate::obj::bounds::Declarer; +use crate::obj::GodotClass; +#[cfg(since_api = "4.2")] +use crate::registry::signal::ToSignalObj; +use godot_ffi::is_main_thread; +use std::ops::DerefMut; + +// Dummy traits to still allow bounds and imports. +#[cfg(before_api = "4.2")] +pub trait WithDeferredCall {} + +/// Trait that is automatically implemented for engine classes and user classes containing a `Base` field. +/// +/// This trait enables type safe deferred method calls. +/// +/// # Usage +/// +/// ```no_compile +/// # use godot::prelude::*; +/// # use godot::classes::CollisionShape2D; +/// # use std::f32::consts::PI; +/// fn some_fn(mut node: Gd) +/// { +/// node.apply_deferred(|shape_mut| shape_mut.rotate(PI)) +/// } +/// ``` +#[cfg(since_api = "4.2")] +pub trait WithDeferredCall { + /// Runs the given Closure deferred. + /// + /// This can be a type-safe alternative to [`Object::call_deferred`][crate::classes::Object::call_deferred]. This method must be used on the main thread. + fn apply_deferred(&mut self, rust_function: F) + where + F: FnMut(&mut T) + 'static; +} + +#[cfg(since_api = "4.2")] +impl WithDeferredCall for S +where + T: UniformObjectDeref, + S: ToSignalObj, + D: Declarer, +{ + fn apply_deferred<'a, F>(&mut self, mut rust_function: F) + where + F: FnMut(&mut T) + 'static, + { + assert!( + is_main_thread(), + "`apply_deferred` must be called on the main thread" + ); + let mut this = self.to_signal_obj().clone(); + let callable = Callable::from_local_fn("apply_deferred", move |_| { + rust_function(T::object_as_mut(&mut this).deref_mut()); + Ok(Variant::nil()) + }); + callable.call_deferred(&[]); + } +} diff --git a/godot-core/src/obj/mod.rs b/godot-core/src/obj/mod.rs index cf1c24a44..81df936c2 100644 --- a/godot-core/src/obj/mod.rs +++ b/godot-core/src/obj/mod.rs @@ -12,6 +12,7 @@ //! * [`Gd`], a smart pointer that manages instances of Godot classes. mod base; +mod call_deferred; mod casts; mod dyn_gd; mod gd; @@ -25,6 +26,7 @@ mod traits; pub(crate) mod rtti; pub use base::*; +pub use call_deferred::WithDeferredCall; pub use dyn_gd::DynGd; pub use gd::*; pub use guards::{BaseMut, BaseRef, DynGdMut, DynGdRef, GdMut, GdRef}; diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index abe96182c..fbbfd375f 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -435,7 +435,6 @@ pub fn main_thread_id() -> std::thread::ThreadId { /// /// # Panics /// - If it is called before the engine bindings have been initialized. -/// pub fn is_main_thread() -> bool { #[cfg(not(wasm_nothreads))] { diff --git a/godot/src/prelude.rs b/godot/src/prelude.rs index fea22fceb..c13498123 100644 --- a/godot/src/prelude.rs +++ b/godot/src/prelude.rs @@ -36,5 +36,6 @@ pub use super::obj::EngineEnum as _; pub use super::obj::NewAlloc as _; pub use super::obj::NewGd as _; pub use super::obj::WithBaseField as _; // base(), base_mut(), to_gd() +pub use super::obj::WithDeferredCall as _; pub use super::obj::WithSignals as _; // Gd::signals() pub use super::obj::WithUserSignals as _; // self.signals() diff --git a/itest/rust/src/object_tests/call_deferred_test.rs b/itest/rust/src/object_tests/call_deferred_test.rs index 456987fd0..50a3b7cf0 100644 --- a/itest/rust/src/object_tests/call_deferred_test.rs +++ b/itest/rust/src/object_tests/call_deferred_test.rs @@ -4,72 +4,107 @@ * 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::framework::itest; -use crate::object_tests::call_deferred_test::TestState::{Accepted, Initial}; +use godot::obj::WithBaseField; use godot::prelude::*; use godot::task::{SignalFuture, TaskHandle}; +use std::ops::DerefMut; -#[derive(GodotConvert, Var, Export, Clone, PartialEq, Debug)] -#[godot(via = GString)] -enum TestState { - Initial, - Accepted, -} +const ACCEPTED_NAME: &str = "touched"; #[derive(GodotClass)] -#[class(base=Node)] +#[class(init,base=Node2D)] struct DeferredTestNode { - base: Base, - state: TestState, + base: Base, } #[godot_api] impl DeferredTestNode { #[signal] - fn test_completed(state: TestState); + fn test_completed(name: StringName); #[func] fn accept(&mut self) { - self.state = Accepted; + self.base_mut().set_name(ACCEPTED_NAME); } - fn as_expectation_task(&self) -> TaskHandle { - assert_eq!(Initial, self.state, "accept evaluated synchronously"); + fn to_assertion_task(&self) -> TaskHandle { + assert_ne!( + self.base().get_name().to_string(), + ACCEPTED_NAME, + "accept evaluated synchronously" + ); - let test_will_succeed: SignalFuture<(Variant,)> = + let test_will_succeed: SignalFuture<(StringName,)> = Signal::from_object_signal(&self.to_gd(), "test_completed").to_future(); + godot::task::spawn(async move { - let (final_state,) = test_will_succeed.await; - let final_state: TestState = final_state.to(); + let (name,) = test_will_succeed.await; - assert_eq!(Accepted, final_state); + assert_eq!(name.to_string(), ACCEPTED_NAME); }) } } #[godot_api] -impl INode for DeferredTestNode { - fn init(base: Base) -> Self { - Self { - base, - state: Initial, - } +impl INode2D for DeferredTestNode { + fn process(&mut self, _delta: f64) { + let name = self.base().get_name(); + self.signals().test_completed().emit(&name); + self.base_mut().queue_free(); } - fn process(&mut self, _delta: f64) { - let args = vslice![self.state]; - self.base_mut().emit_signal("test_completed", args); + fn ready(&mut self) { + self.base_mut().set_name("verify") } } #[itest(async)] -fn calls_method_names_deferred(ctx: &crate::framework::TestContext) -> TaskHandle { +fn call_deferred_untyped(ctx: &crate::framework::TestContext) -> TaskHandle { let mut test_node = DeferredTestNode::new_alloc(); ctx.scene_tree.clone().add_child(&test_node); - + + // this is called through godot binding and therefore requires #[func] on the method test_node.call_deferred("accept", &[]); - let handle = test_node.bind().as_expectation_task(); + let handle = test_node.bind().to_assertion_task(); + handle +} + +#[itest(async)] +fn call_deferred_godot_class(ctx: &crate::framework::TestContext) -> TaskHandle { + let mut test_node = DeferredTestNode::new_alloc(); + ctx.scene_tree.clone().add_child(&test_node); + + let mut gd_mut = test_node.bind_mut(); + // Explicitly check that this can be invoked on &mut T. + let godot_class_ref: &mut DeferredTestNode = gd_mut.deref_mut(); + godot_class_ref.apply_deferred(DeferredTestNode::accept); + drop(gd_mut); + + let handle = test_node.bind().to_assertion_task(); + handle +} + +#[itest(async)] +fn call_deferred_gd_user_class(ctx: &crate::framework::TestContext) -> TaskHandle { + let mut test_node = DeferredTestNode::new_alloc(); + ctx.scene_tree.clone().add_child(&test_node); + + test_node.apply_deferred(DeferredTestNode::accept); + + let handle = test_node.bind().to_assertion_task(); + handle +} + +#[itest(async)] +fn call_deferred_gd_engine_class(ctx: &crate::framework::TestContext) -> TaskHandle { + let test_node = DeferredTestNode::new_alloc(); + ctx.scene_tree.clone().add_child(&test_node); + + let mut node = test_node.clone().upcast::(); + node.apply_deferred(|that_node| that_node.set_name(ACCEPTED_NAME)); + + let handle = test_node.bind().to_assertion_task(); handle } From 002a2ce32674a84af20917774c62745a52805731 Mon Sep 17 00:00:00 2001 From: goatfryed Date: Sat, 5 Jul 2025 21:11:38 +0200 Subject: [PATCH 5/6] feat(core): support easier, type-safe deferred calls --- godot-core/src/obj/call_deferred.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/godot-core/src/obj/call_deferred.rs b/godot-core/src/obj/call_deferred.rs index 64708a9b1..757a7e09e 100644 --- a/godot-core/src/obj/call_deferred.rs +++ b/godot-core/src/obj/call_deferred.rs @@ -23,9 +23,8 @@ pub trait WithDeferredCall {} /// /// # Usage /// -/// ```no_compile +/// ```no_run /// # use godot::prelude::*; -/// # use godot::classes::CollisionShape2D; /// # use std::f32::consts::PI; /// fn some_fn(mut node: Gd) /// { @@ -36,7 +35,10 @@ pub trait WithDeferredCall {} pub trait WithDeferredCall { /// Runs the given Closure deferred. /// - /// This can be a type-safe alternative to [`Object::call_deferred`][crate::classes::Object::call_deferred]. This method must be used on the main thread. + /// This is a type-safe alternative to [`Object::call_deferred`][crate::classes::Object::call_deferred]. + /// + /// # Panics + /// If called outside the main thread. fn apply_deferred(&mut self, rust_function: F) where F: FnMut(&mut T) + 'static; From 40792fc760007d598a504e7f81263b0c06a92820 Mon Sep 17 00:00:00 2001 From: goatfryed Date: Mon, 7 Jul 2025 17:03:09 +0200 Subject: [PATCH 6/6] feat(core): lift FnMut restriction of apply_deferred --- godot-core/src/obj/call_deferred.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/godot-core/src/obj/call_deferred.rs b/godot-core/src/obj/call_deferred.rs index 757a7e09e..eb03e59ab 100644 --- a/godot-core/src/obj/call_deferred.rs +++ b/godot-core/src/obj/call_deferred.rs @@ -41,7 +41,7 @@ pub trait WithDeferredCall { /// If called outside the main thread. fn apply_deferred(&mut self, rust_function: F) where - F: FnMut(&mut T) + 'static; + F: FnOnce(&mut T) + 'static; } #[cfg(since_api = "4.2")] @@ -51,17 +51,22 @@ where S: ToSignalObj, D: Declarer, { - fn apply_deferred<'a, F>(&mut self, mut rust_function: F) + fn apply_deferred<'a, F>(&mut self, rust_function: F) where - F: FnMut(&mut T) + 'static, + F: FnOnce(&mut T) + 'static, { assert!( is_main_thread(), "`apply_deferred` must be called on the main thread" ); + let mut rust_fn_once = Some(rust_function); let mut this = self.to_signal_obj().clone(); let callable = Callable::from_local_fn("apply_deferred", move |_| { - rust_function(T::object_as_mut(&mut this).deref_mut()); + let rust_fn_once = rust_fn_once + .take() + .expect("rust_fn_once was already consumed"); + let mut this_mut = T::object_as_mut(&mut this); + rust_fn_once(this_mut.deref_mut()); Ok(Variant::nil()) }); callable.call_deferred(&[]);