Skip to content

Commit c5c1cd4

Browse files
committed
Merge pull request #105453 from reduz/signals-thread-safe
Add thread safety to Object signals
2 parents 6448ddf + 2f39d8e commit c5c1cd4

File tree

4 files changed

+124
-56
lines changed

4 files changed

+124
-56
lines changed

core/object/object.cpp

Lines changed: 117 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,23 @@ struct _ObjectDebugLock {
6565

6666
#endif
6767

68+
struct _ObjectSignalLock {
69+
Mutex *mutex;
70+
_ObjectSignalLock(const Object *const p_obj) {
71+
mutex = p_obj->signal_mutex;
72+
if (mutex) {
73+
mutex->lock();
74+
}
75+
}
76+
~_ObjectSignalLock() {
77+
if (mutex) {
78+
mutex->unlock();
79+
}
80+
}
81+
};
82+
83+
#define OBJ_SIGNAL_LOCK _ObjectSignalLock _signal_lock(this);
84+
6885
PropertyInfo::operator Dictionary() const {
6986
Dictionary d;
7087
d["name"] = name;
@@ -255,6 +272,9 @@ void Object::_initialize() {
255272
}
256273

257274
void Object::_postinitialize() {
275+
if (_uses_signal_mutex()) {
276+
signal_mutex = memnew(Mutex);
277+
}
258278
notification(NOTIFICATION_POSTINITIALIZE);
259279
}
260280

@@ -1123,20 +1143,27 @@ void Object::get_meta_list(List<StringName> *p_list) const {
11231143
void Object::add_user_signal(const MethodInfo &p_signal) {
11241144
ERR_FAIL_COND_MSG(p_signal.name.is_empty(), "Signal name cannot be empty.");
11251145
ERR_FAIL_COND_MSG(ClassDB::has_signal(get_class_name(), p_signal.name), vformat("User signal's name conflicts with a built-in signal of '%s'.", get_class_name()));
1146+
1147+
OBJ_SIGNAL_LOCK
1148+
11261149
ERR_FAIL_COND_MSG(signal_map.has(p_signal.name), vformat("Trying to add already existing signal '%s'.", p_signal.name));
11271150
SignalData s;
11281151
s.user = p_signal;
11291152
signal_map[p_signal.name] = s;
11301153
}
11311154

11321155
bool Object::_has_user_signal(const StringName &p_name) const {
1156+
OBJ_SIGNAL_LOCK
1157+
11331158
if (!signal_map.has(p_name)) {
11341159
return false;
11351160
}
11361161
return signal_map[p_name].user.name.length() > 0;
11371162
}
11381163

11391164
void Object::_remove_user_signal(const StringName &p_name) {
1165+
OBJ_SIGNAL_LOCK
1166+
11401167
SignalData *s = signal_map.getptr(p_name);
11411168
ERR_FAIL_NULL_MSG(s, "Provided signal does not exist.");
11421169
ERR_FAIL_COND_MSG(!s->removable, "Signal is not removable (not added with add_user_signal).");
@@ -1183,46 +1210,53 @@ Error Object::emit_signalp(const StringName &p_name, const Variant **p_args, int
11831210
return ERR_CANT_ACQUIRE_RESOURCE; //no emit, signals blocked
11841211
}
11851212

1186-
SignalData *s = signal_map.getptr(p_name);
1187-
if (!s) {
1213+
Callable *slot_callables = nullptr;
1214+
uint32_t *slot_flags = nullptr;
1215+
uint32_t slot_count = 0;
1216+
1217+
{
1218+
OBJ_SIGNAL_LOCK
1219+
1220+
SignalData *s = signal_map.getptr(p_name);
1221+
if (!s) {
11881222
#ifdef DEBUG_ENABLED
1189-
bool signal_is_valid = ClassDB::has_signal(get_class_name(), p_name);
1190-
//check in script
1191-
ERR_FAIL_COND_V_MSG(!signal_is_valid && !script.is_null() && !Ref<Script>(script)->has_script_signal(p_name), ERR_UNAVAILABLE, vformat("Can't emit non-existing signal \"%s\".", p_name));
1223+
bool signal_is_valid = ClassDB::has_signal(get_class_name(), p_name);
1224+
//check in script
1225+
ERR_FAIL_COND_V_MSG(!signal_is_valid && !script.is_null() && !Ref<Script>(script)->has_script_signal(p_name), ERR_UNAVAILABLE, vformat("Can't emit non-existing signal \"%s\".", p_name));
11921226
#endif
1193-
//not connected? just return
1194-
return ERR_UNAVAILABLE;
1195-
}
1227+
//not connected? just return
1228+
return ERR_UNAVAILABLE;
1229+
}
11961230

1197-
// If this is a ref-counted object, prevent it from being destroyed during signal emission,
1198-
// which is needed in certain edge cases; e.g., https://github.com/godotengine/godot/issues/73889.
1199-
Ref<RefCounted> rc = Ref<RefCounted>(Object::cast_to<RefCounted>(this));
1231+
// If this is a ref-counted object, prevent it from being destroyed during signal emission,
1232+
// which is needed in certain edge cases; e.g., https://github.com/godotengine/godot/issues/73889.
1233+
Ref<RefCounted> rc = Ref<RefCounted>(Object::cast_to<RefCounted>(this));
12001234

1201-
// Ensure that disconnecting the signal or even deleting the object
1202-
// will not affect the signal calling.
1203-
Callable *slot_callables = (Callable *)alloca(sizeof(Callable) * s->slot_map.size());
1204-
uint32_t *slot_flags = (uint32_t *)alloca(sizeof(uint32_t) * s->slot_map.size());
1205-
uint32_t slot_count = 0;
1235+
// Ensure that disconnecting the signal or even deleting the object
1236+
// will not affect the signal calling.
1237+
slot_callables = (Callable *)alloca(sizeof(Callable) * s->slot_map.size());
1238+
slot_flags = (uint32_t *)alloca(sizeof(uint32_t) * s->slot_map.size());
12061239

1207-
for (const KeyValue<Callable, SignalData::Slot> &slot_kv : s->slot_map) {
1208-
memnew_placement(&slot_callables[slot_count], Callable(slot_kv.value.conn.callable));
1209-
slot_flags[slot_count] = slot_kv.value.conn.flags;
1210-
++slot_count;
1211-
}
1240+
for (const KeyValue<Callable, SignalData::Slot> &slot_kv : s->slot_map) {
1241+
memnew_placement(&slot_callables[slot_count], Callable(slot_kv.value.conn.callable));
1242+
slot_flags[slot_count] = slot_kv.value.conn.flags;
1243+
++slot_count;
1244+
}
12121245

1213-
DEV_ASSERT(slot_count == s->slot_map.size());
1246+
DEV_ASSERT(slot_count == s->slot_map.size());
12141247

1215-
// Disconnect all one-shot connections before emitting to prevent recursion.
1216-
for (uint32_t i = 0; i < slot_count; ++i) {
1217-
bool disconnect = slot_flags[i] & CONNECT_ONE_SHOT;
1248+
// Disconnect all one-shot connections before emitting to prevent recursion.
1249+
for (uint32_t i = 0; i < slot_count; ++i) {
1250+
bool disconnect = slot_flags[i] & CONNECT_ONE_SHOT;
12181251
#ifdef TOOLS_ENABLED
1219-
if (disconnect && (slot_flags[i] & CONNECT_PERSIST) && Engine::get_singleton()->is_editor_hint()) {
1220-
// This signal was connected from the editor, and is being edited. Just don't disconnect for now.
1221-
disconnect = false;
1222-
}
1252+
if (disconnect && (slot_flags[i] & CONNECT_PERSIST) && Engine::get_singleton()->is_editor_hint()) {
1253+
// This signal was connected from the editor, and is being edited. Just don't disconnect for now.
1254+
disconnect = false;
1255+
}
12231256
#endif
1224-
if (disconnect) {
1225-
_disconnect(p_name, slot_callables[i]);
1257+
if (disconnect) {
1258+
_disconnect(p_name, slot_callables[i]);
1259+
}
12261260
}
12271261
}
12281262

@@ -1280,6 +1314,8 @@ void Object::_add_user_signal(const String &p_name, const Array &p_args) {
12801314
// without access to ADD_SIGNAL in bind_methods
12811315
// added events are per instance, as opposed to the other ones, which are global
12821316

1317+
OBJ_SIGNAL_LOCK
1318+
12831319
MethodInfo mi;
12841320
mi.name = p_name;
12851321

@@ -1360,6 +1396,8 @@ bool Object::has_signal(const StringName &p_name) const {
13601396
}
13611397

13621398
void Object::get_signal_list(List<MethodInfo> *p_signals) const {
1399+
OBJ_SIGNAL_LOCK
1400+
13631401
if (!script.is_null()) {
13641402
Ref<Script> scr = script;
13651403
if (scr.is_valid()) {
@@ -1379,6 +1417,8 @@ void Object::get_signal_list(List<MethodInfo> *p_signals) const {
13791417
}
13801418

13811419
void Object::get_all_signal_connections(List<Connection> *p_connections) const {
1420+
OBJ_SIGNAL_LOCK
1421+
13821422
for (const KeyValue<StringName, SignalData> &E : signal_map) {
13831423
const SignalData *s = &E.value;
13841424

@@ -1389,6 +1429,8 @@ void Object::get_all_signal_connections(List<Connection> *p_connections) const {
13891429
}
13901430

13911431
void Object::get_signal_connection_list(const StringName &p_signal, List<Connection> *p_connections) const {
1432+
OBJ_SIGNAL_LOCK
1433+
13921434
const SignalData *s = signal_map.getptr(p_signal);
13931435
if (!s) {
13941436
return; //nothing
@@ -1400,6 +1442,7 @@ void Object::get_signal_connection_list(const StringName &p_signal, List<Connect
14001442
}
14011443

14021444
int Object::get_persistent_signal_connection_count() const {
1445+
OBJ_SIGNAL_LOCK
14031446
int count = 0;
14041447

14051448
for (const KeyValue<StringName, SignalData> &E : signal_map) {
@@ -1416,13 +1459,16 @@ int Object::get_persistent_signal_connection_count() const {
14161459
}
14171460

14181461
void Object::get_signals_connected_to_this(List<Connection> *p_connections) const {
1462+
OBJ_SIGNAL_LOCK
1463+
14191464
for (const Connection &E : connections) {
14201465
p_connections->push_back(E);
14211466
}
14221467
}
14231468

14241469
Error Object::connect(const StringName &p_signal, const Callable &p_callable, uint32_t p_flags) {
14251470
ERR_FAIL_COND_V_MSG(p_callable.is_null(), ERR_INVALID_PARAMETER, vformat("Cannot connect to '%s': the provided callable is null.", p_signal));
1471+
OBJ_SIGNAL_LOCK
14261472

14271473
if (p_callable.is_standard()) {
14281474
// FIXME: This branch should probably removed in favor of the `is_valid()` branch, but there exist some classes
@@ -1491,6 +1537,8 @@ Error Object::connect(const StringName &p_signal, const Callable &p_callable, ui
14911537

14921538
bool Object::is_connected(const StringName &p_signal, const Callable &p_callable) const {
14931539
ERR_FAIL_COND_V_MSG(p_callable.is_null(), false, vformat("Cannot determine if connected to '%s': the provided callable is null.", p_signal)); // Should use `is_null`, see note in `connect` about the use of `is_valid`.
1540+
OBJ_SIGNAL_LOCK
1541+
14941542
const SignalData *s = signal_map.getptr(p_signal);
14951543
if (!s) {
14961544
bool signal_is_valid = ClassDB::has_signal(get_class_name(), p_signal);
@@ -1509,6 +1557,8 @@ bool Object::is_connected(const StringName &p_signal, const Callable &p_callable
15091557
}
15101558

15111559
bool Object::has_connections(const StringName &p_signal) const {
1560+
OBJ_SIGNAL_LOCK
1561+
15121562
const SignalData *s = signal_map.getptr(p_signal);
15131563
if (!s) {
15141564
bool signal_is_valid = ClassDB::has_signal(get_class_name(), p_signal);
@@ -1532,6 +1582,7 @@ void Object::disconnect(const StringName &p_signal, const Callable &p_callable)
15321582

15331583
bool Object::_disconnect(const StringName &p_signal, const Callable &p_callable, bool p_force) {
15341584
ERR_FAIL_COND_V_MSG(p_callable.is_null(), false, vformat("Cannot disconnect from '%s': the provided callable is null.", p_signal)); // Should use `is_null`, see note in `connect` about the use of `is_valid`.
1585+
OBJ_SIGNAL_LOCK
15351586

15361587
SignalData *s = signal_map.getptr(p_signal);
15371588
if (!s) {
@@ -1569,6 +1620,10 @@ bool Object::_disconnect(const StringName &p_signal, const Callable &p_callable,
15691620
return true;
15701621
}
15711622

1623+
bool Object::_uses_signal_mutex() const {
1624+
return true;
1625+
}
1626+
15721627
void Object::_set_bind(const StringName &p_set, const Variant &p_value) {
15731628
set(p_set, p_value);
15741629
}
@@ -2204,33 +2259,36 @@ Object::~Object() {
22042259
ERR_PRINT(vformat("Object '%s' was freed or unreferenced while a signal is being emitted from it. Try connecting to the signal using 'CONNECT_DEFERRED' flag, or use queue_free() to free the object (if this object is a Node) to avoid this error and potential crashes.", to_string()));
22052260
}
22062261

2207-
// Drop all connections to the signals of this object.
2208-
while (signal_map.size()) {
2209-
// Avoid regular iteration so erasing is safe.
2210-
KeyValue<StringName, SignalData> &E = *signal_map.begin();
2211-
SignalData *s = &E.value;
2212-
2213-
for (const KeyValue<Callable, SignalData::Slot> &slot_kv : s->slot_map) {
2214-
Object *target = slot_kv.value.conn.callable.get_object();
2215-
if (likely(target)) {
2216-
target->connections.erase(slot_kv.value.cE);
2262+
{
2263+
OBJ_SIGNAL_LOCK
2264+
// Drop all connections to the signals of this object.
2265+
while (signal_map.size()) {
2266+
// Avoid regular iteration so erasing is safe.
2267+
KeyValue<StringName, SignalData> &E = *signal_map.begin();
2268+
SignalData *s = &E.value;
2269+
2270+
for (const KeyValue<Callable, SignalData::Slot> &slot_kv : s->slot_map) {
2271+
Object *target = slot_kv.value.conn.callable.get_object();
2272+
if (likely(target)) {
2273+
target->connections.erase(slot_kv.value.cE);
2274+
}
22172275
}
2218-
}
22192276

2220-
signal_map.erase(E.key);
2221-
}
2222-
2223-
// Disconnect signals that connect to this object.
2224-
while (connections.size()) {
2225-
Connection c = connections.front()->get();
2226-
Object *obj = c.callable.get_object();
2227-
bool disconnected = false;
2228-
if (likely(obj)) {
2229-
disconnected = c.signal.get_object()->_disconnect(c.signal.get_name(), c.callable, true);
2277+
signal_map.erase(E.key);
22302278
}
2231-
if (unlikely(!disconnected)) {
2232-
// If the disconnect has failed, abandon the connection to avoid getting trapped in an infinite loop here.
2233-
connections.pop_front();
2279+
2280+
// Disconnect signals that connect to this object.
2281+
while (connections.size()) {
2282+
Connection c = connections.front()->get();
2283+
Object *obj = c.callable.get_object();
2284+
bool disconnected = false;
2285+
if (likely(obj)) {
2286+
disconnected = c.signal.get_object()->_disconnect(c.signal.get_name(), c.callable, true);
2287+
}
2288+
if (unlikely(!disconnected)) {
2289+
// If the disconnect has failed, abandon the connection to avoid getting trapped in an infinite loop here.
2290+
connections.pop_front();
2291+
}
22342292
}
22352293
}
22362294

@@ -2248,6 +2306,10 @@ Object::~Object() {
22482306
}
22492307
memfree(_instance_bindings);
22502308
}
2309+
2310+
if (signal_mutex) {
2311+
memdelete(signal_mutex);
2312+
}
22512313
}
22522314

22532315
bool predelete_handler(Object *p_object) {

core/object/object.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,8 @@ class Object {
608608
HashMap<Callable, Slot, HashableHasher<Callable>> slot_map;
609609
bool removable = false;
610610
};
611-
611+
friend struct _ObjectSignalLock;
612+
mutable Mutex *signal_mutex = nullptr;
612613
HashMap<StringName, SignalData> signal_map;
613614
List<Connection> connections;
614615
#ifdef DEBUG_ENABLED
@@ -756,6 +757,8 @@ class Object {
756757

757758
bool _disconnect(const StringName &p_signal, const Callable &p_callable, bool p_force = false);
758759

760+
virtual bool _uses_signal_mutex() const;
761+
759762
#ifdef TOOLS_ENABLED
760763
struct VirtualMethodTracker {
761764
void **method;

doc/classes/Object.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,7 @@
521521
A signal can only be connected once to the same [Callable]. If the signal is already connected, this method returns [constant ERR_INVALID_PARAMETER] and generates an error, unless the signal is connected with [constant CONNECT_REFERENCE_COUNTED]. To prevent this, use [method is_connected] first to check for existing connections.
522522
[b]Note:[/b] If the [param callable]'s object is freed, the connection will be lost.
523523
[b]Note:[/b] In GDScript, it is generally recommended to connect signals with [method Signal.connect] instead.
524+
[b]Note:[/b] This operation (and all other signal related operations) is thread-safe.
524525
</description>
525526
</method>
526527
<method name="disconnect">

scene/main/node.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,8 @@ class Node : public Object {
387387
void _validate_property(PropertyInfo &p_property) const;
388388

389389
protected:
390+
virtual bool _uses_signal_mutex() const override { return false; } // Node uses thread guards instead.
391+
390392
virtual void input(const Ref<InputEvent> &p_event);
391393
virtual void shortcut_input(const Ref<InputEvent> &p_key_event);
392394
virtual void unhandled_input(const Ref<InputEvent> &p_event);

0 commit comments

Comments
 (0)