Skip to content

Commit

Permalink
Merge pull request #989 from godot-rust/feature/static-callable
Browse files Browse the repository at this point in the history
Support static functions in `Callable`
  • Loading branch information
Bromeon authored Dec 24, 2024
2 parents 24db37d + ba03a6d commit 06dcda7
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 13 deletions.
67 changes: 58 additions & 9 deletions godot-core/src/builtin/callable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
use godot_ffi as sys;

use crate::builtin::{inner, GString, StringName, Variant, VariantArray};
use crate::classes::Object;
use crate::meta::{AsArg, CallContext, GodotType, ToGodot};
use crate::classes;
use crate::meta::{GodotType, ToGodot};
use crate::obj::bounds::DynMemory;
use crate::obj::Bounds;
use crate::obj::{Gd, GodotClass, InstanceId};
Expand Down Expand Up @@ -43,7 +43,7 @@ impl Callable {
Self { opaque }
}

/// Create a callable for the method `object::method_name`.
/// Create a callable for the non-static method `object.method_name`.
///
/// See also [`Gd::callable()`].
///
Expand All @@ -65,6 +65,55 @@ impl Callable {
}
}

/// Create a callable for the static method `class_name::function` (single-threaded).
///
/// Allows you to call static functions through `Callable`.
///
/// Note that due to varying support across different engine versions, the resulting `Callable` has unspecified behavior for
/// methods such as [`method_name()`][Self::method_name], [`object()`][Self::object], [`object_id()`][Self::object_id] or
/// [`get_argument_count()`][Self::arg_len] among others. It is recommended to only use this for calling the function.
pub fn from_local_static(
class_name: impl meta::AsArg<StringName>,
function_name: impl meta::AsArg<StringName>,
) -> Self {
meta::arg_into_owned!(class_name);
meta::arg_into_owned!(function_name);

// Modern implementation: use ClassDb::class_call_static().
#[cfg(since_api = "4.4")]
{
let callable_name = format!("{class_name}.{function_name}");

Self::from_local_fn(&callable_name, move |args| {
let args = args.iter().cloned().cloned().collect::<Vec<_>>();

let result: Variant = classes::ClassDb::singleton().class_call_static(
&class_name,
&function_name,
args.as_slice(),
);
Ok(result)
})
}

// Polyfill for <= Godot 4.3: use GDScript expressions.
#[cfg(before_api = "4.4")]
{
use crate::obj::NewGd;

let code = format!(
"static func __callable():\n\treturn Callable({class_name}, \"{function_name}\")"
);

let mut script = classes::GDScript::new_gd();
script.set_source_code(&code);
script.reload();

let callable = script.call("__callable", &[]);
callable.to()
}
}

#[cfg(since_api = "4.2")]
fn default_callable_custom_info() -> CallableCustomInfo {
CallableCustomInfo {
Expand Down Expand Up @@ -94,7 +143,7 @@ impl Callable {
pub fn from_local_fn<F, S>(name: S, rust_function: F) -> Self
where
F: 'static + FnMut(&[&Variant]) -> Result<Variant, ()>,
S: AsArg<GString>,
S: meta::AsArg<GString>,
{
meta::arg_into_owned!(name);

Expand Down Expand Up @@ -129,7 +178,7 @@ impl Callable {
pub fn from_sync_fn<F, S>(name: S, rust_function: F) -> Self
where
F: 'static + Send + Sync + FnMut(&[&Variant]) -> Result<Variant, ()>,
S: AsArg<GString>,
S: meta::AsArg<GString>,
{
meta::arg_into_owned!(name);

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

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

crate::private::handle_varcall_panic(&ctx, &mut *r_error, move || {
// Get the FnWrapper again inside closure so the FnMut doesn't have to be UnwindSafe.
Expand Down
35 changes: 31 additions & 4 deletions itest/rust/src/builtin_tests/containers/callable_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ impl CallableTestObj {
fn baz(&self, a: i32, b: GString, c: Array<NodePath>, d: Gd<RefCounted>) -> VariantArray {
varray![a, b, c, d]
}

#[func]
fn static_function(c: i32) -> GString {
c.to_variant().stringify()
}
}

#[itest]
Expand All @@ -63,7 +68,9 @@ fn callable_validity() {
assert!(!Callable::invalid().is_valid());
assert!(Callable::invalid().is_null());
assert!(!Callable::invalid().is_custom());
assert!(Callable::invalid().object().is_none());
assert_eq!(Callable::invalid().object(), None);
assert_eq!(Callable::invalid().object_id(), None);
assert_eq!(Callable::invalid().method_name(), None);
}

#[itest]
Expand All @@ -87,10 +94,30 @@ fn callable_object_method() {
drop(object);
assert_eq!(callable.object_id(), Some(object_id));
assert_eq!(callable.object(), None);
}

assert_eq!(Callable::invalid().object(), None);
assert_eq!(Callable::invalid().object_id(), None);
assert_eq!(Callable::invalid().method_name(), None);
#[itest]
fn callable_static() {
let callable = Callable::from_local_static("CallableTestObj", "static_function");

// Test current behavior in <4.4 and >=4.4. Although our API explicitly leaves it unspecified, we then notice change in implementation.
if cfg!(since_api = "4.4") {
assert_eq!(callable.object(), None);
assert_eq!(callable.object_id(), None);
assert_eq!(callable.method_name(), None);
} else {
assert!(callable.object().is_some());
assert!(callable.object_id().is_some());
assert_eq!(callable.method_name(), Some("static_function".into()));
assert_eq!(callable.to_string(), "GDScriptNativeClass::static_function");
}

// Calling works consistently everywhere.
let result = callable.callv(&varray![12345]);
assert_eq!(result, "12345".to_variant());

#[cfg(since_api = "4.3")]
assert_eq!(callable.arg_len(), 0); // Consistently doesn't work :)
}

#[itest]
Expand Down

0 comments on commit 06dcda7

Please sign in to comment.