Browse Source

GDScript: Cancel suspended functions when reloading a script

HolonProduction 6 months ago
parent
commit
676e4c9013

+ 22 - 15
modules/gdscript/gdscript.cpp

@@ -1625,6 +1625,27 @@ void GDScript::clear(ClearData *p_clear_data) {
 	}
 }
 
+void GDScript::cancel_pending_functions(bool warn) {
+	MutexLock lock(GDScriptLanguage::get_singleton()->mutex);
+
+	while (SelfList<GDScriptFunctionState> *E = pending_func_states.first()) {
+		// Order matters since clearing the stack may already cause
+		// the GDScriptFunctionState to be destroyed and thus removed from the list.
+		pending_func_states.remove(E);
+		GDScriptFunctionState *state = E->self();
+#ifdef DEBUG_ENABLED
+		if (warn) {
+			WARN_PRINT("Canceling suspended execution of \"" + state->get_readable_function() + "\" due to a script reload.");
+		}
+#endif
+		ObjectID state_id = state->get_instance_id();
+		state->_clear_connections();
+		if (ObjectDB::get_instance(state_id)) {
+			state->_clear_stack();
+		}
+	}
+}
+
 GDScript::~GDScript() {
 	if (destructing) {
 		return;
@@ -1640,21 +1661,7 @@ GDScript::~GDScript() {
 
 	clear();
 
-	{
-		MutexLock lock(GDScriptLanguage::get_singleton()->mutex);
-
-		while (SelfList<GDScriptFunctionState> *E = pending_func_states.first()) {
-			// Order matters since clearing the stack may already cause
-			// the GDScriptFunctionState to be destroyed and thus removed from the list.
-			pending_func_states.remove(E);
-			GDScriptFunctionState *state = E->self();
-			ObjectID state_id = state->get_instance_id();
-			state->_clear_connections();
-			if (ObjectDB::get_instance(state_id)) {
-				state->_clear_stack();
-			}
-		}
-	}
+	cancel_pending_functions(false);
 
 	{
 		MutexLock lock(GDScriptLanguage::get_singleton()->mutex);

+ 3 - 0
modules/gdscript/gdscript.h

@@ -244,6 +244,9 @@ public:
 
 	void clear(GDScript::ClearData *p_clear_data = nullptr);
 
+	// Cancels all functions of the script that are are waiting to be resumed after using await.
+	void cancel_pending_functions(bool warn);
+
 	virtual bool is_valid() const override { return valid; }
 	virtual bool is_abstract() const override { return false; } // GDScript does not support abstract classes.
 

+ 2 - 0
modules/gdscript/gdscript_compiler.cpp

@@ -2660,6 +2660,8 @@ Error GDScriptCompiler::_prepare_compilation(GDScript *p_script, const GDScriptP
 
 	p_script->clearing = true;
 
+	p_script->cancel_pending_functions(true);
+
 	p_script->native = Ref<GDScriptNativeClass>();
 	p_script->base = Ref<GDScript>();
 	p_script->_base = nullptr;

+ 7 - 0
modules/gdscript/gdscript_function.h

@@ -616,6 +616,13 @@ public:
 	bool is_valid(bool p_extended_check = false) const;
 	Variant resume(const Variant &p_arg = Variant());
 
+#ifdef DEBUG_ENABLED
+	// Returns a human-readable representation of the function.
+	String get_readable_function() {
+		return state.function_name;
+	}
+#endif
+
 	void _clear_stack();
 	void _clear_connections();
 

+ 12 - 0
modules/gdscript/tests/scripts/runtime/errors/reload_suspended_function.notest.gd

@@ -0,0 +1,12 @@
+# TODO: This test is currently disabled since it triggers some complex memory leaks. Try enabling it again once GH-101830 is fixed.
+
+signal finished
+
+const scr: GDScript = preload("reload_suspended_function_helper.notest.gd")
+
+func test():
+	@warning_ignore("UNSAFE_METHOD_ACCESS")
+	scr.test(self)
+	@warning_ignore("RETURN_VALUE_DISCARDED")
+	scr.reload(true)
+	finished.emit()

+ 2 - 0
modules/gdscript/tests/scripts/runtime/errors/reload_suspended_function.out

@@ -0,0 +1,2 @@
+GDTEST_RUNTIME_ERROR
+>> WARNING: Canceling suspended execution of "test" due to a script reload.

+ 3 - 0
modules/gdscript/tests/scripts/runtime/errors/reload_suspended_function_helper.notest.gd

@@ -0,0 +1,3 @@
+static func test(a):
+	await a.finished
+	pass