Skip to content

Commit aaf74b3

Browse files
authored
Merge pull request #965 from godot-rust/feature/local-callable
`Callable::from_local_fn()` + `Callable::from_sync_fn()`
2 parents 289f5df + 9656eb2 commit aaf74b3

File tree

6 files changed

+230
-41
lines changed

6 files changed

+230
-41
lines changed

godot-core/src/builtin/callable.rs

+94-27
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
use godot_ffi as sys;
99

10-
use crate::builtin::{inner, StringName, Variant, VariantArray};
10+
use crate::builtin::{inner, GString, StringName, Variant, VariantArray};
1111
use crate::classes::Object;
12-
use crate::meta::{CallContext, GodotType, ToGodot};
12+
use crate::meta::{AsArg, CallContext, GodotType, ToGodot};
1313
use crate::obj::bounds::DynMemory;
1414
use crate::obj::Bounds;
1515
use crate::obj::{Gd, GodotClass, InstanceId};
@@ -84,43 +84,75 @@ impl Callable {
8484
}
8585
}
8686

87-
/// Create a callable from a Rust function or closure.
87+
/// Create callable from **single-threaded** Rust function or closure.
8888
///
8989
/// `name` is used for the string representation of the closure, which helps debugging.
9090
///
91-
/// Callables created through multiple `from_fn()` calls are never equal, even if they refer to the same function. If you want to use
92-
/// equality, either clone an existing `Callable` instance, or define your own `PartialEq` impl with [`Callable::from_custom`].
91+
/// This constructor only allows the callable to be invoked from the same thread as creating it. If you need to invoke it from any thread,
92+
/// use [`from_sync_fn`][Self::from_sync_fn] instead (requires crate feature `experimental-threads`; only enable if really needed).
93+
#[cfg(since_api = "4.2")]
94+
pub fn from_local_fn<F, S>(name: S, rust_function: F) -> Self
95+
where
96+
F: 'static + FnMut(&[&Variant]) -> Result<Variant, ()>,
97+
S: AsArg<GString>,
98+
{
99+
meta::arg_into_owned!(name);
100+
101+
Self::from_fn_wrapper(FnWrapper {
102+
rust_function,
103+
name,
104+
thread_id: Some(std::thread::current().id()),
105+
})
106+
}
107+
108+
/// Create callable from **thread-safe** Rust function or closure.
109+
///
110+
/// `name` is used for the string representation of the closure, which helps debugging.
111+
///
112+
/// This constructor requires `Send` + `Sync` bound and allows the callable to be invoked from any thread. If you guarantee that you invoke
113+
/// it from the same thread as creating it, use [`from_local_fn`][Self::from_local_fn] instead.
114+
///
115+
/// Callables created through multiple `from_local_fn` or `from_sync_fn()` calls are never equal, even if they refer to the same function.
116+
/// If you want to use equality, either clone an existing `Callable` instance, or define your own `PartialEq` impl with
117+
/// [`Callable::from_custom`].
93118
///
94119
/// # Example
95120
/// ```no_run
96121
/// # use godot::prelude::*;
97-
/// let callable = Callable::from_fn("sum", |args: &[&Variant]| {
122+
/// let callable = Callable::from_sync_fn("sum", |args: &[&Variant]| {
98123
/// let sum: i32 = args.iter().map(|arg| arg.to::<i32>()).sum();
99124
/// Ok(sum.to_variant())
100125
/// });
101126
/// ```
102127
#[cfg(since_api = "4.2")]
103-
pub fn from_fn<F, S>(name: S, rust_function: F) -> Self
128+
#[cfg(feature = "experimental-threads")]
129+
pub fn from_sync_fn<F, S>(name: S, rust_function: F) -> Self
104130
where
105131
F: 'static + Send + Sync + FnMut(&[&Variant]) -> Result<Variant, ()>,
106-
S: Into<crate::builtin::GString>,
132+
S: AsArg<GString>,
107133
{
108-
let userdata = CallableUserdata {
109-
inner: FnWrapper {
110-
rust_function,
111-
name: name.into(),
112-
},
113-
};
134+
meta::arg_into_owned!(name);
114135

115-
let info = CallableCustomInfo {
116-
callable_userdata: Box::into_raw(Box::new(userdata)) as *mut std::ffi::c_void,
117-
call_func: Some(rust_callable_call_fn::<F>),
118-
free_func: Some(rust_callable_destroy::<FnWrapper<F>>),
119-
to_string_func: Some(rust_callable_to_string_named::<F>),
120-
..Self::default_callable_custom_info()
121-
};
136+
Self::from_fn_wrapper(FnWrapper {
137+
rust_function,
138+
name,
139+
thread_id: None,
140+
})
141+
}
122142

123-
Self::from_custom_info(info)
143+
#[deprecated = "Now split into from_local_fn (single-threaded) and from_sync_fn (multi-threaded)."]
144+
#[cfg(since_api = "4.2")]
145+
pub fn from_fn<F, S>(name: S, rust_function: F) -> Self
146+
where
147+
F: 'static + Send + Sync + FnMut(&[&Variant]) -> Result<Variant, ()>,
148+
S: Into<GString>,
149+
{
150+
// Do not call from_sync_fn() since that is feature-gated, but this isn't due to compatibility.
151+
Self::from_fn_wrapper(FnWrapper {
152+
rust_function,
153+
name: name.into(),
154+
thread_id: None,
155+
})
124156
}
125157

126158
/// Create a highly configurable callable from Rust.
@@ -146,6 +178,24 @@ impl Callable {
146178
Self::from_custom_info(info)
147179
}
148180

181+
#[cfg(since_api = "4.2")]
182+
fn from_fn_wrapper<F>(inner: FnWrapper<F>) -> Self
183+
where
184+
F: FnMut(&[&Variant]) -> Result<Variant, ()>,
185+
{
186+
let userdata = CallableUserdata { inner };
187+
188+
let info = CallableCustomInfo {
189+
callable_userdata: Box::into_raw(Box::new(userdata)) as *mut std::ffi::c_void,
190+
call_func: Some(rust_callable_call_fn::<F>),
191+
free_func: Some(rust_callable_destroy::<FnWrapper<F>>),
192+
to_string_func: Some(rust_callable_to_string_named::<F>),
193+
..Self::default_callable_custom_info()
194+
};
195+
196+
Self::from_custom_info(info)
197+
}
198+
149199
#[cfg(since_api = "4.2")]
150200
fn from_custom_info(mut info: CallableCustomInfo) -> Callable {
151201
// SAFETY: callable_custom_create() is a valid way of creating callables.
@@ -330,7 +380,7 @@ unsafe impl GodotFfi for Callable {
330380
}
331381
}
332382

333-
crate::meta::impl_godot_as_self!(Callable);
383+
meta::impl_godot_as_self!(Callable);
334384

335385
impl fmt::Debug for Callable {
336386
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -365,6 +415,7 @@ mod custom_callable {
365415
use super::*;
366416
use crate::builtin::GString;
367417
use std::hash::Hash;
418+
use std::thread::ThreadId;
368419

369420
pub struct CallableUserdata<T> {
370421
pub inner: T,
@@ -380,8 +431,11 @@ mod custom_callable {
380431
}
381432

382433
pub(crate) struct FnWrapper<F> {
383-
pub(crate) rust_function: F,
384-
pub(crate) name: GString,
434+
pub(super) rust_function: F,
435+
pub(super) name: GString,
436+
437+
/// `None` if the callable is multi-threaded ([`Callable::from_sync_fn`]).
438+
pub(super) thread_id: Option<ThreadId>,
385439
}
386440

387441
/// Represents a custom callable object defined in Rust.
@@ -419,7 +473,7 @@ mod custom_callable {
419473
// Get the RustCallable again inside closure so it doesn't have to be UnwindSafe.
420474
let c: &mut C = CallableUserdata::inner_from_raw(callable_userdata);
421475
let result = c.invoke(arg_refs);
422-
crate::meta::varcall_return_checked(result, r_return, r_error);
476+
meta::varcall_return_checked(result, r_return, r_error);
423477
Ok(())
424478
});
425479
}
@@ -444,8 +498,21 @@ mod custom_callable {
444498
crate::private::handle_varcall_panic(&ctx, &mut *r_error, move || {
445499
// Get the FnWrapper again inside closure so the FnMut doesn't have to be UnwindSafe.
446500
let w: &mut FnWrapper<F> = CallableUserdata::inner_from_raw(callable_userdata);
501+
502+
if w.thread_id
503+
.is_some_and(|tid| tid != std::thread::current().id())
504+
{
505+
// NOTE: this panic is currently not propagated to the caller, but results in an error message and Nil return.
506+
// See comments in itest callable_call() for details.
507+
panic!(
508+
"Callable '{}' created with from_local_fn() must be called from the same thread it was created in.\n\
509+
If you need to call it from any thread, use from_sync_fn() instead (requires `experimental-threads` feature).",
510+
w.name
511+
);
512+
}
513+
447514
let result = (w.rust_function)(arg_refs);
448-
crate::meta::varcall_return_checked(result, r_return, r_error);
515+
meta::varcall_return_checked(result, r_return, r_error);
449516
Ok(())
450517
});
451518
}

godot-ffi/src/binding/single_threaded.rs

+17-5
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,23 @@ impl BindingStorage {
156156
"Godot engine not available; make sure you are not calling it from unit/doc tests",
157157
);
158158

159-
assert_eq!(
160-
main_thread_id,
161-
std::thread::current().id(),
162-
"attempted to access binding from different thread than main thread; this is UB - use the \"experimental-threads\" feature."
163-
);
159+
if main_thread_id != std::thread::current().id() {
160+
// If a binding is accessed the first time, this will panic and start unwinding. It can then happen that during unwinding,
161+
// another FFI call happens (e.g. Godot destructor), which would cause immediate abort, swallowing the error message.
162+
// Thus check if a panic is already in progress.
163+
164+
if std::thread::panicking() {
165+
eprintln!(
166+
"ERROR: Attempted to access binding from different thread than main thread; this is UB.\n\
167+
Cannot panic because panic unwind is already in progress. Please check surrounding messages to fix the bug."
168+
);
169+
} else {
170+
panic!(
171+
"attempted to access binding from different thread than main thread; \
172+
this is UB - use the \"experimental-threads\" feature."
173+
)
174+
};
175+
}
164176
}
165177

166178
// SAFETY: This function can only be called when the binding is initialized and from the main thread, so we know that it's initialized.

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ fn array_binary_search_custom() {
494494

495495
#[cfg(since_api = "4.2")]
496496
fn backwards_sort_callable() -> Callable {
497-
Callable::from_fn("sort backwards", |args: &[&Variant]| {
497+
Callable::from_local_fn("sort backwards", |args: &[&Variant]| {
498498
let res = args[0].to::<i32>() > args[1].to::<i32>();
499499
Ok(res.to_variant())
500500
})

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

+75-7
Original file line numberDiff line numberDiff line change
@@ -169,16 +169,84 @@ impl CallableRefcountTest {
169169
#[cfg(since_api = "4.2")]
170170
pub mod custom_callable {
171171
use super::*;
172-
use crate::framework::assert_eq_self;
172+
use crate::framework::{assert_eq_self, quick_thread, ThreadCrosser};
173173
use godot::builtin::{Dictionary, RustCallable};
174+
use godot::sys;
174175
use godot::sys::GdextBuild;
175176
use std::fmt;
176177
use std::hash::Hash;
177178
use std::sync::{Arc, Mutex};
178179

179180
#[itest]
180-
fn callable_from_fn() {
181-
let callable = Callable::from_fn("sum", sum);
181+
fn callable_from_local_fn() {
182+
let callable = Callable::from_local_fn("sum", sum);
183+
184+
assert!(callable.is_valid());
185+
assert!(!callable.is_null());
186+
assert!(callable.is_custom());
187+
assert!(callable.object().is_none());
188+
189+
let sum1 = callable.callv(&varray![1, 2, 4, 8]);
190+
assert_eq!(sum1, 15.to_variant());
191+
192+
// Important to test 0 arguments, as the FFI call passes a null pointer for the argument array.
193+
let sum2 = callable.callv(&varray![]);
194+
assert_eq!(sum2, 0.to_variant());
195+
}
196+
197+
// Without this feature, any access to the global binding from another thread fails; so the from_local_fn() cannot be tested in isolation.
198+
#[itest]
199+
fn callable_from_local_fn_crossthread() {
200+
// This static is a workaround for not being able to propagate failed `Callable` invocations as panics.
201+
// See note in itest callable_call() for further info.
202+
static GLOBAL: sys::Global<i32> = sys::Global::default();
203+
204+
let callable = Callable::from_local_fn("change_global", |_args| {
205+
*GLOBAL.lock() = 777;
206+
Ok(Variant::nil())
207+
});
208+
209+
// Note that Callable itself isn't Sync/Send, so we have to transfer it unsafely.
210+
// Godot may pass it to another thread though without `unsafe`.
211+
let crosser = ThreadCrosser::new(callable);
212+
213+
// Create separate thread and ensure calling fails.
214+
// Why expect_panic for (single-threaded && Debug) but not (multi-threaded || Release) mode:
215+
// - Check is only enabled in Debug, not Release.
216+
// - We currently can't catch panics from Callable invocations, see above. True for both single/multi-threaded.
217+
// - In single-threaded mode, there's an FFI access check which panics as soon as another thread is invoked. *This* panics.
218+
// - In multi-threaded, we need to observe the effect instead (see below).
219+
220+
if !cfg!(feature = "experimental-threads") && cfg!(debug_assertions) {
221+
// Single-threaded and Debug.
222+
crate::framework::expect_panic(
223+
"Callable created with from_local_fn() must panic when invoked on other thread",
224+
|| {
225+
quick_thread(|| {
226+
let callable = unsafe { crosser.extract() };
227+
callable.callv(&varray![5]);
228+
});
229+
},
230+
);
231+
} else {
232+
// Multi-threaded OR Release.
233+
quick_thread(|| {
234+
let callable = unsafe { crosser.extract() };
235+
callable.callv(&varray![5]);
236+
});
237+
}
238+
239+
assert_eq!(
240+
*GLOBAL.lock(),
241+
0,
242+
"Callable created with from_local_fn() must not run when invoked on other thread"
243+
);
244+
}
245+
246+
#[itest]
247+
#[cfg(feature = "experimental-threads")]
248+
fn callable_from_sync_fn() {
249+
let callable = Callable::from_sync_fn("sum", sum);
182250

183251
assert!(callable.is_valid());
184252
assert!(!callable.is_null());
@@ -199,16 +267,16 @@ pub mod custom_callable {
199267
#[itest]
200268
fn callable_custom_with_err() {
201269
let callable_with_err =
202-
Callable::from_fn("on_error_doesnt_crash", |_args: &[&Variant]| Err(()));
270+
Callable::from_local_fn("on_error_doesnt_crash", |_args: &[&Variant]| Err(()));
203271
// Errors in Godot, but should not crash.
204272
assert_eq!(callable_with_err.callv(&varray![]), Variant::nil());
205273
}
206274

207275
#[itest]
208276
fn callable_from_fn_eq() {
209-
let a = Callable::from_fn("sum", sum);
277+
let a = Callable::from_local_fn("sum", sum);
210278
let b = a.clone();
211-
let c = Callable::from_fn("sum", sum);
279+
let c = Callable::from_local_fn("sum", sum);
212280

213281
assert_eq!(a, b, "same function, same instance -> equal");
214282
assert_ne!(a, c, "same function, different instance -> not equal");
@@ -312,7 +380,7 @@ pub mod custom_callable {
312380
fn callable_callv_panic_from_fn() {
313381
let received = Arc::new(AtomicU32::new(0));
314382
let received_callable = received.clone();
315-
let callable = Callable::from_fn("test", move |_args| {
383+
let callable = Callable::from_local_fn("test", move |_args| {
316384
panic!("TEST: {}", received_callable.fetch_add(1, Ordering::SeqCst))
317385
});
318386

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ mod custom_callable {
247247
}
248248

249249
fn connect_signal_panic_from_fn(received: Arc<AtomicU32>) -> Callable {
250-
Callable::from_fn("test", move |_args| {
250+
Callable::from_local_fn("test", move |_args| {
251251
panic!("TEST: {}", received.fetch_add(1, Ordering::SeqCst))
252252
})
253253
}

0 commit comments

Comments
 (0)