Skip to content

Commit 47cc8da

Browse files
authored
Merge pull request #1204 from goatfryed/feat/1199-type-safe-call-deferred
Type-safe `call_deferred` alternative
2 parents 00ec5e1 + 3cf6fdc commit 47cc8da

File tree

8 files changed

+196
-3
lines changed

8 files changed

+196
-3
lines changed

godot-core/src/obj/call_deferred.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright (c) godot-rust; Bromeon and contributors.
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
*/
7+
use crate::builtin::{Callable, Variant};
8+
use crate::meta::UniformObjectDeref;
9+
use crate::obj::bounds::Declarer;
10+
use crate::obj::GodotClass;
11+
#[cfg(since_api = "4.2")]
12+
use crate::registry::signal::ToSignalObj;
13+
use godot_ffi::is_main_thread;
14+
use std::ops::DerefMut;
15+
16+
// Dummy traits to still allow bounds and imports.
17+
#[cfg(before_api = "4.2")]
18+
pub trait WithDeferredCall<T: GodotClass> {}
19+
20+
/// Enables `Gd::apply_deferred()` for type-safe deferred calls.
21+
///
22+
/// The trait is automatically available for all engine-defined Godot classes and user classes containing a `Base<T>` field.
23+
///
24+
/// # Usage
25+
///
26+
/// ```no_run
27+
/// # use godot::prelude::*;
28+
/// # use std::f32::consts::PI;
29+
/// fn some_fn(mut node: Gd<Node2D>) {
30+
/// node.apply_deferred(|n: &mut Node2D| n.rotate(PI))
31+
/// }
32+
/// ```
33+
#[cfg(since_api = "4.2")]
34+
pub trait WithDeferredCall<T: GodotClass> {
35+
/// Defers the given closure to run during [idle time](https://docs.godotengine.org/en/stable/classes/class_object.html#class-object-method-call-deferred).
36+
///
37+
/// This is a type-safe alternative to [`Object::call_deferred()`][crate::classes::Object::call_deferred].
38+
///
39+
/// # Panics
40+
/// If called outside the main thread.
41+
fn apply_deferred<F>(&mut self, rust_function: F)
42+
where
43+
F: FnOnce(&mut T) + 'static;
44+
}
45+
46+
#[cfg(since_api = "4.2")]
47+
impl<T, S, D> WithDeferredCall<T> for S
48+
where
49+
T: UniformObjectDeref<D, Declarer = D>,
50+
S: ToSignalObj<T>,
51+
D: Declarer,
52+
{
53+
fn apply_deferred<'a, F>(&mut self, rust_function: F)
54+
where
55+
F: FnOnce(&mut T) + 'static,
56+
{
57+
assert!(
58+
is_main_thread(),
59+
"`apply_deferred` must be called on the main thread"
60+
);
61+
let mut rust_fn_once = Some(rust_function);
62+
let mut this = self.to_signal_obj().clone();
63+
let callable = Callable::from_local_fn("apply_deferred", move |_| {
64+
let rust_fn_once = rust_fn_once
65+
.take()
66+
.expect("rust_fn_once was already consumed");
67+
let mut this_mut = T::object_as_mut(&mut this);
68+
rust_fn_once(this_mut.deref_mut());
69+
Ok(Variant::nil())
70+
});
71+
callable.call_deferred(&[]);
72+
}
73+
}

godot-core/src/obj/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
//! * [`Gd`], a smart pointer that manages instances of Godot classes.
1313
1414
mod base;
15+
mod call_deferred;
1516
mod casts;
1617
mod dyn_gd;
1718
mod gd;
@@ -25,6 +26,7 @@ mod traits;
2526
pub(crate) mod rtti;
2627

2728
pub use base::*;
29+
pub use call_deferred::WithDeferredCall;
2830
pub use dyn_gd::DynGd;
2931
pub use gd::*;
3032
pub use guards::{BaseMut, BaseRef, DynGdMut, DynGdRef, GdMut, GdRef};

godot-core/src/registry/signal/typed_signal.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use std::borrow::Cow;
1616
use std::marker::PhantomData;
1717
use std::ops::DerefMut;
1818

19+
// TODO(v0.4): find more general name for trait.
1920
/// Object part of the signal receiver (handler).
2021
///
2122
/// Functionality overlaps partly with [`meta::AsObjectArg`] and [`meta::AsArg<ObjectArg>`]. Can however not directly be replaced

godot-core/src/task/async_runtime.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ pub fn spawn(future: impl Future<Output = ()> + 'static) -> TaskHandle {
9595
// By limiting async tasks to the main thread we can redirect all signal callbacks back to the main thread via `call_deferred`.
9696
//
9797
// Once thread-safe futures are possible the restriction can be lifted.
98-
#[cfg(not(wasm_nothreads))]
9998
assert!(
10099
crate::init::is_main_thread(),
101100
"godot_task() can only be used on the main thread"

godot-ffi/src/lib.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -435,9 +435,16 @@ pub fn main_thread_id() -> std::thread::ThreadId {
435435
///
436436
/// # Panics
437437
/// - If it is called before the engine bindings have been initialized.
438-
#[cfg(not(wasm_nothreads))]
439438
pub fn is_main_thread() -> bool {
440-
std::thread::current().id() == main_thread_id()
439+
#[cfg(not(wasm_nothreads))]
440+
{
441+
std::thread::current().id() == main_thread_id()
442+
}
443+
444+
#[cfg(wasm_nothreads)]
445+
{
446+
true
447+
}
441448
}
442449

443450
// ----------------------------------------------------------------------------------------------------------------------------------------------

godot/src/prelude.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,6 @@ pub use super::obj::EngineEnum as _;
3636
pub use super::obj::NewAlloc as _;
3737
pub use super::obj::NewGd as _;
3838
pub use super::obj::WithBaseField as _; // base(), base_mut(), to_gd()
39+
pub use super::obj::WithDeferredCall as _; // apply_deferred()
3940
pub use super::obj::WithSignals as _; // Gd::signals()
4041
pub use super::obj::WithUserSignals as _; // self.signals()
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright (c) godot-rust; Bromeon and contributors.
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
*/
7+
use crate::framework::itest;
8+
use godot::obj::WithBaseField;
9+
use godot::prelude::*;
10+
use godot::task::{SignalFuture, TaskHandle};
11+
use std::ops::DerefMut;
12+
13+
const ACCEPTED_NAME: &str = "touched";
14+
15+
#[derive(GodotClass)]
16+
#[class(init,base=Node2D)]
17+
struct DeferredTestNode {
18+
base: Base<Node2D>,
19+
}
20+
21+
#[godot_api]
22+
impl DeferredTestNode {
23+
#[signal]
24+
fn test_completed(name: StringName);
25+
26+
#[func]
27+
fn accept(&mut self) {
28+
self.base_mut().set_name(ACCEPTED_NAME);
29+
}
30+
31+
fn create_assertion_task(&mut self) -> TaskHandle {
32+
assert_ne!(
33+
self.base().get_name().to_string(),
34+
ACCEPTED_NAME,
35+
"accept evaluated synchronously"
36+
);
37+
38+
let run_test: SignalFuture<(StringName,)> = self.signals().test_completed().to_future();
39+
40+
godot::task::spawn(async move {
41+
let (name,) = run_test.await;
42+
43+
assert_eq!(name.to_string(), ACCEPTED_NAME);
44+
})
45+
}
46+
}
47+
48+
#[godot_api]
49+
impl INode2D for DeferredTestNode {
50+
fn process(&mut self, _delta: f64) {
51+
let name = self.base().get_name();
52+
self.signals().test_completed().emit(&name);
53+
self.base_mut().queue_free();
54+
}
55+
56+
fn ready(&mut self) {
57+
self.base_mut().set_name("verify");
58+
}
59+
}
60+
61+
#[itest(async)]
62+
fn call_deferred_untyped(ctx: &crate::framework::TestContext) -> TaskHandle {
63+
let mut test_node = DeferredTestNode::new_alloc();
64+
ctx.scene_tree.clone().add_child(&test_node);
65+
66+
// Called through Godot and therefore requires #[func] on the method.
67+
test_node.call_deferred("accept", &[]);
68+
69+
let mut gd_mut = test_node.bind_mut();
70+
gd_mut.create_assertion_task()
71+
}
72+
73+
#[itest(async)]
74+
fn call_deferred_godot_class(ctx: &crate::framework::TestContext) -> TaskHandle {
75+
let mut test_node = DeferredTestNode::new_alloc();
76+
ctx.scene_tree.clone().add_child(&test_node);
77+
78+
let mut gd_mut = test_node.bind_mut();
79+
// Explicitly check that this can be invoked on &mut T.
80+
let godot_class_ref: &mut DeferredTestNode = gd_mut.deref_mut();
81+
godot_class_ref.apply_deferred(DeferredTestNode::accept);
82+
83+
gd_mut.create_assertion_task()
84+
}
85+
86+
#[itest(async)]
87+
fn call_deferred_gd_user_class(ctx: &crate::framework::TestContext) -> TaskHandle {
88+
let mut test_node = DeferredTestNode::new_alloc();
89+
ctx.scene_tree.clone().add_child(&test_node);
90+
91+
test_node.apply_deferred(DeferredTestNode::accept);
92+
93+
let mut gd_mut = test_node.bind_mut();
94+
gd_mut.create_assertion_task()
95+
}
96+
97+
#[itest(async)]
98+
fn call_deferred_gd_engine_class(ctx: &crate::framework::TestContext) -> TaskHandle {
99+
let mut test_node = DeferredTestNode::new_alloc();
100+
ctx.scene_tree.clone().add_child(&test_node);
101+
102+
let mut node = test_node.clone().upcast::<Node>();
103+
node.apply_deferred(|that_node| that_node.set_name(ACCEPTED_NAME));
104+
105+
let mut gd_mut = test_node.bind_mut();
106+
gd_mut.create_assertion_task()
107+
}

itest/rust/src/object_tests/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
mod base_test;
99
mod class_name_test;
1010
mod class_rename_test;
11+
// Test code depends on task API, Godot 4.2+.
12+
#[cfg(since_api = "4.2")]
13+
mod call_deferred_test;
1114
mod dyn_gd_test;
1215
mod dynamic_call_test;
1316
mod enum_test;

0 commit comments

Comments
 (0)