-
-
Notifications
You must be signed in to change notification settings - Fork 231
Fixed bug causing ConnectHandle::is_connected() to sometimes panic. #1212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Fixed bug causing ConnectHandle::is_connected() to sometimes panic. #1212
Conversation
API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-1212 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A ConnectHandle created from a connection from A to B would panic if
is_connected()
ordisconnect()
was called after A was freed. Fixed byis_connected()
checking if the contained object is valid throughis_instance_valid()
.
Calling disconnect()
on a non-connected signal would still panic, no?
You just change is_connected()
in this PR, and thus the definition of "connected".
Because I think disconnecting an A->B connection after A has been freed is a logic error and should panic, see #1198 (comment).
if !self.receiver_object.is_instance_valid() { | ||
return false; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we go with this, could just use &&
That is a very good point. In addition to not being valid, A wouldn't have any signals either so trying to disconnect anything doesn't make sense. I still feel |
A ConnectHandle created from a connection from A to B would panic if `is_connected()` was called after A was freed. Fixed by `is_connected()` checking if the contained object is valid through `is_instance_valid()`.
c730734
to
3084c81
Compare
@@ -54,7 +54,9 @@ impl ConnectHandle { | |||
/// [`disconnect()`][Self::disconnect] -- e.g. through [`Signal::disconnect()`][crate::builtin::Signal::disconnect] or | |||
/// [`Object::disconnect()`][crate::classes::Object::disconnect]. | |||
pub fn is_connected(&self) -> bool { | |||
self.receiver_object | |||
.is_connected(&*self.signal_name, &self.callable) | |||
self.receiver_object.is_instance_valid() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest something along the lines of:
self.receiver_object.is_instance_valid() | |
if !self.receiver_object.is_instance_valid() { | |
godot_warn!("..."); | |
return false | |
} |
- This is something that probably comes from logic error which should be properly addressed in the userspace and hiding it from user kinda sucks 😅 (i.e. there is 0% chance that it will be spotted). I can't say for sure without analyzing solid usecases though 🤔.
- We can always easily slap
#[cfg(debug_assertions)]
on it later. (or maybe never? or maybe now? Hell if I know, CC: @Bromeon)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm, now I see the previous comment 🤔. I still personally think that we should emit some warning in such case though (correct me if I'm wrong ofc).
@andersgaustad could you tell us more about the use case / context in which you encountered this, and where you think |
Sure! I have certain "Card" nodes that initializes certain connections when they are spawned. For all incoming connections (e.g., from the global singleton "GlobalEventBus"), I save the ConnectHandles in the following way: #[derive(GodotClass)]
#[class(init, base=Node)]
struct DemoCard {
connections_to_me: Vec<ConnectHandle>,
_base: Base<Node>,
} I also implement Drop so that the connections are disconnected automatically once the cards are freed: impl Drop for DemoCard {
fn drop(&mut self) {
for connection_handle in self.connections_to_me.drain(..) {
if connection_handle.is_connected() {
connection_handle.disconnect();
}
}
}
} This usually works fine, but I accidentally added a handle representing a connection for the card to itself. When the card tries to check the connection, In my case, this was something caused by a clearly unintended configuration, but it might be nice to prevent the panic nevertheless. That way you save and manage any number of handles, and check if the connections are valid regardless of if the underlying objects are freed or not. On the other hand, I can also see that argument of "if you try to query about connections from a freed node you are most probably doing something wrong". I was able to easily work around the panic by simply not adding the handles for connections done from a node to itself. If that is what we end up with we could simply scrap the validity check and just change the documentation to state that |
extends Node
class_name MyLittleSignaller
signal my_signal
func _ready() -> void:
await get_tree().create_timer(0.50).timeout
print("connections to my signal: ", self.get_signal_connection_list("my_signal"))
await get_tree().create_timer(1.50).timeout
print("connections to my signal after Node2 has been pruned: ", self.get_signal_connection_list("my_signal"))
my_signal.emit()
queue_free()
(...)
extends Node
class_name Receiver
###############################################################################
# Builtin functions #
###############################################################################
func _ready() -> void:
$"../Node".my_signal.connect(some_response)
$"../Node".my_signal.connect(func(): print("anonymous lambda is not bound to any Object"))
await get_tree().create_timer(1.0).timeout
queue_free()
(…)
extends Node
class_name PatientListener
func _ready() -> void:
$"../Node".my_signal.connect(receiver3)
print("incoming connections at start: ", self.get_incoming_connections())
await get_tree().create_timer(3.00).timeout
print("incoming connections after Node has been pruned: ", self.get_incoming_connections())
queue_free()
###############################################################################
# Public functions #
###############################################################################
func receiver3():
print("hello from receiver!") Prints:
And the behavior should be the same for Godot-rust! (if it doesn't behave the same we did a moderately-sized oopsie 😅 ) |
Oh, you are right! I was sure this was sent only for the node being freed but not for any of its children, but it seems like it works exactly as I had hoped. In hindsight I should probably have tested this before just assuming it worked that way 😅
From what I understand, if we free A then all connections A -> B should be disconnected. If we free B instead, any A -> B connection is not disconnected. Emitting the signal in A will then lead to Godot logging the error mentioned in #1113 : Just to be clear, if we have an "A -> B"-connection and free A then the connection should also be disconnected. However, if we store a ConnectHandle that has saved that connection and it tries to query if the connection "is connected" it will try to call |
Since we have the knowledge of B whenever one of the |
I would also check if given Callables are pruned as well – i.e. if it doesn't result in some memory leak (I'm 95% sure that it does not? It won't hurt to check it anyway).
Ideally it should work the same as in gdscript (i.e. all related signals are being disconnected when object is freed).
Either tracking or using |
Did some quick testing, and: let typed_a = SignalDisc::new_alloc();
let typed_b = SignalDisc::new_alloc();
typed_a.signals().my_signal().connect_other(
&typed_b,
|other| {
other.increment_self();
}
);
let signal_dict = typed_a.get_signal_connection_list("my_signal").at(0);
let signal = signal_dict.at("signal").to::<Signal>();
let callable = signal_dict.at("callable").to::<Callable>();
callable.call(&[]);
eprintln!("Typed: Before free: S: {:?} - C: {:?} - B: {}", &signal, &callable, typed_b.bind().counter);
typed_a.free();
callable.call(&[]);
eprintln!("Typed: After free: S: {:?} - C: {:?} - B: {}", &signal, &callable, typed_b.bind().counter);
typed_b.free();
let mut untyped_a = SignalDisc::new_alloc();
let untyped_b = SignalDisc::new_alloc();
let mut callable_clone = untyped_b.clone();
let inc_b_callable = Callable::from_local_fn(
"inc_callable",
move |_| {
callable_clone.bind_mut().increment_self();
Ok(Variant::nil())
}
);
untyped_a.connect("my_signal", &inc_b_callable);
let signal_dict = untyped_a.get_signal_connection_list("my_signal").at(0);
let signal = signal_dict.at("signal").to::<Signal>();
let callable = signal_dict.at("callable").to::<Callable>();
callable.call(&[]);
eprintln!("Untyped: Before free: S: {:?} - C: {:?} - B: {}", &signal, &callable, untyped_b.bind().counter);
untyped_a.free();
callable.call(&[]);
eprintln!("Untyped: After free: S: {:?} - C: {:?} - B: {}", &signal, &callable, untyped_b.bind().counter);
untyped_b.free(); prints:
... which seems to indicate that typed and untyped signals behave the same when the broadcaster is freed?
Something like: let incoming = obj.get_incoming_connections();
for to_me in incoming.iter_shared() {
let signal = to_me.at("signal").to::<Signal>();
let callable = to_me.at("callable").to::<Callable>();
signal.disconnect(&callable);
} ... though that would have to be done inside the "destructor" of Object when it recivies NOTIFICATION_PREDELETE, right? |
Follow-up to #1198
A ConnectHandle created from a connection from A to B would panic if
is_connected()
was called after A was freed. Fixed byis_connected()
checking if the contained object is valid throughis_instance_valid()
.For example, the following will panic:
(Note that if the receiver is freed the connection will still exist. Emitting the signal will then cause another panic, but disconnection works as intended. I feel personally this makes sense as the connection in this case does indeed exist, but that the connected receiver isn't valid, and that you normally want to disconnect in this case.)
The fix itself is very simple and achieved by using a
if !self.receiver_object.is_instance_valid()
-guard. As I think(?) we briefly discussed in #1198, the good aspect of this is thatis_connected()
(now) won't panic, but the documentation ofis_instance_valid()
seems to discourage its use.EDIT: Removed part about
disconnect()
- this should always panic if A is freed.