Skip to content

Commit 66466df

Browse files
committed
Improve Array/Dictionary element type caching
1 parent 144ce14 commit 66466df

File tree

8 files changed

+471
-90
lines changed

8 files changed

+471
-90
lines changed

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

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

8+
use std::cell::Cell;
89
use std::marker::PhantomData;
910
use std::{cmp, fmt};
1011

@@ -15,7 +16,7 @@ use crate::builtin::*;
1516
use crate::meta;
1617
use crate::meta::error::{ConvertError, FromGodotError, FromVariantError};
1718
use crate::meta::{
18-
element_godot_type_name, element_variant_type, ArrayElement, ArrayTypeInfo, AsArg, ClassName,
19+
element_godot_type_name, element_variant_type, ArrayElement, AsArg, ClassName, ElementType,
1920
ExtVariantType, FromGodot, GodotConvert, GodotFfiVariant, GodotType, PropertyHintInfo, RefArg,
2021
ToGodot,
2122
};
@@ -141,6 +142,12 @@ pub struct Array<T: ArrayElement> {
141142
// Safety Invariant: The type of all values in `opaque` matches the type `T`.
142143
opaque: sys::types::OpaqueArray,
143144
_phantom: PhantomData<T>,
145+
146+
/// Lazily computed and cached element type information.
147+
///
148+
/// `ElementType::Untyped` means either "not yet queried" or "queried but array was untyped". Since GDScript can call
149+
/// `set_type()` at any time, we must re-query FFI whenever cached value is `Untyped`.
150+
cached_element_type: Cell<ElementType>,
144151
}
145152

146153
/// Guard that can only call immutable methods on the array.
@@ -183,6 +190,7 @@ impl<T: ArrayElement> Array<T> {
183190
Self {
184191
opaque,
185192
_phantom: PhantomData,
193+
cached_element_type: Cell::new(ElementType::Untyped),
186194
}
187195
}
188196

@@ -502,7 +510,8 @@ impl<T: ArrayElement> Array<T> {
502510
let duplicate: VariantArray = unsafe { self.as_inner().duplicate(false) };
503511

504512
// SAFETY: duplicate() returns a typed array with the same type as Self, and all values are taken from `self` so have the right type.
505-
unsafe { duplicate.assume_type() }
513+
let result = unsafe { duplicate.assume_type() };
514+
result.with_cache(self)
506515
}
507516

508517
/// Returns a deep copy of the array. All nested arrays and dictionaries are duplicated and
@@ -516,7 +525,8 @@ impl<T: ArrayElement> Array<T> {
516525
let duplicate: VariantArray = unsafe { self.as_inner().duplicate(true) };
517526

518527
// SAFETY: duplicate() returns a typed array with the same type as Self, and all values are taken from `self` so have the right type.
519-
unsafe { duplicate.assume_type() }
528+
let result = unsafe { duplicate.assume_type() };
529+
result.with_cache(self)
520530
}
521531

522532
/// Returns a sub-range `begin..end`, as a new array.
@@ -571,8 +581,9 @@ impl<T: ArrayElement> Array<T> {
571581
.slice(to_i64(begin), to_i64(end), step.try_into().unwrap(), deep)
572582
};
573583

574-
// SAFETY: slice() returns a typed array with the same type as Self
575-
unsafe { subarray.assume_type() }
584+
// SAFETY: slice() returns a typed array with the same type as Self.
585+
let result = unsafe { subarray.assume_type() };
586+
result.with_cache(self)
576587
}
577588

578589
/// Returns an iterator over the elements of the `Array`. Note that this takes the array
@@ -956,21 +967,22 @@ impl<T: ArrayElement> Array<T> {
956967
std::mem::transmute::<&Array<T>, &Array<U>>(self)
957968
}
958969

970+
/// Validates that all elements in this array can be converted to integers of type `T`.
959971
#[cfg(debug_assertions)]
960-
pub(crate) fn debug_validate_elements(&self) -> Result<(), ConvertError> {
972+
pub(crate) fn debug_validate_int_elements(&self) -> Result<(), ConvertError> {
961973
// SAFETY: every element is internally represented as Variant.
962974
let canonical_array = unsafe { self.assume_type_ref::<Variant>() };
963975

964976
// If any element is not convertible, this will return an error.
965977
for elem in canonical_array.iter_shared() {
966978
elem.try_to::<T>().map_err(|_err| {
967979
FromGodotError::BadArrayTypeInt {
968-
expected: self.type_info(),
980+
expected_int_type: std::any::type_name::<T>(),
969981
value: elem
970982
.try_to::<i64>()
971983
.expect("origin must be i64 compatible; this is a bug"),
972984
}
973-
.into_error(self.clone())
985+
.into_error(self.clone()) // Context info about array, not element.
974986
})?;
975987
}
976988

@@ -979,42 +991,51 @@ impl<T: ArrayElement> Array<T> {
979991

980992
// No-op in Release. Avoids O(n) conversion checks, but still panics on access.
981993
#[cfg(not(debug_assertions))]
982-
pub(crate) fn debug_validate_elements(&self) -> Result<(), ConvertError> {
994+
pub(crate) fn debug_validate_int_elements(&self) -> Result<(), ConvertError> {
983995
Ok(())
984996
}
985997

986-
/// Returns the runtime type info of this array.
987-
fn type_info(&self) -> ArrayTypeInfo {
988-
let variant_type = VariantType::from_sys(
989-
self.as_inner().get_typed_builtin() as sys::GDExtensionVariantType
990-
);
991-
992-
let class_name = if variant_type == VariantType::OBJECT {
993-
Some(self.as_inner().get_typed_class_name())
994-
} else {
995-
None
996-
};
997-
998-
ArrayTypeInfo {
999-
variant_type,
1000-
class_name,
1001-
}
998+
/// Returns the runtime element type information for this array.
999+
///
1000+
/// The result is cached when the array is typed. If the array is untyped, this method
1001+
/// will always re-query Godot's FFI since GDScript may call `set_type()` at any time.
1002+
/// Repeated calls on typed arrays will not result in multiple Godot FFI roundtrips.
1003+
pub fn element_type(&self) -> ElementType {
1004+
ElementType::get_or_compute_cached(
1005+
&self.cached_element_type,
1006+
|| self.as_inner().get_typed_builtin(),
1007+
|| self.as_inner().get_typed_class_name(),
1008+
|| self.as_inner().get_typed_script(),
1009+
)
10021010
}
10031011

10041012
/// Checks that the inner array has the correct type set on it for storing elements of type `T`.
10051013
fn with_checked_type(self) -> Result<Self, ConvertError> {
1006-
let self_ty = self.type_info();
1007-
let target_ty = ArrayTypeInfo::of::<T>();
1014+
let self_ty = self.element_type();
1015+
let target_ty = ElementType::of::<T>();
10081016

1017+
// Exact match: check successful.
10091018
if self_ty == target_ty {
1010-
Ok(self)
1011-
} else {
1012-
Err(FromGodotError::BadArrayType {
1013-
expected: target_ty,
1014-
actual: self_ty,
1019+
return Ok(self);
1020+
}
1021+
1022+
// Check if script class (runtime) matches its native base class (compile-time).
1023+
// This allows an Array[Enemy] from GDScript to be used as Array<Gd<RefCounted>> in Rust.
1024+
if let (ElementType::ScriptClass(_), ElementType::Class(expected_class)) =
1025+
(&self_ty, &target_ty)
1026+
{
1027+
if let Some(actual_base_class) = self_ty.class_name() {
1028+
if actual_base_class == *expected_class {
1029+
return Ok(self);
1030+
}
10151031
}
1016-
.into_error(self))
10171032
}
1033+
1034+
Err(FromGodotError::BadArrayType {
1035+
expected: target_ty,
1036+
actual: self_ty,
1037+
}
1038+
.into_error(self))
10181039
}
10191040

10201041
/// Sets the type of the inner array.
@@ -1024,28 +1045,29 @@ impl<T: ArrayElement> Array<T> {
10241045
/// Must only be called once, directly after creation.
10251046
unsafe fn init_inner_type(&mut self) {
10261047
debug_assert!(self.is_empty());
1027-
debug_assert!(!self.type_info().is_typed());
1048+
debug_assert!(!self.element_type().is_typed());
10281049

1029-
let type_info = ArrayTypeInfo::of::<T>();
1030-
if type_info.is_typed() {
1050+
let elem_ty = ElementType::of::<T>();
1051+
if elem_ty.is_typed() {
10311052
let script = Variant::nil();
10321053

10331054
// A bit contrived because empty StringName is lazy-initialized but must also remain valid.
10341055
#[allow(unused_assignments)]
10351056
let mut empty_string_name = None;
1036-
let class_name = if let Some(class_name) = &type_info.class_name {
1057+
let class_name = if let Some(class_name) = elem_ty.class_name() {
10371058
class_name.string_sys()
10381059
} else {
10391060
empty_string_name = Some(StringName::default());
10401061
// as_ref() crucial here -- otherwise the StringName is dropped.
10411062
empty_string_name.as_ref().unwrap().string_sys()
10421063
};
10431064

1044-
// SAFETY: The array is a newly created empty untyped array.
1065+
// SAFETY: Valid pointers are passed in.
1066+
// Relevant for correctness, not safety: the array is a newly created, empty, untyped array.
10451067
unsafe {
10461068
interface_fn!(array_set_typed)(
10471069
self.sys_mut(),
1048-
type_info.variant_type.sys(),
1070+
elem_ty.variant_type().sys(),
10491071
class_name, // must be empty if variant_type != OBJECT.
10501072
script.var_sys(),
10511073
);
@@ -1059,11 +1081,12 @@ impl<T: ArrayElement> Array<T> {
10591081
/// Should be used only in scenarios where the caller can guarantee that the resulting array will have the correct type,
10601082
/// or when an incorrect Rust type is acceptable (passing raw arrays to Godot FFI).
10611083
unsafe fn clone_unchecked(&self) -> Self {
1062-
Self::new_with_uninit(|self_ptr| {
1084+
let result = Self::new_with_uninit(|self_ptr| {
10631085
let ctor = sys::builtin_fn!(array_construct_copy);
10641086
let args = [self.sys()];
10651087
ctor(self_ptr, args.as_ptr());
1066-
})
1088+
});
1089+
result.with_cache(self)
10671090
}
10681091

10691092
/// Whether this array is untyped and holds `Variant` elements (compile-time check).
@@ -1073,6 +1096,15 @@ impl<T: ArrayElement> Array<T> {
10731096
fn has_variant_t() -> bool {
10741097
element_variant_type::<T>() == VariantType::NIL
10751098
}
1099+
1100+
/// Execute a function that creates a new Array, transferring cached element type if available.
1101+
///
1102+
/// This is a convenience helper for methods that create new Array instances and want to preserve
1103+
/// cached type information to avoid redundant FFI calls.
1104+
fn with_cache(self, source: &Self) -> Self {
1105+
ElementType::transfer_cache(&source.cached_element_type, &self.cached_element_type);
1106+
self
1107+
}
10761108
}
10771109

10781110
#[test]

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

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

8+
use std::cell::Cell;
89
use std::marker::PhantomData;
910
use std::{fmt, ptr};
1011

@@ -13,7 +14,7 @@ use sys::types::OpaqueDictionary;
1314
use sys::{ffi_methods, interface_fn, GodotFfi};
1415

1516
use crate::builtin::{inner, Variant, VariantArray};
16-
use crate::meta::{ExtVariantType, FromGodot, ToGodot};
17+
use crate::meta::{ElementType, ExtVariantType, FromGodot, ToGodot};
1718

1819
/// Godot's `Dictionary` type.
1920
///
@@ -80,11 +81,27 @@ use crate::meta::{ExtVariantType, FromGodot, ToGodot};
8081
/// [`Dictionary` (stable)](https://docs.godotengine.org/en/stable/classes/class_dictionary.html)
8182
pub struct Dictionary {
8283
opaque: OpaqueDictionary,
84+
85+
/// Lazily computed and cached element type information for the key type.
86+
///
87+
/// `ElementType::Untyped` means either "not yet queried" or "queried but array was untyped". Since GDScript can call
88+
/// `set_type()` at any time, we must re-query FFI whenever cached value is `Untyped`.
89+
cached_key_type: Cell<ElementType>,
90+
91+
/// Lazily computed and cached element type information for the key type.
92+
///
93+
/// `ElementType::Untyped` means either "not yet queried" or "queried but array was untyped". Since GDScript can call
94+
/// `set_type()` at any time, we must re-query FFI whenever cached value is `Untyped`.
95+
cached_value_type: Cell<ElementType>,
8396
}
8497

8598
impl Dictionary {
8699
fn from_opaque(opaque: OpaqueDictionary) -> Self {
87-
Self { opaque }
100+
Self {
101+
opaque,
102+
cached_key_type: Cell::new(ElementType::Untyped),
103+
cached_value_type: Cell::new(ElementType::Untyped),
104+
}
88105
}
89106

90107
/// Constructs an empty `Dictionary`.
@@ -318,7 +335,7 @@ impl Dictionary {
318335
///
319336
/// _Godot equivalent: `dict.duplicate(true)`_
320337
pub fn duplicate_deep(&self) -> Self {
321-
self.as_inner().duplicate(true)
338+
self.as_inner().duplicate(true).with_cache(self)
322339
}
323340

324341
/// Shallow copy, copying elements but sharing nested collections.
@@ -331,7 +348,7 @@ impl Dictionary {
331348
///
332349
/// _Godot equivalent: `dict.duplicate(false)`_
333350
pub fn duplicate_shallow(&self) -> Self {
334-
self.as_inner().duplicate(false)
351+
self.as_inner().duplicate(false).with_cache(self)
335352
}
336353

337354
/// Returns an iterator over the key-value pairs of the `Dictionary`.
@@ -402,6 +419,39 @@ impl Dictionary {
402419
);
403420
}
404421

422+
/// Returns the runtime element type information for keys in this dictionary.
423+
///
424+
/// Provides information about Godot typed dictionaries, even though godot-rust currently doesn't implement generics for those.
425+
///
426+
/// The caching strategy is best-effort. In the current implementation, untyped dictionaries are always re-queried over FFI, since
427+
/// it's possible to call `set_key_type()`/`set_value_type()` in Godot. Once typed, the value is cached.
428+
#[cfg(since_api = "4.4")]
429+
pub fn key_element_type(&self) -> ElementType {
430+
ElementType::get_or_compute_cached(
431+
&self.cached_key_type,
432+
|| self.as_inner().get_typed_key_builtin(),
433+
|| self.as_inner().get_typed_key_class_name(),
434+
|| self.as_inner().get_typed_key_script(),
435+
)
436+
}
437+
438+
/// Returns the runtime element type information for values in this dictionary.
439+
///
440+
/// Provides information about Godot typed dictionaries, even though godot-rust currently doesn't implement generics for those.
441+
///
442+
/// The result is cached when the dictionary values are typed. If the values are untyped, this method
443+
/// will always re-query Godot's FFI since GDScript may call `set_type()` at any time.
444+
/// Repeated calls on typed dictionaries will not result in multiple Godot FFI roundtrips.
445+
#[cfg(since_api = "4.4")]
446+
pub fn value_element_type(&self) -> ElementType {
447+
ElementType::get_or_compute_cached(
448+
&self.cached_value_type,
449+
|| self.as_inner().get_typed_value_builtin(),
450+
|| self.as_inner().get_typed_value_class_name(),
451+
|| self.as_inner().get_typed_value_script(),
452+
)
453+
}
454+
405455
#[doc(hidden)]
406456
pub fn as_inner(&self) -> inner::InnerDictionary<'_> {
407457
inner::InnerDictionary::from_outer(self)
@@ -417,6 +467,17 @@ impl Dictionary {
417467
// SAFETY: accessing an unknown key _mutably_ creates that entry in the dictionary, with value `NIL`.
418468
unsafe { interface_fn!(dictionary_operator_index)(self.sys_mut(), key.var_sys()) }
419469
}
470+
471+
/// Execute a function that creates a new Dictionary, transferring cached element types if available.
472+
///
473+
/// This is a convenience helper for methods that create new Dictionary instances and want to preserve
474+
/// cached type information to avoid redundant FFI calls.
475+
fn with_cache(self, source: &Self) -> Self {
476+
// Transfer both key and value type caches independently
477+
ElementType::transfer_cache(&source.cached_key_type, &self.cached_key_type);
478+
ElementType::transfer_cache(&source.cached_value_type, &self.cached_value_type);
479+
self
480+
}
420481
}
421482

422483
// ----------------------------------------------------------------------------------------------------------------------------------------------
@@ -477,13 +538,14 @@ impl fmt::Display for Dictionary {
477538
impl Clone for Dictionary {
478539
fn clone(&self) -> Self {
479540
// SAFETY: `self` is a valid dictionary, since we have a reference that keeps it alive.
480-
unsafe {
541+
let result = unsafe {
481542
Self::new_with_uninit(|self_ptr| {
482543
let ctor = sys::builtin_fn!(dictionary_construct_copy);
483544
let args = [self.sys()];
484545
ctor(self_ptr, args.as_ptr());
485546
})
486-
}
547+
};
548+
result.with_cache(self)
487549
}
488550
}
489551

0 commit comments

Comments
 (0)