Skip to content

Commit 17a4650

Browse files
authored
Merge pull request #1064 from godot-rust/feature/callable-convenience-apis
Callables to builtin methods; `Array::bsearch_by, sort_unstable_by`
2 parents 043cf43 + 44a479c commit 17a4650

File tree

5 files changed

+224
-44
lines changed

5 files changed

+224
-44
lines changed

check.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ function cmd_clippy() {
165165
}
166166

167167
function cmd_klippy() {
168+
# TODO(Rust 1.86): remove `-A clippy::precedence`.
169+
168170
run cargo clippy --fix --all-targets "${extraCargoArgs[@]}" -- \
169171
-D clippy::suspicious \
170172
-D clippy::style \
@@ -173,7 +175,8 @@ function cmd_klippy() {
173175
-D clippy::dbg_macro \
174176
-D clippy::todo \
175177
-D clippy::unimplemented \
176-
-D warnings
178+
-D warnings \
179+
-A clippy::precedence
177180
}
178181

179182
function cmd_test() {

godot-core/src/builtin/callable.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,32 @@ impl Callable {
6464
}
6565
}
6666

67+
/// Create a callable for a method on any [`Variant`].
68+
///
69+
/// Allows to dynamically call methods on builtin types (e.g. `String.md5_text`). Note that Godot method names are used, not Rust ones.
70+
/// If the variant type is `Object`, the behavior will match that of `from_object_method()`.
71+
///
72+
/// If the builtin type does not have the method, the returned callable will be invalid.
73+
///
74+
/// Static builtin methods (e.g. `String.humanize_size`) are not supported in reflection as of Godot 4.4. For static _class_ functions,
75+
/// use [`from_local_static()`][Self::from_local_static] instead.
76+
///
77+
/// _Godot equivalent: `Callable.create(Variant variant, StringName method)`_
78+
#[cfg(since_api = "4.3")]
79+
pub fn from_variant_method<S>(variant: &Variant, method_name: S) -> Self
80+
where
81+
S: meta::AsArg<StringName>,
82+
{
83+
meta::arg_into_ref!(method_name);
84+
inner::InnerCallable::create(variant, method_name)
85+
}
86+
6787
/// Create a callable for the static method `class_name::function` (single-threaded).
6888
///
6989
/// Allows you to call static functions through `Callable`.
7090
///
91+
/// Does not support built-in types (such as `String`), only classes.
92+
///
7193
/// # Compatibility
7294
/// Not available before Godot 4.4. Library versions <0.3 used to provide this, however the polyfill used to emulate it was half-broken
7395
/// (not supporting signals, bind(), method_name(), is_valid(), etc).
@@ -133,6 +155,24 @@ impl Callable {
133155
})
134156
}
135157

158+
#[cfg(since_api = "4.2")]
159+
pub(crate) fn with_scoped_fn<S, F, Fc, R>(name: S, rust_function: F, callable_usage: Fc) -> R
160+
where
161+
S: meta::AsArg<GString>,
162+
F: FnMut(&[&Variant]) -> Result<Variant, ()>,
163+
Fc: FnOnce(&Callable) -> R,
164+
{
165+
meta::arg_into_owned!(name);
166+
167+
let callable = Self::from_fn_wrapper(FnWrapper {
168+
rust_function,
169+
name,
170+
thread_id: Some(std::thread::current().id()),
171+
});
172+
173+
callable_usage(&callable)
174+
}
175+
136176
/// Create callable from **thread-safe** Rust function or closure.
137177
///
138178
/// `name` is used for the string representation of the closure, which helps debugging.

godot-core/src/builtin/collections/array.rs

Lines changed: 100 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
66
*/
77

8-
use std::fmt;
98
use std::marker::PhantomData;
9+
use std::{cmp, fmt};
1010

1111
use crate::builtin::*;
1212
use crate::meta;
@@ -612,37 +612,80 @@ impl<T: ArrayElement> Array<T> {
612612
}
613613
}
614614

615-
/// Finds the index of an existing value in a sorted array using binary search.
616-
/// Equivalent of `bsearch` in GDScript.
615+
/// Finds the index of a value in a sorted array using binary search.
617616
///
618-
/// If the value is not present in the array, returns the insertion index that
619-
/// would maintain sorting order.
617+
/// If the value is not present in the array, returns the insertion index that would maintain sorting order.
620618
///
621-
/// Calling `bsearch` on an unsorted array results in unspecified behavior.
619+
/// Calling `bsearch` on an unsorted array results in unspecified behavior. Consider using `sort()` to ensure the sorting
620+
/// order is compatible with your callable's ordering.
622621
pub fn bsearch(&self, value: impl AsArg<T>) -> usize {
623622
meta::arg_into_ref!(value: T);
624623

625624
to_usize(self.as_inner().bsearch(&value.to_variant(), true))
626625
}
627626

628-
/// Finds the index of an existing value in a sorted array using binary search.
629-
/// Equivalent of `bsearch_custom` in GDScript.
627+
/// Finds the index of a value in a sorted array using binary search, with type-safe custom predicate.
630628
///
631-
/// Takes a `Callable` and uses the return value of it to perform binary search.
629+
/// The comparator function should return an ordering that indicates whether its argument is `Less`, `Equal` or `Greater` the desired value.
630+
/// For example, for an ascending-ordered array, a simple predicate searching for a constant value would be `|elem| elem.cmp(&4)`.
631+
/// See also [`slice::binary_search_by()`].
632632
///
633-
/// If the value is not present in the array, returns the insertion index that
634-
/// would maintain sorting order.
633+
/// If the value is found, returns `Ok(index)` with its index. Otherwise, returns `Err(index)`, where `index` is the insertion index
634+
/// that would maintain sorting order.
635635
///
636-
/// Calling `bsearch_custom` on an unsorted array results in unspecified behavior.
636+
/// Calling `bsearch_by` on an unsorted array results in unspecified behavior. Consider using [`sort_by()`] to ensure
637+
/// the sorting order is compatible with your callable's ordering.
638+
#[cfg(since_api = "4.2")]
639+
pub fn bsearch_by<F>(&self, mut func: F) -> Result<usize, usize>
640+
where
641+
F: FnMut(&T) -> cmp::Ordering + 'static,
642+
{
643+
// Early exit; later code relies on index 0 being present.
644+
if self.is_empty() {
645+
return Err(0);
646+
}
647+
648+
// We need one dummy element of type T, because Godot's bsearch_custom() checks types (so Variant::nil() can't be passed).
649+
// Optimization: roundtrip Variant -> T -> Variant could be avoided, but anyone needing speed would use Rust binary search...
650+
let ignored_value = self.at(0);
651+
let ignored_value = <T as ParamType>::owned_to_arg(ignored_value);
652+
653+
let godot_comparator = |args: &[&Variant]| {
654+
let value = T::from_variant(args[0]);
655+
let is_less = matches!(func(&value), cmp::Ordering::Less);
656+
657+
Ok(is_less.to_variant())
658+
};
659+
660+
let debug_name = std::any::type_name::<F>();
661+
let index = Callable::with_scoped_fn(debug_name, godot_comparator, |pred| {
662+
self.bsearch_custom(ignored_value, pred)
663+
});
664+
665+
if let Some(value_at_index) = self.get(index) {
666+
if func(&value_at_index) == cmp::Ordering::Equal {
667+
return Ok(index);
668+
}
669+
}
670+
671+
Err(index)
672+
}
673+
674+
/// Finds the index of a value in a sorted array using binary search, with `Callable` custom predicate.
675+
///
676+
/// The callable `pred` takes two elements `(a, b)` and should return if `a < b` (strictly less).
677+
/// For a type-safe version, check out [`bsearch_by()`][Self::bsearch_by].
637678
///
638-
/// Consider using `sort_custom()` to ensure the sorting order is compatible with
639-
/// your callable's ordering
640-
pub fn bsearch_custom(&self, value: impl AsArg<T>, func: &Callable) -> usize {
679+
/// If the value is not present in the array, returns the insertion index that would maintain sorting order.
680+
///
681+
/// Calling `bsearch_custom` on an unsorted array results in unspecified behavior. Consider using `sort_custom()` to ensure
682+
/// the sorting order is compatible with your callable's ordering.
683+
pub fn bsearch_custom(&self, value: impl AsArg<T>, pred: &Callable) -> usize {
641684
meta::arg_into_ref!(value: T);
642685

643686
to_usize(
644687
self.as_inner()
645-
.bsearch_custom(&value.to_variant(), func, true),
688+
.bsearch_custom(&value.to_variant(), pred, true),
646689
)
647690
}
648691

@@ -654,20 +697,55 @@ impl<T: ArrayElement> Array<T> {
654697

655698
/// Sorts the array.
656699
///
657-
/// Note: The sorting algorithm used is not [stable](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability).
658-
/// This means that values considered equal may have their order changed when using `sort_unstable`.
700+
/// The sorting algorithm used is not [stable](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability).
701+
/// This means that values considered equal may have their order changed when using `sort_unstable`. For most variant types,
702+
/// this distinction should not matter though.
703+
///
704+
/// _Godot equivalent: `Array.sort()`_
659705
#[doc(alias = "sort")]
660706
pub fn sort_unstable(&mut self) {
661707
// SAFETY: We do not write any values that don't already exist in the array, so all values have the correct type.
662708
unsafe { self.as_inner_mut() }.sort();
663709
}
664710

665-
/// Sorts the array.
711+
/// Sorts the array, using a type-safe comparator.
712+
///
713+
/// The predicate expects two parameters `(a, b)` and should return an ordering relation. For example, simple ascending ordering of the
714+
/// elements themselves would be achieved with `|a, b| a.cmp(b)`.
715+
///
716+
/// The sorting algorithm used is not [stable](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability).
717+
/// This means that values considered equal may have their order changed when using `sort_unstable_by`. For most variant types,
718+
/// this distinction should not matter though.
719+
#[cfg(since_api = "4.2")]
720+
pub fn sort_unstable_by<F>(&mut self, mut func: F)
721+
where
722+
F: FnMut(&T, &T) -> cmp::Ordering,
723+
{
724+
let godot_comparator = |args: &[&Variant]| {
725+
let lhs = T::from_variant(args[0]);
726+
let rhs = T::from_variant(args[1]);
727+
let is_less = matches!(func(&lhs, &rhs), cmp::Ordering::Less);
728+
729+
Ok(is_less.to_variant())
730+
};
731+
732+
let debug_name = std::any::type_name::<F>();
733+
Callable::with_scoped_fn(debug_name, godot_comparator, |pred| {
734+
self.sort_unstable_custom(pred)
735+
});
736+
}
737+
738+
/// Sorts the array, using type-unsafe `Callable` comparator.
739+
///
740+
/// For a type-safe variant of this method, use [`sort_unstable_by()`][Self::sort_unstable_by].
741+
///
742+
/// The callable expects two parameters `(lhs, rhs)` and should return a bool `lhs < rhs`.
666743
///
667-
/// Uses the provided `Callable` to determine ordering.
744+
/// The sorting algorithm used is not [stable](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability).
745+
/// This means that values considered equal may have their order changed when using `sort_unstable_custom`.For most variant types,
746+
/// this distinction should not matter though.
668747
///
669-
/// Note: The sorting algorithm used is not [stable](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability).
670-
/// This means that values considered equal may have their order changed when using `sort_unstable_custom`.
748+
/// _Godot equivalent: `Array.sort_custom()`_
671749
#[doc(alias = "sort_custom")]
672750
pub fn sort_unstable_custom(&mut self, func: &Callable) {
673751
// SAFETY: We do not write any values that don't already exist in the array, so all values have the correct type.

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

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -197,17 +197,6 @@ fn array_first_last() {
197197
assert_eq!(empty_array.back(), None);
198198
}
199199

200-
#[itest]
201-
fn array_binary_search() {
202-
let array = array![1, 3];
203-
204-
assert_eq!(array.bsearch(0), 0);
205-
assert_eq!(array.bsearch(1), 0);
206-
assert_eq!(array.bsearch(2), 1);
207-
assert_eq!(array.bsearch(3), 1);
208-
assert_eq!(array.bsearch(4), 2);
209-
}
210-
211200
#[itest]
212201
fn array_find() {
213202
let array = array![1, 2, 1];
@@ -298,13 +287,6 @@ fn array_extend() {
298287
assert_eq!(array, array![1, 2, 3, 4]);
299288
}
300289

301-
#[itest]
302-
fn array_sort() {
303-
let mut array = array![2, 1];
304-
array.sort_unstable();
305-
assert_eq!(array, array![1, 2]);
306-
}
307-
308290
#[itest]
309291
fn array_reverse() {
310292
let mut array = array![1, 2];
@@ -474,18 +456,57 @@ fn array_should_format_with_display() {
474456
assert_eq!(format!("{a}"), "[]");
475457
}
476458

459+
#[itest]
460+
fn array_sort_unstable() {
461+
let mut array = array![2, 1];
462+
array.sort_unstable();
463+
assert_eq!(array, array![1, 2]);
464+
}
465+
477466
#[itest]
478467
#[cfg(since_api = "4.2")]
479-
fn array_sort_custom() {
468+
fn array_sort_unstable_by() {
469+
let mut array: Array<i32> = array![2, 1, 4, 3];
470+
array.sort_unstable_by(|a, b| a.cmp(b));
471+
assert_eq!(array, array![1, 2, 3, 4]);
472+
}
473+
474+
#[itest]
475+
#[cfg(since_api = "4.2")]
476+
fn array_sort_unstable_custom() {
480477
let mut a = array![1, 2, 3, 4];
481478
let func = backwards_sort_callable();
482479
a.sort_unstable_custom(&func);
483480
assert_eq!(a, array![4, 3, 2, 1]);
484481
}
485482

483+
#[itest]
484+
fn array_bsearch() {
485+
let array = array![1, 3];
486+
487+
assert_eq!(array.bsearch(0), 0);
488+
assert_eq!(array.bsearch(1), 0);
489+
assert_eq!(array.bsearch(2), 1);
490+
assert_eq!(array.bsearch(3), 1);
491+
assert_eq!(array.bsearch(4), 2);
492+
}
493+
494+
#[itest]
495+
#[cfg(since_api = "4.2")]
496+
fn array_bsearch_by() {
497+
let a: Array<i32> = array![1, 2, 4, 5];
498+
499+
assert_eq!(a.bsearch_by(|e| e.cmp(&2)), Ok(1));
500+
assert_eq!(a.bsearch_by(|e| e.cmp(&4)), Ok(2));
501+
502+
assert_eq!(a.bsearch_by(|e| e.cmp(&0)), Err(0));
503+
assert_eq!(a.bsearch_by(|e| e.cmp(&3)), Err(2));
504+
assert_eq!(a.bsearch_by(|e| e.cmp(&9)), Err(4));
505+
}
506+
486507
#[itest]
487508
#[cfg(since_api = "4.2")]
488-
fn array_binary_search_custom() {
509+
fn array_bsearch_custom() {
489510
let a = array![5, 4, 2, 1];
490511
let func = backwards_sort_callable();
491512
assert_eq!(a.bsearch_custom(1, &func), 3);

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

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88
use crate::framework::itest;
99
use godot::builtin::{
10-
array, varray, Array, Callable, GString, NodePath, StringName, Variant, VariantArray, Vector2,
10+
array, dict, varray, Array, Callable, Color, GString, NodePath, StringName, Variant,
11+
VariantArray, Vector2,
1112
};
1213
use godot::classes::{Node2D, Object, RefCounted};
1314
use godot::init::GdextBuild;
@@ -97,6 +98,43 @@ fn callable_object_method() {
9798
assert_eq!(callable.object(), None);
9899
}
99100

101+
#[itest]
102+
#[cfg(since_api = "4.3")]
103+
fn callable_variant_method() {
104+
// Dictionary
105+
let dict = dict! { "one": 1, "value": 2 };
106+
let dict_get = Callable::from_variant_method(&dict.to_variant(), "get");
107+
assert_eq!(dict_get.call(&["one".to_variant()]), 1.to_variant());
108+
109+
// GString
110+
let string = GString::from("some string").to_variant();
111+
let string_md5 = Callable::from_variant_method(&string, "md5_text");
112+
assert_eq!(
113+
string_md5.call(&[]),
114+
"5ac749fbeec93607fc28d666be85e73a".to_variant()
115+
);
116+
117+
// Object
118+
let obj = CallableTestObj::new_gd().to_variant();
119+
let obj_stringify = Callable::from_variant_method(&obj, "stringify_int");
120+
assert_eq!(obj_stringify.call(&[10.to_variant()]), "10".to_variant());
121+
122+
// Vector3
123+
let vector = Vector2::new(-1.2, 2.5).to_variant();
124+
let vector_round = Callable::from_variant_method(&vector, "round");
125+
assert_eq!(vector_round.call(&[]), Vector2::new(-1.0, 3.0).to_variant());
126+
127+
// Color
128+
let color = Color::from_rgba8(255, 0, 127, 255).to_variant();
129+
let color_to_html = Callable::from_variant_method(&color, "to_html");
130+
assert_eq!(color_to_html.call(&[]), "ff007fff".to_variant());
131+
132+
// Color - invalid method.
133+
let color = Color::from_rgba8(255, 0, 127, 255).to_variant();
134+
let color_to_html = Callable::from_variant_method(&color, "to_htmI");
135+
assert!(!color_to_html.is_valid());
136+
}
137+
100138
#[itest]
101139
#[cfg(since_api = "4.4")]
102140
fn callable_static() {

0 commit comments

Comments
 (0)