Skip to content

Flesh out Callable functions #979

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions godot-codegen/src/special_cases/special_cases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ pub fn is_class_method_deleted(class_name: &TyName, method: &JsonClassMethod, ct
| ("VisualShaderNodeComment", "set_description")
| ("VisualShaderNodeComment", "get_description")
=> true,

// Workaround for methods unexposed in Release mode, see https://github.com/godotengine/godot/pull/100317
// and https://github.com/godotengine/godot/pull/100328.
#[cfg(not(debug_assertions))]
| ("CollisionShape2D", "set_debug_color")
| ("CollisionShape2D", "get_debug_color")
| ("CollisionShape3D", "set_debug_color")
| ("CollisionShape3D", "get_debug_color")
| ("CollisionShape3D", "set_debug_fill_enabled")
| ("CollisionShape3D", "get_debug_fill_enabled") => true,

// Thread APIs
#[cfg(not(feature = "experimental-threads"))]
Expand Down Expand Up @@ -241,6 +251,14 @@ pub fn is_builtin_method_exposed(builtin_ty: &TyName, godot_method_name: &str) -

// NodePath

// Callable
| ("Callable", "call")
| ("Callable", "call_deferred")
| ("Callable", "bind")
| ("Callable", "get_bound_arguments")
| ("Callable", "rpc")
| ("Callable", "rpc_id")

// (add more builtin types below)

=> true, _ => false
Expand Down
15 changes: 15 additions & 0 deletions godot-core/src/builtin/callable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,21 @@ impl Callable {
self.as_inner().is_valid()
}

pub fn unbind(&self, args: usize) -> Callable {
self.as_inner().unbind(args as i64)
}

#[cfg(since_api = "4.3")]
#[doc(alias = "get_argument_count")]
pub fn arg_len(&self) -> usize {
self.as_inner().get_argument_count() as usize
}

#[doc(alias = "get_bound_arguments_count")]
pub fn bound_args_len(&self) -> i64 {
self.as_inner().get_bound_arguments_count()
}

#[doc(hidden)]
pub fn as_inner(&self) -> inner::InnerCallable {
inner::InnerCallable::from_outer(self)
Expand Down
5 changes: 4 additions & 1 deletion godot-core/src/classes/class_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
//! Runtime checks and inspection of Godot classes.

use crate::builtin::{GString, StringName};
#[cfg(debug_assertions)]
use crate::classes::{ClassDb, Object};
use crate::meta::{CallContext, ClassName};
use crate::meta::CallContext;
#[cfg(debug_assertions)]
use crate::meta::ClassName;
use crate::obj::{bounds, Bounds, Gd, GodotClass, InstanceId};
use crate::sys;

Expand Down
2 changes: 2 additions & 0 deletions godot-core/src/meta/error/convert_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ pub(crate) enum FromGodotError {
},

/// Special case of `BadArrayType` where a custom int type such as `i8` cannot hold a dynamic `i64` value.
#[cfg(debug_assertions)]
BadArrayTypeInt { expected: ArrayTypeInfo, value: i64 },

/// InvalidEnum is also used by bitfields.
Expand Down Expand Up @@ -236,6 +237,7 @@ impl fmt::Display for FromGodotError {
"expected array of class {exp_class}, got array of class {act_class}"
)
}
#[cfg(debug_assertions)]
Self::BadArrayTypeInt { expected, value } => {
write!(
f,
Expand Down
5 changes: 4 additions & 1 deletion godot-core/src/private.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ use crate::global::godot_error;
use crate::meta::error::CallError;
use crate::meta::CallContext;
use crate::sys;
use std::sync::{atomic, Arc, Mutex};
use std::sync::atomic;
#[cfg(debug_assertions)]
use std::sync::{Arc, Mutex};
use sys::Global;

// ----------------------------------------------------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -412,6 +414,7 @@ where

#[cfg(not(debug_assertions))]
{
let _ = error_context; // Unused warning.
let msg = extract_panic_message(err);
let msg = format_panic_message(msg);

Expand Down
127 changes: 114 additions & 13 deletions itest/rust/src/builtin_tests/containers/callable_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
*/

use godot::builtin::inner::InnerCallable;
use godot::builtin::{varray, Callable, GString, StringName, Variant};
use godot::classes::{Node2D, Object};
use godot::builtin::{
array, varray, Array, Callable, GString, NodePath, StringName, Variant, VariantArray,
};
use godot::classes::{Node2D, Object, RefCounted};
use godot::init::GdextBuild;
use godot::meta::ToGodot;
use godot::obj::{NewAlloc, NewGd};
use godot::obj::{Gd, NewAlloc, NewGd};
use godot::register::{godot_api, GodotClass};
use std::hash::Hasher;
use std::sync::atomic::{AtomicU32, Ordering};
Expand All @@ -33,6 +36,11 @@ impl CallableTestObj {
fn bar(&self, b: i32) -> GString {
b.to_variant().stringify()
}

#[func]
fn baz(&self, a: i32, b: GString, c: Array<NodePath>, d: Gd<RefCounted>) -> VariantArray {
varray![a, b, c, d]
}
}

#[itest]
Expand Down Expand Up @@ -86,7 +94,7 @@ fn callable_object_method() {
}

#[itest]
fn callable_call() {
fn callable_callv() {
let obj = CallableTestObj::new_gd();
let callable = obj.callable("foo");

Expand All @@ -106,6 +114,31 @@ fn callable_call() {
assert_eq!(Callable::invalid().callv(&varray![1, 2, 3]), Variant::nil());
}

#[cfg(since_api = "4.2")]
#[itest]
fn callable_call() {
let obj = CallableTestObj::new_gd();
let callable = obj.callable("foo");

assert_eq!(obj.bind().value, 0);
callable.call(&[10.to_variant()]);
assert_eq!(obj.bind().value, 10);

// Too many arguments: this call fails, its logic is not applied.
// In the future, panic should be propagated to caller.
callable.call(&[20.to_variant(), 30.to_variant()]);
assert_eq!(obj.bind().value, 10);

// TODO(bromeon): this causes a Rust panic, but since call() is routed to Godot, the panic is handled at the FFI boundary.
// Can there be a way to notify the caller about failed calls like that?
assert_eq!(callable.call(&["string".to_variant()]), Variant::nil());

assert_eq!(
Callable::invalid().call(&[1.to_variant(), 2.to_variant(), 3.to_variant()]),
Variant::nil()
);
}

#[itest]
fn callable_call_return() {
let obj = CallableTestObj::new_gd();
Expand Down Expand Up @@ -151,24 +184,92 @@ fn callable_bindv() {
);
}

// This is also testing that using works at all.
#[itest]
#[cfg(since_api = "4.2")]
fn callable_varargs() {
// TODO: Replace with proper apis instead of using `InnerCallable`.
use godot::builtin::inner;
#[itest]
fn callable_bind() {
let obj = CallableTestObj::new_gd();
let callable = obj.callable("bar");
let inner_callable = inner::InnerCallable::from_outer(&callable);
let callable_bound = inner_callable.bind(&[10.to_variant()]);
let inner_callable_bound = inner::InnerCallable::from_outer(&callable_bound);
let callable_bound = callable.bind(&[10.to_variant()]);

assert_eq!(
inner_callable_bound.call(&[]),
callable_bound.call(&[]),
10.to_variant().stringify().to_variant()
);
}

#[cfg(since_api = "4.2")]
#[itest]
fn callable_unbind() {
let obj = CallableTestObj::new_gd();
let callable = obj.callable("bar");
let callable_unbound = callable.unbind(3);

assert_eq!(
callable_unbound.call(&[
121.to_variant(),
20.to_variant(),
30.to_variant(),
40.to_variant()
]),
121.to_variant().stringify().to_variant()
);
}

#[cfg(since_api = "4.3")]
#[itest]
fn callable_arg_len() {
let obj = CallableTestObj::new_gd();

assert_eq!(obj.callable("foo").arg_len(), 1);
assert_eq!(obj.callable("bar").arg_len(), 1);
assert_eq!(obj.callable("baz").arg_len(), 4);
assert_eq!(obj.callable("foo").unbind(10).arg_len(), 11);
assert_eq!(
obj.callable("baz")
.bind(&[10.to_variant(), "hello".to_variant()])
.arg_len(),
2
);
}

#[itest]
fn callable_bound_args_len() {
let obj = CallableTestObj::new_gd();

assert_eq!(obj.callable("foo").bound_args_len(), 0);
assert_eq!(obj.callable("foo").bindv(&varray![10]).bound_args_len(), 1);
#[cfg(since_api = "4.3")]
assert_eq!(
obj.callable("foo").unbind(28).bound_args_len(),
if GdextBuild::since_api("4.4") { 0 } else { -28 }
);
#[cfg(since_api = "4.3")]
assert_eq!(
obj.callable("foo")
.bindv(&varray![10])
.unbind(5)
.bound_args_len(),
if GdextBuild::since_api("4.4") { 1 } else { -4 }
);
}

#[itest]
fn callable_get_bound_arguments() {
let obj = CallableTestObj::new_gd();

let a: i32 = 10;
let b: &str = "hello!";
let c: Array<NodePath> = array!["my/node/path"];
let d: Gd<RefCounted> = RefCounted::new_gd();

let callable = obj.callable("baz");
let callable_bound = callable.bindv(&varray![a, b, c, d]);

assert_eq!(callable_bound.get_bound_arguments(), varray![a, b, c, d]);
}

// TODO: Add tests for `Callable::rpc` and `Callable::rpc_id`.

// Testing https://github.com/godot-rust/gdext/issues/410

#[derive(GodotClass)]
Expand Down
Loading