Skip to content

Commit d1a25b3

Browse files
authored
Merge pull request #1029 from arocull/static-callable-test
`Callable::from_local_static()` now requires Godot 4.4+
2 parents ad42a35 + 523552a commit d1a25b3

File tree

4 files changed

+81
-57
lines changed

4 files changed

+81
-57
lines changed

godot-core/src/builtin/callable.rs

+46-38
Original file line numberDiff line numberDiff line change
@@ -68,54 +68,29 @@ impl Callable {
6868
///
6969
/// Allows you to call static functions through `Callable`.
7070
///
71-
/// Note that due to varying support across different engine versions, the resulting `Callable` has unspecified behavior for
72-
/// methods such as [`method_name()`][Self::method_name], [`object()`][Self::object], [`object_id()`][Self::object_id] or
73-
/// [`get_argument_count()`][Self::arg_len] among others. It is recommended to only use this for calling the function.
74-
///
7571
/// # Compatibility
76-
/// Up until and including Godot 4.3, this method has some limitations:
77-
/// - [`is_valid()`][Self::is_valid] will return `false`, even though the call itself succeeds.
78-
/// - You cannot use statics to connect signals to such callables. Use the new typed signal API instead.
72+
/// Not available before Godot 4.4. Library versions <0.3 used to provide this, however the polyfill used to emulate it was half-broken
73+
/// (not supporting signals, bind(), method_name(), is_valid(), etc).
74+
#[cfg(since_api = "4.4")]
7975
pub fn from_local_static(
8076
class_name: impl meta::AsArg<StringName>,
8177
function_name: impl meta::AsArg<StringName>,
8278
) -> Self {
8379
meta::arg_into_owned!(class_name);
8480
meta::arg_into_owned!(function_name);
8581

86-
// Modern implementation: use ClassDb::class_call_static().
87-
#[cfg(since_api = "4.4")]
88-
{
89-
let callable_name = format!("{class_name}.{function_name}");
90-
91-
Self::from_local_fn(&callable_name, move |args| {
92-
let args = args.iter().cloned().cloned().collect::<Vec<_>>();
93-
94-
let result: Variant = classes::ClassDb::singleton().class_call_static(
95-
&class_name,
96-
&function_name,
97-
args.as_slice(),
98-
);
99-
Ok(result)
100-
})
101-
}
82+
let callable_name = format!("{class_name}.{function_name}");
10283

103-
// Polyfill for <= Godot 4.3: use GDScript expressions.
104-
#[cfg(before_api = "4.4")]
105-
{
106-
use crate::obj::NewGd;
84+
Self::from_local_fn(&callable_name, move |args| {
85+
let args = args.iter().cloned().cloned().collect::<Vec<_>>();
10786

108-
let code = format!(
109-
"static func __callable():\n\treturn Callable({class_name}, \"{function_name}\")"
87+
let result: Variant = classes::ClassDb::singleton().class_call_static(
88+
&class_name,
89+
&function_name,
90+
args.as_slice(),
11091
);
111-
112-
let mut script = classes::GDScript::new_gd();
113-
script.set_source_code(&code);
114-
script.reload();
115-
116-
let callable = script.call("__callable", &[]);
117-
callable.to()
118-
}
92+
Ok(result)
93+
})
11994
}
12095

12196
#[cfg(since_api = "4.2")]
@@ -125,7 +100,7 @@ impl Callable {
125100
token: ptr::null_mut(),
126101
object_id: 0,
127102
call_func: None,
128-
is_valid_func: None, // could be customized, but no real use case yet.
103+
is_valid_func: None, // overwritten later.
129104
free_func: None,
130105
hash_func: None,
131106
equal_func: None,
@@ -219,12 +194,14 @@ impl Callable {
219194
let userdata = CallableUserdata { inner: callable };
220195

221196
let info = CallableCustomInfo {
197+
// We could technically associate an object_id with the custom callable. is_valid_func would then check that for validity.
222198
callable_userdata: Box::into_raw(Box::new(userdata)) as *mut std::ffi::c_void,
223199
call_func: Some(rust_callable_call_custom::<C>),
224200
free_func: Some(rust_callable_destroy::<C>),
225201
hash_func: Some(rust_callable_hash::<C>),
226202
equal_func: Some(rust_callable_equal::<C>),
227203
to_string_func: Some(rust_callable_to_string_display::<C>),
204+
is_valid_func: Some(rust_callable_is_valid_custom::<C>),
228205
..Self::default_callable_custom_info()
229206
};
230207

@@ -243,6 +220,7 @@ impl Callable {
243220
call_func: Some(rust_callable_call_fn::<F>),
244221
free_func: Some(rust_callable_destroy::<FnWrapper<F>>),
245222
to_string_func: Some(rust_callable_to_string_named::<F>),
223+
is_valid_func: Some(rust_callable_is_valid),
246224
..Self::default_callable_custom_info()
247225
};
248226

@@ -530,6 +508,17 @@ mod custom_callable {
530508
/// Error handling is mostly needed in case argument number or types mismatch.
531509
#[allow(clippy::result_unit_err)] // TODO remove once there's a clear error type here.
532510
fn invoke(&mut self, args: &[&Variant]) -> Result<Variant, ()>;
511+
512+
// TODO(v0.3): add object_id().
513+
514+
/// Returns whether the callable is considered valid.
515+
///
516+
/// True by default.
517+
///
518+
/// If this Callable stores an object, this method should return whether that object is alive.
519+
fn is_valid(&self) -> bool {
520+
true
521+
}
533522
}
534523

535524
pub unsafe extern "C" fn rust_callable_call_custom<C: RustCallable>(
@@ -641,4 +630,23 @@ mod custom_callable {
641630
w.name.clone().move_into_string_ptr(r_out);
642631
*r_is_valid = sys::conv::SYS_TRUE;
643632
}
633+
634+
// Implementing this is necessary because the default (nullptr) may consider custom callables as invalid in some cases.
635+
pub unsafe extern "C" fn rust_callable_is_valid_custom<C: RustCallable>(
636+
callable_userdata: *mut std::ffi::c_void,
637+
) -> sys::GDExtensionBool {
638+
let w: &mut C = CallableUserdata::inner_from_raw(callable_userdata);
639+
let valid = w.is_valid();
640+
641+
sys::conv::bool_to_sys(valid)
642+
}
643+
644+
// Implementing this is necessary because the default (nullptr) may consider custom callables as invalid in some cases.
645+
pub unsafe extern "C" fn rust_callable_is_valid(
646+
_callable_userdata: *mut std::ffi::c_void,
647+
) -> sys::GDExtensionBool {
648+
// If we had an object (CallableCustomInfo::object_id field), we could check whether that object is alive.
649+
// But since we just take a Rust function/closure, not knowing what happens inside, we assume always valid.
650+
sys::conv::SYS_TRUE
651+
}
644652
}

godot-core/src/builtin/variant/mod.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use crate::builtin::{
99
GString, StringName, VariantArray, VariantDispatch, VariantOperator, VariantType,
1010
};
11-
use crate::meta::error::{ConvertError, ErrorKind, FromVariantError};
11+
use crate::meta::error::ConvertError;
1212
use crate::meta::{arg_into_ref, ArrayElement, AsArg, FromGodot, ToGodot};
1313
use godot_ffi as sys;
1414
use std::{fmt, ptr};
@@ -128,6 +128,7 @@ impl Variant {
128128

129129
#[cfg(before_api = "4.4")]
130130
{
131+
use crate::meta::error::{ErrorKind, FromVariantError};
131132
match self.try_to::<crate::obj::Gd<crate::classes::Object>>() {
132133
Ok(obj) => Some(obj.instance_id_unchecked()),
133134
Err(c)

godot-core/src/meta/error/convert_error.rs

+1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ impl ConvertError {
9292
ErasedConvertError::from(self)
9393
}
9494

95+
#[cfg(before_api = "4.4")]
9596
pub(crate) fn kind(&self) -> &ErrorKind {
9697
&self.kind
9798
}

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

+32-18
Original file line numberDiff line numberDiff line change
@@ -98,27 +98,15 @@ fn callable_object_method() {
9898
}
9999

100100
#[itest]
101+
#[cfg(since_api = "4.4")]
101102
fn callable_static() {
102103
let callable = Callable::from_local_static("CallableTestObj", "concat_array");
103104

104-
// Test current behavior in <4.4 and >=4.4. Although our API explicitly leaves it unspecified, we then notice change in implementation.
105-
if cfg!(since_api = "4.4") {
106-
assert_eq!(callable.object(), None);
107-
assert_eq!(callable.object_id(), None);
108-
assert_eq!(callable.method_name(), None);
109-
assert!(callable.is_custom());
110-
assert!(callable.is_valid());
111-
} else {
112-
assert!(callable.object().is_some());
113-
assert!(callable.object_id().is_some());
114-
assert_eq!(callable.method_name(), Some("concat_array".into()));
115-
assert_eq!(callable.to_string(), "GDScriptNativeClass::concat_array");
116-
assert!(!callable.is_custom());
117-
118-
// Surprisingly false, but call still works (see test below).
119-
// What DOESN'T work is connecting 4.3 static methods to signals via this approach.
120-
assert!(!callable.is_valid());
121-
}
105+
assert_eq!(callable.object(), None);
106+
assert_eq!(callable.object_id(), None);
107+
assert_eq!(callable.method_name(), None);
108+
assert!(callable.is_custom());
109+
assert!(callable.is_valid());
122110

123111
assert!(!callable.is_null());
124112

@@ -138,6 +126,32 @@ fn callable_static() {
138126
assert_eq!(callable.get_argument_count(), 0); // Consistently doesn't work :)
139127
}
140128

129+
// Regression test, see https://github.com/godot-rust/gdext/pull/1029.
130+
#[itest]
131+
#[cfg(since_api = "4.4")]
132+
fn callable_static_bind() {
133+
let callable = Callable::from_local_static("CallableTestObj", "concat_array");
134+
assert!(callable.is_valid());
135+
136+
// Test varying binds to static callables.
137+
// Last 3 of 4 arguments. Within Godot, bound arguments are used in-order AFTER call arguments.
138+
let bindv = callable.bindv(&varray![
139+
"two",
140+
array![&NodePath::from("three/four")],
141+
&RefCounted::new_gd(),
142+
]);
143+
assert!(bindv.is_valid());
144+
145+
assert!(!bindv.to_variant().is_nil());
146+
let args = varray![1];
147+
let bindv_result = bindv.callv(&args);
148+
149+
assert!(!bindv_result.is_nil());
150+
151+
let bind_result_data: VariantArray = bindv_result.to();
152+
assert_eq!(4, bind_result_data.len());
153+
}
154+
141155
#[itest]
142156
fn callable_callv() {
143157
let obj = CallableTestObj::new_gd();

0 commit comments

Comments
 (0)