Skip to content

Commit 06dcda7

Browse files
authored
Merge pull request #989 from godot-rust/feature/static-callable
Support static functions in `Callable`
2 parents 24db37d + ba03a6d commit 06dcda7

File tree

2 files changed

+89
-13
lines changed

2 files changed

+89
-13
lines changed

godot-core/src/builtin/callable.rs

+58-9
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
use godot_ffi as sys;
99

1010
use crate::builtin::{inner, GString, StringName, Variant, VariantArray};
11-
use crate::classes::Object;
12-
use crate::meta::{AsArg, CallContext, GodotType, ToGodot};
11+
use crate::classes;
12+
use crate::meta::{GodotType, ToGodot};
1313
use crate::obj::bounds::DynMemory;
1414
use crate::obj::Bounds;
1515
use crate::obj::{Gd, GodotClass, InstanceId};
@@ -43,7 +43,7 @@ impl Callable {
4343
Self { opaque }
4444
}
4545

46-
/// Create a callable for the method `object::method_name`.
46+
/// Create a callable for the non-static method `object.method_name`.
4747
///
4848
/// See also [`Gd::callable()`].
4949
///
@@ -65,6 +65,55 @@ impl Callable {
6565
}
6666
}
6767

68+
/// Create a callable for the static method `class_name::function` (single-threaded).
69+
///
70+
/// Allows you to call static functions through `Callable`.
71+
///
72+
/// Note that due to varying support across different engine versions, the resulting `Callable` has unspecified behavior for
73+
/// methods such as [`method_name()`][Self::method_name], [`object()`][Self::object], [`object_id()`][Self::object_id] or
74+
/// [`get_argument_count()`][Self::arg_len] among others. It is recommended to only use this for calling the function.
75+
pub fn from_local_static(
76+
class_name: impl meta::AsArg<StringName>,
77+
function_name: impl meta::AsArg<StringName>,
78+
) -> Self {
79+
meta::arg_into_owned!(class_name);
80+
meta::arg_into_owned!(function_name);
81+
82+
// Modern implementation: use ClassDb::class_call_static().
83+
#[cfg(since_api = "4.4")]
84+
{
85+
let callable_name = format!("{class_name}.{function_name}");
86+
87+
Self::from_local_fn(&callable_name, move |args| {
88+
let args = args.iter().cloned().cloned().collect::<Vec<_>>();
89+
90+
let result: Variant = classes::ClassDb::singleton().class_call_static(
91+
&class_name,
92+
&function_name,
93+
args.as_slice(),
94+
);
95+
Ok(result)
96+
})
97+
}
98+
99+
// Polyfill for <= Godot 4.3: use GDScript expressions.
100+
#[cfg(before_api = "4.4")]
101+
{
102+
use crate::obj::NewGd;
103+
104+
let code = format!(
105+
"static func __callable():\n\treturn Callable({class_name}, \"{function_name}\")"
106+
);
107+
108+
let mut script = classes::GDScript::new_gd();
109+
script.set_source_code(&code);
110+
script.reload();
111+
112+
let callable = script.call("__callable", &[]);
113+
callable.to()
114+
}
115+
}
116+
68117
#[cfg(since_api = "4.2")]
69118
fn default_callable_custom_info() -> CallableCustomInfo {
70119
CallableCustomInfo {
@@ -94,7 +143,7 @@ impl Callable {
94143
pub fn from_local_fn<F, S>(name: S, rust_function: F) -> Self
95144
where
96145
F: 'static + FnMut(&[&Variant]) -> Result<Variant, ()>,
97-
S: AsArg<GString>,
146+
S: meta::AsArg<GString>,
98147
{
99148
meta::arg_into_owned!(name);
100149

@@ -129,7 +178,7 @@ impl Callable {
129178
pub fn from_sync_fn<F, S>(name: S, rust_function: F) -> Self
130179
where
131180
F: 'static + Send + Sync + FnMut(&[&Variant]) -> Result<Variant, ()>,
132-
S: AsArg<GString>,
181+
S: meta::AsArg<GString>,
133182
{
134183
meta::arg_into_owned!(name);
135184

@@ -273,11 +322,11 @@ impl Callable {
273322
/// target or not). Also returns `None` if the object is dead. You can differentiate these two cases using [`object_id()`][Self::object_id].
274323
///
275324
/// _Godot equivalent: `get_object`_
276-
pub fn object(&self) -> Option<Gd<Object>> {
325+
pub fn object(&self) -> Option<Gd<classes::Object>> {
277326
// Increment refcount because we're getting a reference, and `InnerCallable::get_object` doesn't
278327
// increment the refcount.
279328
self.as_inner().get_object().map(|mut object| {
280-
<Object as Bounds>::DynMemory::maybe_inc_ref(&mut object.raw);
329+
<classes::Object as Bounds>::DynMemory::maybe_inc_ref(&mut object.raw);
281330
object
282331
})
283332
}
@@ -482,7 +531,7 @@ mod custom_callable {
482531
let c: &C = CallableUserdata::inner_from_raw(callable_userdata);
483532
c.to_string()
484533
};
485-
let ctx = CallContext::custom_callable(name.as_str());
534+
let ctx = meta::CallContext::custom_callable(name.as_str());
486535

487536
crate::private::handle_varcall_panic(&ctx, &mut *r_error, move || {
488537
// Get the RustCallable again inside closure so it doesn't have to be UnwindSafe.
@@ -508,7 +557,7 @@ mod custom_callable {
508557
let w: &FnWrapper<F> = CallableUserdata::inner_from_raw(callable_userdata);
509558
w.name.to_string()
510559
};
511-
let ctx = CallContext::custom_callable(name.as_str());
560+
let ctx = meta::CallContext::custom_callable(name.as_str());
512561

513562
crate::private::handle_varcall_panic(&ctx, &mut *r_error, move || {
514563
// Get the FnWrapper again inside closure so the FnMut doesn't have to be UnwindSafe.

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

+31-4
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ impl CallableTestObj {
4141
fn baz(&self, a: i32, b: GString, c: Array<NodePath>, d: Gd<RefCounted>) -> VariantArray {
4242
varray![a, b, c, d]
4343
}
44+
45+
#[func]
46+
fn static_function(c: i32) -> GString {
47+
c.to_variant().stringify()
48+
}
4449
}
4550

4651
#[itest]
@@ -63,7 +68,9 @@ fn callable_validity() {
6368
assert!(!Callable::invalid().is_valid());
6469
assert!(Callable::invalid().is_null());
6570
assert!(!Callable::invalid().is_custom());
66-
assert!(Callable::invalid().object().is_none());
71+
assert_eq!(Callable::invalid().object(), None);
72+
assert_eq!(Callable::invalid().object_id(), None);
73+
assert_eq!(Callable::invalid().method_name(), None);
6774
}
6875

6976
#[itest]
@@ -87,10 +94,30 @@ fn callable_object_method() {
8794
drop(object);
8895
assert_eq!(callable.object_id(), Some(object_id));
8996
assert_eq!(callable.object(), None);
97+
}
9098

91-
assert_eq!(Callable::invalid().object(), None);
92-
assert_eq!(Callable::invalid().object_id(), None);
93-
assert_eq!(Callable::invalid().method_name(), None);
99+
#[itest]
100+
fn callable_static() {
101+
let callable = Callable::from_local_static("CallableTestObj", "static_function");
102+
103+
// Test current behavior in <4.4 and >=4.4. Although our API explicitly leaves it unspecified, we then notice change in implementation.
104+
if cfg!(since_api = "4.4") {
105+
assert_eq!(callable.object(), None);
106+
assert_eq!(callable.object_id(), None);
107+
assert_eq!(callable.method_name(), None);
108+
} else {
109+
assert!(callable.object().is_some());
110+
assert!(callable.object_id().is_some());
111+
assert_eq!(callable.method_name(), Some("static_function".into()));
112+
assert_eq!(callable.to_string(), "GDScriptNativeClass::static_function");
113+
}
114+
115+
// Calling works consistently everywhere.
116+
let result = callable.callv(&varray![12345]);
117+
assert_eq!(result, "12345".to_variant());
118+
119+
#[cfg(since_api = "4.3")]
120+
assert_eq!(callable.arg_len(), 0); // Consistently doesn't work :)
94121
}
95122

96123
#[itest]

0 commit comments

Comments
 (0)