Skip to content

Commit 454eb73

Browse files
authored
Merge pull request #1304 from godot-rust/feature/element-type
Add `ElementType`; expose it in arrays and dictionaries
2 parents c18ac14 + d9a1469 commit 454eb73

File tree

13 files changed

+672
-161
lines changed

13 files changed

+672
-161
lines changed

godot-codegen/src/generator/central_files.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ pub fn make_sys_central_code(api: &ExtensionApi) -> TokenStream {
3838
// This will need refactoring if VariantType is changed to a real enum.
3939
#[doc(hidden)]
4040
pub fn from_sys(enumerator: crate::GDExtensionVariantType) -> Self {
41+
#[allow(clippy::unnecessary_cast)] // on Windows already i32.
4142
Self { ord: enumerator as i32 }
4243
}
4344

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

Lines changed: 79 additions & 42 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::OnceCell;
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,9 @@ 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+
cached_element_type: OnceCell<ElementType>,
144148
}
145149

146150
/// Guard that can only call immutable methods on the array.
@@ -183,6 +187,7 @@ impl<T: ArrayElement> Array<T> {
183187
Self {
184188
opaque,
185189
_phantom: PhantomData,
190+
cached_element_type: OnceCell::new(),
186191
}
187192
}
188193

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

504509
// 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() }
510+
let result = unsafe { duplicate.assume_type() };
511+
result.with_cache(self)
506512
}
507513

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

518524
// 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() }
525+
let result = unsafe { duplicate.assume_type() };
526+
result.with_cache(self)
520527
}
521528

522529
/// Returns a sub-range `begin..end`, as a new array.
@@ -571,8 +578,9 @@ impl<T: ArrayElement> Array<T> {
571578
.slice(to_i64(begin), to_i64(end), step.try_into().unwrap(), deep)
572579
};
573580

574-
// SAFETY: slice() returns a typed array with the same type as Self
575-
unsafe { subarray.assume_type() }
581+
// SAFETY: slice() returns a typed array with the same type as Self.
582+
let result = unsafe { subarray.assume_type() };
583+
result.with_cache(self)
576584
}
577585

578586
/// Returns an iterator over the elements of the `Array`. Note that this takes the array
@@ -956,21 +964,22 @@ impl<T: ArrayElement> Array<T> {
956964
std::mem::transmute::<&Array<T>, &Array<U>>(self)
957965
}
958966

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

964973
// If any element is not convertible, this will return an error.
965974
for elem in canonical_array.iter_shared() {
966975
elem.try_to::<T>().map_err(|_err| {
967976
FromGodotError::BadArrayTypeInt {
968-
expected: self.type_info(),
977+
expected_int_type: std::any::type_name::<T>(),
969978
value: elem
970979
.try_to::<i64>()
971980
.expect("origin must be i64 compatible; this is a bug"),
972981
}
973-
.into_error(self.clone())
982+
.into_error(self.clone()) // Context info about array, not element.
974983
})?;
975984
}
976985

@@ -979,73 +988,91 @@ impl<T: ArrayElement> Array<T> {
979988

980989
// No-op in Release. Avoids O(n) conversion checks, but still panics on access.
981990
#[cfg(not(debug_assertions))]
982-
pub(crate) fn debug_validate_elements(&self) -> Result<(), ConvertError> {
991+
pub(crate) fn debug_validate_int_elements(&self) -> Result<(), ConvertError> {
983992
Ok(())
984993
}
985994

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-
}
995+
/// Returns the runtime element type information for this array.
996+
///
997+
/// The result is generally cached, so feel free to call this method repeatedly.
998+
///
999+
/// # Panics (Debug)
1000+
/// In the astronomically rare case where another extension in Godot modifies an array's type (which godot-rust already cached as `Untyped`)
1001+
/// via C function `array_set_typed`, thus leading to incorrect cache values. Such bad practice of not typing arrays immediately on
1002+
/// construction is not supported, and will not be checked in Release mode.
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.
10211042
///
10221043
/// # Safety
1023-
///
10241044
/// Must only be called once, directly after creation.
10251045
unsafe fn init_inner_type(&mut self) {
10261046
debug_assert!(self.is_empty());
1027-
debug_assert!(!self.type_info().is_typed());
1047+
debug_assert!(
1048+
self.cached_element_type.get().is_none(),
1049+
"init_inner_type() called twice"
1050+
);
1051+
1052+
// Immediately set cache to static type.
1053+
let elem_ty = ElementType::of::<T>();
1054+
let _ = self.cached_element_type.set(elem_ty);
10281055

1029-
let type_info = ArrayTypeInfo::of::<T>();
1030-
if type_info.is_typed() {
1056+
if elem_ty.is_typed() {
10311057
let script = Variant::nil();
10321058

10331059
// A bit contrived because empty StringName is lazy-initialized but must also remain valid.
10341060
#[allow(unused_assignments)]
10351061
let mut empty_string_name = None;
1036-
let class_name = if let Some(class_name) = &type_info.class_name {
1062+
let class_name = if let Some(class_name) = elem_ty.class_name() {
10371063
class_name.string_sys()
10381064
} else {
10391065
empty_string_name = Some(StringName::default());
10401066
// as_ref() crucial here -- otherwise the StringName is dropped.
10411067
empty_string_name.as_ref().unwrap().string_sys()
10421068
};
10431069

1044-
// SAFETY: The array is a newly created empty untyped array.
1070+
// SAFETY: Valid pointers are passed in.
1071+
// Relevant for correctness, not safety: the array is a newly created, empty, untyped array.
10451072
unsafe {
10461073
interface_fn!(array_set_typed)(
10471074
self.sys_mut(),
1048-
type_info.variant_type.sys(),
1075+
elem_ty.variant_type().sys(),
10491076
class_name, // must be empty if variant_type != OBJECT.
10501077
script.var_sys(),
10511078
);
@@ -1059,11 +1086,12 @@ impl<T: ArrayElement> Array<T> {
10591086
/// Should be used only in scenarios where the caller can guarantee that the resulting array will have the correct type,
10601087
/// or when an incorrect Rust type is acceptable (passing raw arrays to Godot FFI).
10611088
unsafe fn clone_unchecked(&self) -> Self {
1062-
Self::new_with_uninit(|self_ptr| {
1089+
let result = Self::new_with_uninit(|self_ptr| {
10631090
let ctor = sys::builtin_fn!(array_construct_copy);
10641091
let args = [self.sys()];
10651092
ctor(self_ptr, args.as_ptr());
1066-
})
1093+
});
1094+
result.with_cache(self)
10671095
}
10681096

10691097
/// Whether this array is untyped and holds `Variant` elements (compile-time check).
@@ -1073,6 +1101,15 @@ impl<T: ArrayElement> Array<T> {
10731101
fn has_variant_t() -> bool {
10741102
element_variant_type::<T>() == VariantType::NIL
10751103
}
1104+
1105+
/// Execute a function that creates a new Array, transferring cached element type if available.
1106+
///
1107+
/// This is a convenience helper for methods that create new Array instances and want to preserve
1108+
/// cached type information to avoid redundant FFI calls.
1109+
fn with_cache(self, source: &Self) -> Self {
1110+
ElementType::transfer_cache(&source.cached_element_type, &self.cached_element_type);
1111+
self
1112+
}
10761113
}
10771114

10781115
#[test]

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

Lines changed: 69 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::OnceCell;
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,21 @@ 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+
cached_key_type: OnceCell<ElementType>,
87+
88+
/// Lazily computed and cached element type information for the value type.
89+
cached_value_type: OnceCell<ElementType>,
8390
}
8491

8592
impl Dictionary {
8693
fn from_opaque(opaque: OpaqueDictionary) -> Self {
87-
Self { opaque }
94+
Self {
95+
opaque,
96+
cached_key_type: OnceCell::new(),
97+
cached_value_type: OnceCell::new(),
98+
}
8899
}
89100

90101
/// Constructs an empty `Dictionary`.
@@ -318,7 +329,7 @@ impl Dictionary {
318329
///
319330
/// _Godot equivalent: `dict.duplicate(true)`_
320331
pub fn duplicate_deep(&self) -> Self {
321-
self.as_inner().duplicate(true)
332+
self.as_inner().duplicate(true).with_cache(self)
322333
}
323334

324335
/// Shallow copy, copying elements but sharing nested collections.
@@ -331,7 +342,7 @@ impl Dictionary {
331342
///
332343
/// _Godot equivalent: `dict.duplicate(false)`_
333344
pub fn duplicate_shallow(&self) -> Self {
334-
self.as_inner().duplicate(false)
345+
self.as_inner().duplicate(false).with_cache(self)
335346
}
336347

337348
/// Returns an iterator over the key-value pairs of the `Dictionary`.
@@ -402,6 +413,46 @@ impl Dictionary {
402413
);
403414
}
404415

416+
/// Returns the runtime element type information for keys in this dictionary.
417+
///
418+
/// Provides information about Godot typed dictionaries, even though godot-rust currently doesn't implement generics for those.
419+
///
420+
/// The result is generally cached, so feel free to call this method repeatedly.
421+
///
422+
/// # Panics (Debug)
423+
/// In the astronomically rare case where another extension in Godot modifies a dictionary's key type (which godot-rust already cached as `Untyped`)
424+
/// via C function `dictionary_set_typed`, thus leading to incorrect cache values. Such bad practice of not typing dictionaries immediately on
425+
/// construction is not supported, and will not be checked in Release mode.
426+
#[cfg(since_api = "4.4")]
427+
pub fn key_element_type(&self) -> ElementType {
428+
ElementType::get_or_compute_cached(
429+
&self.cached_key_type,
430+
|| self.as_inner().get_typed_key_builtin(),
431+
|| self.as_inner().get_typed_key_class_name(),
432+
|| self.as_inner().get_typed_key_script(),
433+
)
434+
}
435+
436+
/// Returns the runtime element type information for values in this dictionary.
437+
///
438+
/// Provides information about Godot typed dictionaries, even though godot-rust currently doesn't implement generics for those.
439+
///
440+
/// The result is generally cached, so feel free to call this method repeatedly.
441+
///
442+
/// # Panics (Debug)
443+
/// In the astronomically rare case where another extension in Godot modifies a dictionary's value type (which godot-rust already cached as `Untyped`)
444+
/// via C function `dictionary_set_typed`, thus leading to incorrect cache values. Such bad practice of not typing dictionaries immediately on
445+
/// construction is not supported, and will not be checked in Release mode.
446+
#[cfg(since_api = "4.4")]
447+
pub fn value_element_type(&self) -> ElementType {
448+
ElementType::get_or_compute_cached(
449+
&self.cached_value_type,
450+
|| self.as_inner().get_typed_value_builtin(),
451+
|| self.as_inner().get_typed_value_class_name(),
452+
|| self.as_inner().get_typed_value_script(),
453+
)
454+
}
455+
405456
#[doc(hidden)]
406457
pub fn as_inner(&self) -> inner::InnerDictionary<'_> {
407458
inner::InnerDictionary::from_outer(self)
@@ -417,6 +468,17 @@ impl Dictionary {
417468
// SAFETY: accessing an unknown key _mutably_ creates that entry in the dictionary, with value `NIL`.
418469
unsafe { interface_fn!(dictionary_operator_index)(self.sys_mut(), key.var_sys()) }
419470
}
471+
472+
/// Execute a function that creates a new Dictionary, transferring cached element types if available.
473+
///
474+
/// This is a convenience helper for methods that create new Dictionary instances and want to preserve
475+
/// cached type information to avoid redundant FFI calls.
476+
fn with_cache(self, source: &Self) -> Self {
477+
// Transfer both key and value type caches independently
478+
ElementType::transfer_cache(&source.cached_key_type, &self.cached_key_type);
479+
ElementType::transfer_cache(&source.cached_value_type, &self.cached_value_type);
480+
self
481+
}
420482
}
421483

422484
// ----------------------------------------------------------------------------------------------------------------------------------------------
@@ -477,13 +539,14 @@ impl fmt::Display for Dictionary {
477539
impl Clone for Dictionary {
478540
fn clone(&self) -> Self {
479541
// SAFETY: `self` is a valid dictionary, since we have a reference that keeps it alive.
480-
unsafe {
542+
let result = unsafe {
481543
Self::new_with_uninit(|self_ptr| {
482544
let ctor = sys::builtin_fn!(dictionary_construct_copy);
483545
let args = [self.sys()];
484546
ctor(self_ptr, args.as_ptr());
485547
})
486-
}
548+
};
549+
result.with_cache(self)
487550
}
488551
}
489552

0 commit comments

Comments
 (0)