Skip to content

Commit 73fed1f

Browse files
committed
No longer execute run_deferred* during engine shutdown
1 parent f5ff3f7 commit 73fed1f

2 files changed

Lines changed: 37 additions & 1 deletion

File tree

godot-core/src/obj/gd.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,12 @@ impl<T: GodotClass> Gd<T> {
878878
);
879879

880880
let callable = Callable::from_once_fn("run_deferred", move |_| {
881+
// Skip if the engine is exiting: the deferred call would otherwise run after `SceneTree` teardown, where accessing freed objects
882+
// (e.g. autoloads) panics. This matches Godot's own `call_deferred()`, which drops queued calls to freed objects at shutdown.
883+
// See `async_runtime::is_engine_exiting()`.
884+
if crate::task::is_engine_exiting() {
885+
return;
886+
}
881887
gd_function(obj);
882888
});
883889
callable.call_deferred(&[]);

itest/rust/src/object_tests/call_deferred_test.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
66
*/
77

8+
use std::cell::Cell;
89
use std::ops::DerefMut;
10+
use std::rc::Rc;
911

1012
use godot::obj::WithBaseField;
1113
use godot::prelude::*;
12-
use godot::task::{SignalFuture, TaskHandle};
14+
use godot::task::{self, SignalFuture, TaskHandle};
1315

1416
use crate::framework::itest;
1517

@@ -143,6 +145,34 @@ fn run_deferred_gd_user_class(ctx: &crate::framework::TestContext) -> TaskHandle
143145
guard.create_assertion_task()
144146
}
145147

148+
// Regression test for https://github.com/godot-rust/gdext/issues/1624: A `run_deferred[_gd]` closure must not run after `SceneTree` teardown,
149+
// matching Godot's `call_deferred()` which drops such queued calls.
150+
#[itest(async)]
151+
fn run_deferred_skipped_when_exiting(ctx: &crate::framework::TestContext) -> TaskHandle {
152+
let mut test_node = DeferredTestNode::new_alloc();
153+
ctx.scene_tree.clone().add_child(&test_node);
154+
155+
// The guard clears the exiting flag when dropped. The deferred call is flushed only a frame later, so we must keep the guard alive until
156+
// after that flush -- otherwise the flag would be cleared too early and the closure would run. Hence it is moved into the async task below.
157+
let guard = task::simulate_engine_exiting();
158+
159+
let ran = Rc::new(Cell::new(false));
160+
let ran_setter = ran.clone();
161+
test_node.run_deferred_gd(move |_| ran_setter.set(true));
162+
163+
// `test_completed` fires from `process()`, by which point the deferred queue has already been flushed (see other tests above).
164+
let mut guard_node = test_node.bind_mut();
165+
let run_test: SignalFuture<(StringName,)> = guard_node.signals().test_completed().to_future();
166+
drop(guard_node);
167+
168+
task::spawn(async move {
169+
let _ = run_test.await;
170+
let was_run = ran.get();
171+
drop(guard); // Keep the flag set until after the deferred flush.
172+
assert!(!was_run, "run_deferred_gd() must not run during shutdown");
173+
})
174+
}
175+
146176
#[itest(async)]
147177
fn run_deferred_engine_class(ctx: &crate::framework::TestContext) -> TaskHandle {
148178
let mut test_node = DeferredTestNode::new_alloc();

0 commit comments

Comments
 (0)