Is there a proper way to connect a lambda to a signal?
I've realized that the problem with my original code was not doing
callable = callable.bindv([callable, button])
If I actually connect the bound copy then it works as intended
xyz Here's how it works:
Are you sure? That sounds backwards.
I think it makes sense. The signal has a reference to the callable. The callable may have references to the object. If the object is deleted, and the callable has references to it, then calling the callable would lead to errors or undefined behavior. If it doesn't have references to the object, then it should still be able to run on its own fine.
I suspect it's deliberately implemented this way rather than a byproduct of reference-counting.
- Edited
DaveTheCoder Are you sure? That sounds backwards.
Test it out.
Callable can be a method or a standalone function. If it's associated with an object, then it's considered a method. You can check if there is object association via Callable::get_object()
. If a lambda is accessing any properties it will be magically treated as a method. Otherwise as a standalone function.
So if a connected lambda has an object association (is a method), the signal connection will be broken when the associated object is destroyed. If there is no object association then the labmda will live "forever".
There's a corner case to add to xyz's description:
If a lambda only accesses a second lambda that in turn accesses properties, you'll get a crash (as of 4.3). The system isn't clever enough to see that the first lambda accesses the object through the second lambda, so it doesn't break the signal connection when the associated object is destroyed.
Example:
# Level_1.gd
func _ready():
var respawn = func(ship_id):
var new_ship: Ship = self.ship_scene.instantiate()
new_ship.ship_id = ship_id
self.add_child(new_ship)
Signals.ship_died.connect(func(ship): respawn.call(ship.ship_id))
In the above code, func(ship): respawn.call(ship.ship_id)
doesn't directly refer to self
, so when the Level_1
instance is destroyed, the Signals.ship_died
connection is not broken. The next time Signals.ship_died
is emitted, the lambda attempts to access respawn
on the destroyed Level_1
instance. This results in
Attempt to call function `<anonymous lambda>(lambda) (Callable)' on a null instance.
The workaround is to make respawn
a method, or to put a dummy reference to self
in the connected lambda.
- Edited
jumpingmechanic It's not about "cleverness" of the system.
The system can only know about the function that is actually connected. If this function is a method associated with an object, then it can be disconnected in the case of that object's destruction. If you connect an object-less lambda (like in your example), then the connection is not associated with any objects. The fact that lambda's code is written in some class, doesn't make it associated with that object. (That's in fact the logical error in your reasoning). That lambda has no concept of self
object and the object holds no reference to that lambda. From the object's point of view, the signal connection to that lambda is as if it was connected to some other object. So it's not that object's responsibility to disconnect the signal connected to the lambda.
Tldr, the "cleverness" you here expected from the system would actually make no sense and is not feasible.
xyz Thanks for explaining!
You mentioned earlier: "If a lambda is accessing any properties it will be magically treated as a method". If the lambda being written in class A
doesn't get any special relationship with the A
instance, what exactly is the magic "upgrade" that happens to the lambda when it refers to self
? (I found that func(): print(self)
gets auto-disconnected, while func(): print(4)
does not.) What if the lambda refers both to the self
and a B
instance - which one becomes the associated object?
- Edited
jumpingmechanic The object association is stored with every Callable
object. You can get it via Callable::get_object()
. If you use the keyword self
inside lambda body it will get associated with the object running the script (i.e. the object self
refers to in that context). Otherwise it won't get associated with any object and get_object()
will just return a reference to the Script
object its code is written in.
So it doesn't matter what object(s) the lambda is referencing. What matters is the usage of self
keyword.
var s = self
var a: Callable = func(): print(self)
var b: Callable = func(): print(Node.new())
var c: Callable = func(): print(s)
print(a.get_object())
print(b.get_object())
print(c.get_object())
xyz Thanks, callable c
not being associated with self
really makes the point clear. So only lambdas that use the keyword self
, or implicitly use it by referencing a member without prefixing self.
, get auto-disconnected.
- Edited
jumpingmechanic Yes, because only those lambdas store the object reference, and the object keeps them in its internal list of incoming connections along with connections to its regular methods.
This can also be verified. The following will print only one of the lambdas as object's incoming connection:
ready.connect(func(): pass)
ready.connect(func(): self)
for c in get_incoming_connections():
if c.signal == ready:
print(c.callable)
So when the object is about to be deleted, it goes through the list of its incoming connections and disconnects the callables found in that list from their corresponding signals.
You can also verify this by looking at the Object
class destructor in engine's source code. From Object::~Object()
in object.cpp.
// Disconnect signals that connect to this object.
while (connections.size()) {
Connection c = connections.front()->get();
Object *obj = c.callable.get_object();
bool disconnected = false;
if (likely(obj)) {
disconnected = c.signal.get_object()->_disconnect(c.signal.get_name(), c.callable, true);
}
if (unlikely(!disconnected)) {
// If the disconnect has failed, abandon the connection to avoid getting trapped in an infinite loop here.
connections.pop_front();
}
}
Note that it calls Callable::get_object()
: