Skip to content

Commit db2bdce

Browse files
authored
Merge pull request #1223 from Yarwin/add-bound-callable
Callables linked to objects; let Godot auto-disconnect signals
2 parents d272741 + 7844f77 commit db2bdce

File tree

6 files changed

+112
-30
lines changed

6 files changed

+112
-30
lines changed

godot-core/src/builtin/callable.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,32 @@ impl Callable {
152152
rust_function,
153153
name,
154154
thread_id: Some(std::thread::current().id()),
155+
linked_obj_id: None,
156+
})
157+
}
158+
159+
/// Creates a new callable linked to the given object from **single-threaded** Rust function or closure.
160+
///
161+
/// `name` is used for the string representation of the closure, which helps with debugging.
162+
///
163+
/// Such a callable will be automatically invalidated by Godot when a linked object is freed.
164+
/// Prefer using [`Gd::linked_callable()`] instead.
165+
///
166+
/// If you need a callable which can live indefinitely use [`Callable::from_local_fn()`].
167+
#[cfg(since_api = "4.2")]
168+
pub fn from_linked_fn<F, T, S>(name: S, linked_object: &Gd<T>, rust_function: F) -> Self
169+
where
170+
T: GodotClass,
171+
F: 'static + FnMut(&[&Variant]) -> Result<Variant, ()>,
172+
S: meta::AsArg<GString>,
173+
{
174+
meta::arg_into_owned!(name);
175+
176+
Self::from_fn_wrapper(FnWrapper {
177+
rust_function,
178+
name,
179+
thread_id: Some(std::thread::current().id()),
180+
linked_obj_id: Some(linked_object.instance_id()),
155181
})
156182
}
157183

@@ -168,6 +194,7 @@ impl Callable {
168194
rust_function,
169195
name,
170196
thread_id: Some(std::thread::current().id()),
197+
linked_obj_id: None,
171198
});
172199

173200
callable_usage(&callable)
@@ -205,6 +232,7 @@ impl Callable {
205232
rust_function,
206233
name,
207234
thread_id: None,
235+
linked_obj_id: None,
208236
})
209237
}
210238

@@ -238,9 +266,12 @@ impl Callable {
238266
where
239267
F: FnMut(&[&Variant]) -> Result<Variant, ()>,
240268
{
269+
let object_id = inner.linked_object_id();
270+
241271
let userdata = CallableUserdata { inner };
242272

243273
let info = CallableCustomInfo {
274+
object_id,
244275
callable_userdata: Box::into_raw(Box::new(userdata)) as *mut std::ffi::c_void,
245276
call_func: Some(rust_callable_call_fn::<F>),
246277
free_func: Some(rust_callable_destroy::<FnWrapper<F>>),
@@ -493,6 +524,7 @@ pub use custom_callable::RustCallable;
493524
mod custom_callable {
494525
use super::*;
495526
use crate::builtin::GString;
527+
use godot_ffi::GDObjectInstanceID;
496528
use std::hash::Hash;
497529
use std::thread::ThreadId;
498530

@@ -515,6 +547,14 @@ mod custom_callable {
515547

516548
/// `None` if the callable is multi-threaded ([`Callable::from_sync_fn`]).
517549
pub(super) thread_id: Option<ThreadId>,
550+
/// `None` if callable is not linked with any object.
551+
pub(super) linked_obj_id: Option<InstanceId>,
552+
}
553+
554+
impl<F> FnWrapper<F> {
555+
pub(crate) fn linked_object_id(&self) -> GDObjectInstanceID {
556+
self.linked_obj_id.map(InstanceId::to_u64).unwrap_or(0)
557+
}
518558
}
519559

520560
/// Represents a custom callable object defined in Rust.

godot-core/src/obj/gd.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use std::ops::{Deref, DerefMut};
1111
use godot_ffi as sys;
1212
use sys::{static_assert_eq_size_align, SysPtr as _};
1313

14-
use crate::builtin::{Callable, NodePath, StringName, Variant};
14+
use crate::builtin::{Callable, GString, NodePath, StringName, Variant};
1515
use crate::meta::error::{ConvertError, FromFfiError};
1616
use crate::meta::{
1717
ArrayElement, AsArg, ByRef, CallContext, ClassName, CowArg, FromGodot, GodotConvert, GodotType,
@@ -494,6 +494,21 @@ impl<T: GodotClass> Gd<T> {
494494
Callable::from_object_method(self, method_name)
495495
}
496496

497+
/// Creates a new callable linked to the given object from **single-threaded** Rust function or closure.
498+
/// This is shorter syntax for [`Callable::from_linked_fn()`].
499+
///
500+
/// `name` is used for the string representation of the closure, which helps with debugging.
501+
///
502+
/// Such a callable will be automatically invalidated by Godot when a linked Object is freed.
503+
/// If you need a Callable which can live indefinitely use [`Callable::from_local_fn()`].
504+
#[cfg(since_api = "4.2")]
505+
pub fn linked_callable<F>(&self, method_name: impl AsArg<GString>, rust_function: F) -> Callable
506+
where
507+
F: 'static + FnMut(&[&Variant]) -> Result<Variant, ()>,
508+
{
509+
Callable::from_linked_fn(method_name, self, rust_function)
510+
}
511+
497512
pub(crate) unsafe fn from_obj_sys_or_none(
498513
ptr: sys::GDExtensionObjectPtr,
499514
) -> Result<Self, ConvertError> {

godot-core/src/registry/signal/connect_builder.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
*/
77

88
use super::{make_callable_name, make_godot_fn};
9-
use crate::builtin::{Callable, GString, Variant};
9+
#[cfg(feature = "experimental-threads")]
10+
use crate::builtin::Callable;
11+
use crate::builtin::{GString, Variant};
1012
use crate::classes::object::ConnectFlags;
1113
use crate::meta;
1214
use crate::meta::InParamTuple;
@@ -125,13 +127,14 @@ where
125127
fn inner_connect_godot_fn<F>(
126128
self,
127129
godot_fn: impl FnMut(&[&Variant]) -> Result<Variant, ()> + 'static,
130+
bound: &Gd<impl GodotClass>,
128131
) -> ConnectHandle {
129132
let callable_name = match &self.data.callable_name {
130133
Some(user_provided_name) => user_provided_name,
131134
None => &make_callable_name::<F>(),
132135
};
133136

134-
let callable = Callable::from_local_fn(callable_name, godot_fn);
137+
let callable = bound.linked_callable(callable_name, godot_fn);
135138
self.parent_sig
136139
.inner_connect_untyped(callable, self.data.connect_flags)
137140
}
@@ -165,7 +168,8 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> ConnectBuilder<'_, '_, C, Ps> {
165168
.call((), args);
166169
});
167170

168-
self.inner_connect_godot_fn::<F>(godot_fn)
171+
let bound = self.parent_sig.receiver_object();
172+
self.inner_connect_godot_fn::<F>(godot_fn, &bound)
169173
}
170174

171175
/// Connect a method with `&mut self` as the first parameter (user classes only).
@@ -191,7 +195,8 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> ConnectBuilder<'_, '_, C, Ps> {
191195
.call(&mut *guard, args);
192196
});
193197

194-
self.inner_connect_godot_fn::<F>(godot_fn)
198+
let bound = self.parent_sig.receiver_object();
199+
self.inner_connect_godot_fn::<F>(godot_fn, &bound)
195200
}
196201

197202
/// Connect a method with `&mut Gd<Self>` as the first parameter (user + engine classes).
@@ -208,14 +213,15 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> ConnectBuilder<'_, '_, C, Ps> {
208213
for<'c_rcv> IndirectSignalReceiver<'c_rcv, Gd<C>, Ps, F>: From<&'c_rcv mut F>,
209214
{
210215
let gd = self.parent_sig.receiver_object();
216+
let bound = gd.clone();
211217

212218
let godot_fn = make_godot_fn(move |args| {
213219
IndirectSignalReceiver::from(&mut function)
214220
.function()
215221
.call(gd.clone(), args);
216222
});
217223

218-
self.inner_connect_godot_fn::<F>(godot_fn)
224+
self.inner_connect_godot_fn::<F>(godot_fn, &bound)
219225
}
220226

221227
/// Connect a method with any `&mut OtherC` as the first parameter (user classes only).
@@ -250,7 +256,7 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> ConnectBuilder<'_, '_, C, Ps> {
250256
.call(&mut *guard, args);
251257
});
252258

253-
self.inner_connect_godot_fn::<F>(godot_fn)
259+
self.inner_connect_godot_fn::<F>(godot_fn, &object.to_signal_obj())
254260
}
255261

256262
/// Connect a method with any `&mut Gd<OtherC>` as the first parameter (user + engine classes).
@@ -282,7 +288,7 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> ConnectBuilder<'_, '_, C, Ps> {
282288
.call(gd.clone(), args);
283289
});
284290

285-
self.inner_connect_godot_fn::<F>(godot_fn)
291+
self.inner_connect_godot_fn::<F>(godot_fn, &object.to_signal_obj())
286292
}
287293

288294
/// Connect to this signal using a thread-safe function, allows the signal to be called across threads.

godot-core/src/registry/signal/typed_signal.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,16 +132,19 @@ impl<'c, C: WithSignals, Ps: meta::ParamTuple> TypedSignal<'c, C, Ps> {
132132
});
133133
}
134134

135-
/// Directly connect a Rust callable `godot_fn`, with a name based on `F`.
135+
/// Directly connect a Rust callable `godot_fn`, with a name based on `F` bound to given object.
136+
///
137+
/// Signal will be automatically disconnected by Godot after bound object will be freed.
136138
///
137139
/// This exists as a shorthand for the connect methods on [`TypedSignal`] and avoids the generic instantiation of the full-blown
138140
/// type state builder for simple + common connections, thus hopefully being a tiny bit lighter on compile times.
139141
fn inner_connect_godot_fn<F>(
140142
&self,
141143
godot_fn: impl FnMut(&[&Variant]) -> Result<Variant, ()> + 'static,
144+
bound: &Gd<impl GodotClass>,
142145
) -> ConnectHandle {
143146
let callable_name = make_callable_name::<F>();
144-
let callable = Callable::from_local_fn(&callable_name, godot_fn);
147+
let callable = bound.linked_callable(&callable_name, godot_fn);
145148
self.inner_connect_untyped(callable, None)
146149
}
147150

@@ -197,7 +200,7 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> TypedSignal<'_, C, Ps> {
197200
.call((), args);
198201
});
199202

200-
self.inner_connect_godot_fn::<F>(godot_fn)
203+
self.inner_connect_godot_fn::<F>(godot_fn, &self.receiver_object())
201204
}
202205

203206
/// Connect a method (member function) with `&mut self` as the first parameter.
@@ -219,7 +222,7 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> TypedSignal<'_, C, Ps> {
219222
.call(target_mut, args);
220223
});
221224

222-
self.inner_connect_godot_fn::<F>(godot_fn)
225+
self.inner_connect_godot_fn::<F>(godot_fn, &self.receiver_object())
223226
}
224227

225228
/// Connect a method (member function) with any `&mut OtherC` as the first parameter, where
@@ -254,6 +257,6 @@ impl<C: WithSignals, Ps: InParamTuple + 'static> TypedSignal<'_, C, Ps> {
254257
.call(target_mut, args);
255258
});
256259

257-
self.inner_connect_godot_fn::<F>(godot_fn)
260+
self.inner_connect_godot_fn::<F>(godot_fn, &object.to_signal_obj())
258261
}
259262
}

itest/rust/src/builtin_tests/containers/signal_disconnect_test.rs

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -154,12 +154,12 @@ fn handle_recognizes_direct_object_disconnect() {
154154

155155
#[itest]
156156
fn test_handle_after_freeing_broadcaster() {
157-
test_freed_nodes(true);
157+
test_freed_nodes_handles(true);
158158
}
159159

160160
#[itest]
161161
fn test_handle_after_freeing_receiver() {
162-
test_freed_nodes(false);
162+
test_freed_nodes_handles(false);
163163
}
164164

165165
// Helper functions:
@@ -233,7 +233,7 @@ fn test_handle_recognizes_non_valid_state(disconnect_function: impl FnOnce(&mut
233233
obj.free();
234234
}
235235

236-
fn test_freed_nodes(free_broadcaster_first: bool) {
236+
fn test_freed_nodes_handles(free_broadcaster_first: bool) {
237237
let broadcaster = SignalDisc::new_alloc();
238238
let receiver = SignalDisc::new_alloc();
239239

@@ -251,21 +251,13 @@ fn test_freed_nodes(free_broadcaster_first: bool) {
251251
};
252252

253253
// Free one of the nodes, and check if the handle thinks the objects are connected.
254-
// If the broadcaster is freed, its signals to other objects is implicitly freed as well. Thus, the handle should not be connected.
255-
// If the receiver is freed, the connection between the valid broadcaster and the non-valid freed object still exists.
256-
// In the latter case, the connection can - and probably should - be freed with disconnect().
254+
// In both cases godot runtime should handle disconnecting the signals.
257255
to_free.free();
258-
let is_connected = handle.is_connected();
259-
assert_ne!(
260-
free_broadcaster_first, is_connected,
261-
"Handle should only return false if broadcasting is freed!"
262-
);
263-
264-
// It should be possible to disconnect a connection between a valid broadcaster and a non-valid receiver.
265-
// If the connection is not valid (e.g. because of freed broadcaster), calling disconnect() should panic.
266-
if is_connected {
267-
handle.disconnect();
268-
} else {
256+
assert!(!handle.is_connected());
257+
258+
// Calling disconnect() on already disconnected handle should panic in the Debug mode.
259+
// Otherwise, in release mode, the error will happen in Godot runtime.
260+
if cfg!(debug_assertions) {
269261
expect_panic("Disconnected invalid handle!", || {
270262
handle.disconnect();
271263
});

itest/rust/src/builtin_tests/containers/signal_test.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ fn signal_symbols_internal() {
5555
internal.connect_signals_internal(tracker.clone());
5656
drop(internal);
5757

58+
// Make sure that connection has been properly registered by Godot.
59+
assert!(!emitter.get_incoming_connections().is_empty());
60+
5861
emitter.bind_mut().emit_signals_internal();
5962

6063
// Check that closure is invoked.
@@ -146,6 +149,29 @@ fn signal_symbols_complex_emit() {
146149
emitter.free();
147150
}
148151

152+
#[cfg(since_api = "4.2")]
153+
#[itest]
154+
fn signal_receiver_auto_disconnect() {
155+
let emitter = Emitter::new_alloc();
156+
let sig = emitter.signals().signal_int();
157+
158+
let receiver = Receiver::new_alloc();
159+
sig.connect_other(&receiver, Receiver::receive_int_mut);
160+
161+
let outgoing_connections = emitter.get_signal_connection_list("signal_int");
162+
let incoming_connections = receiver.get_incoming_connections();
163+
164+
assert_eq!(incoming_connections.len(), 1);
165+
assert_eq!(incoming_connections, outgoing_connections);
166+
167+
receiver.free();
168+
169+
// Should be auto-disconnected by Godot.
170+
let outgoing_connections = emitter.get_signal_connection_list("signal_int");
171+
assert!(outgoing_connections.is_empty());
172+
emitter.free();
173+
}
174+
149175
// "External" means connect/emit happens from outside the class, via Gd::signals().
150176
#[cfg(since_api = "4.2")]
151177
#[itest]

0 commit comments

Comments
 (0)