Skip to content

Commit 3211780

Browse files
authored
Merge pull request #853 from godot-rust/qol/array-element-conversions
Best-effort checks for `Array<Integer>` conversions; fix `Debug` for variants containing typed arrays
2 parents 05446f6 + 970b40b commit 3211780

File tree

15 files changed

+250
-51
lines changed

15 files changed

+250
-51
lines changed

godot-codegen/src/conv/type_conversions.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,9 @@ fn to_rust_type_uncached(full_ty: &GodotTy, ctx: &mut Context) -> RustTy {
217217
} else if let Some(elem_ty) = ty.strip_prefix("typedarray::") {
218218
let rust_elem_ty = to_rust_type(elem_ty, None, ctx);
219219
return if ctx.is_builtin(elem_ty) {
220-
RustTy::BuiltinArray(quote! { Array<#rust_elem_ty> })
220+
RustTy::BuiltinArray {
221+
elem_type: quote! { Array<#rust_elem_ty> },
222+
}
221223
} else {
222224
RustTy::EngineArray {
223225
tokens: quote! { Array<#rust_elem_ty> },

godot-codegen/src/models/domain.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -588,11 +588,13 @@ pub struct GodotTy {
588588

589589
#[derive(Clone, Debug)]
590590
pub enum RustTy {
591-
/// `bool`, `Vector3i`
591+
/// `bool`, `Vector3i`, `Array`
592592
BuiltinIdent(Ident),
593593

594594
/// `Array<i32>`
595-
BuiltinArray(TokenStream),
595+
///
596+
/// Note that untyped arrays are mapped as `BuiltinIdent("Array")`.
597+
BuiltinArray { elem_type: TokenStream },
596598

597599
/// C-style raw pointer to a `RustTy`.
598600
RawPointer { inner: Box<RustTy>, is_const: bool },
@@ -674,7 +676,7 @@ impl ToTokens for RustTy {
674676
fn to_tokens(&self, tokens: &mut TokenStream) {
675677
match self {
676678
RustTy::BuiltinIdent(ident) => ident.to_tokens(tokens),
677-
RustTy::BuiltinArray(path) => path.to_tokens(tokens),
679+
RustTy::BuiltinArray { elem_type } => elem_type.to_tokens(tokens),
678680
RustTy::RawPointer {
679681
inner,
680682
is_const: true,

godot-codegen/src/special_cases/codegen_special_cases.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ fn is_type_excluded(ty: &str, ctx: &mut Context) -> bool {
3737
fn is_rust_type_excluded(ty: &RustTy) -> bool {
3838
match ty {
3939
RustTy::BuiltinIdent(_) => false,
40-
RustTy::BuiltinArray(_) => false,
40+
RustTy::BuiltinArray { .. } => false,
4141
RustTy::RawPointer { inner, .. } => is_rust_type_excluded(inner),
4242
RustTy::EngineArray { elem_class, .. } => is_class_excluded(elem_class.as_str()),
4343
RustTy::EngineEnum {

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

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ use sys::{ffi_methods, interface_fn, GodotFfi};
3434
/// `Array<T>`, where the type `T` must implement `ArrayElement`. Some types like `Array<T>` cannot
3535
/// be stored inside arrays, as Godot prevents nesting.
3636
///
37+
/// If you plan to use any integer or float types apart from `i64` and `f64`, read
38+
/// [this documentation](../meta/trait.ArrayElement.html#integer-and-float-types).
39+
///
3740
/// # Reference semantics
3841
///
3942
/// Like in GDScript, `Array` acts as a reference type: multiple `Array` instances may
@@ -716,19 +719,57 @@ impl<T: ArrayElement> Array<T> {
716719
///
717720
/// Note also that any `GodotType` can be written to a `Variant` array.
718721
///
719-
/// In the current implementation, both cases will produce a panic rather than undefined
720-
/// behavior, but this should not be relied upon.
722+
/// In the current implementation, both cases will produce a panic rather than undefined behavior, but this should not be relied upon.
721723
unsafe fn assume_type<U: ArrayElement>(self) -> Array<U> {
722-
// SAFETY: The memory layout of `Array<T>` does not depend on `T`.
723-
unsafe { std::mem::transmute(self) }
724+
// The memory layout of `Array<T>` does not depend on `T`.
725+
std::mem::transmute::<Array<T>, Array<U>>(self)
726+
}
727+
728+
/// # Safety
729+
/// See [`assume_type`](Self::assume_type).
730+
unsafe fn assume_type_ref<U: ArrayElement>(&self) -> &Array<U> {
731+
// The memory layout of `Array<T>` does not depend on `T`.
732+
std::mem::transmute::<&Array<T>, &Array<U>>(self)
733+
}
734+
735+
#[cfg(debug_assertions)]
736+
pub(crate) fn debug_validate_elements(&self) -> Result<(), ConvertError> {
737+
// SAFETY: every element is internally represented as Variant.
738+
let canonical_array = unsafe { self.assume_type_ref::<Variant>() };
739+
740+
// If any element is not convertible, this will return an error.
741+
for elem in canonical_array.iter_shared() {
742+
elem.try_to::<T>().map_err(|_err| {
743+
FromGodotError::BadArrayTypeInt {
744+
expected: self.type_info(),
745+
value: elem
746+
.try_to::<i64>()
747+
.expect("origin must be i64 compatible; this is a bug"),
748+
}
749+
.into_error(self.clone())
750+
})?;
751+
}
752+
753+
Ok(())
754+
}
755+
756+
// No-op in Release. Avoids O(n) conversion checks, but still panics on access.
757+
#[cfg(not(debug_assertions))]
758+
pub(crate) fn debug_validate_elements(&self) -> Result<(), ConvertError> {
759+
Ok(())
724760
}
725761

726762
/// Returns the runtime type info of this array.
727763
fn type_info(&self) -> ArrayTypeInfo {
728764
let variant_type = VariantType::from_sys(
729765
self.as_inner().get_typed_builtin() as sys::GDExtensionVariantType
730766
);
731-
let class_name = self.as_inner().get_typed_class_name();
767+
768+
let class_name = if variant_type == VariantType::OBJECT {
769+
Some(self.as_inner().get_typed_class_name())
770+
} else {
771+
None
772+
};
732773

733774
ArrayTypeInfo {
734775
variant_type,
@@ -765,12 +806,23 @@ impl<T: ArrayElement> Array<T> {
765806
if type_info.is_typed() {
766807
let script = Variant::nil();
767808

809+
// A bit contrived because empty StringName is lazy-initialized but must also remain valid.
810+
#[allow(unused_assignments)]
811+
let mut empty_string_name = None;
812+
let class_name = if let Some(class_name) = &type_info.class_name {
813+
class_name.string_sys()
814+
} else {
815+
empty_string_name = Some(StringName::default());
816+
// as_ref() crucial here -- otherwise the StringName is dropped.
817+
empty_string_name.as_ref().unwrap().string_sys()
818+
};
819+
768820
// SAFETY: The array is a newly created empty untyped array.
769821
unsafe {
770822
interface_fn!(array_set_typed)(
771823
self.sys_mut(),
772824
type_info.variant_type.sys(),
773-
type_info.class_name.string_sys(),
825+
class_name, // must be empty if variant_type != OBJECT.
774826
script.var_sys(),
775827
);
776828
}
@@ -786,6 +838,19 @@ impl<T: ArrayElement> Array<T> {
786838
}
787839
}
788840

841+
impl VariantArray {
842+
/// # Safety
843+
/// - Variant must have type `VariantType::ARRAY`.
844+
/// - Subsequent operations on this array must not rely on the type of the array.
845+
pub(crate) unsafe fn from_variant_unchecked(variant: &Variant) -> Self {
846+
// See also ffi_from_variant().
847+
Self::new_with_uninit(|self_ptr| {
848+
let array_from_variant = sys::builtin_fn!(array_from_variant);
849+
array_from_variant(self_ptr, sys::SysPtr::force_mut(variant.var_sys()));
850+
})
851+
}
852+
}
853+
789854
// ----------------------------------------------------------------------------------------------------------------------------------------------
790855
// Traits
791856

@@ -838,14 +903,16 @@ impl<T: ArrayElement> ToGodot for Array<T> {
838903

839904
impl<T: ArrayElement> FromGodot for Array<T> {
840905
fn try_from_godot(via: Self::Via) -> Result<Self, ConvertError> {
906+
T::debug_validate_elements(&via)?;
841907
Ok(via)
842908
}
843909
}
844910

845911
impl<T: ArrayElement> fmt::Debug for Array<T> {
846912
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
847913
// Going through `Variant` because there doesn't seem to be a direct way.
848-
write!(f, "{:?}", self.to_variant().stringify())
914+
// Reuse Display.
915+
write!(f, "{}", self.to_variant().stringify())
849916
}
850917
}
851918

@@ -878,17 +945,21 @@ impl<T: ArrayElement + fmt::Display> fmt::Display for Array<T> {
878945
impl<T: ArrayElement> Clone for Array<T> {
879946
fn clone(&self) -> Self {
880947
// SAFETY: `self` is a valid array, since we have a reference that keeps it alive.
881-
let array = unsafe {
948+
let copy = unsafe {
882949
Self::new_with_uninit(|self_ptr| {
883950
let ctor = sys::builtin_fn!(array_construct_copy);
884951
let args = [self.sys()];
885952
ctor(self_ptr, args.as_ptr());
886953
})
887954
};
888955

889-
array
890-
.with_checked_type()
891-
.expect("copied array should have same type as original array")
956+
// Double-check copy's runtime type in Debug mode.
957+
if cfg!(debug_assertions) {
958+
copy.with_checked_type()
959+
.expect("copied array should have same type as original array")
960+
} else {
961+
copy
962+
}
892963
}
893964
}
894965

@@ -1000,6 +1071,7 @@ impl<T: ArrayElement> GodotFfiVariant for Array<T> {
10001071
}
10011072

10021073
fn ffi_from_variant(variant: &Variant) -> Result<Self, ConvertError> {
1074+
// First check if the variant is an array. The array conversion shouldn't be called otherwise.
10031075
if variant.get_type() != Self::variant_type() {
10041076
return Err(FromVariantError::BadType {
10051077
expected: Self::variant_type(),
@@ -1015,6 +1087,7 @@ impl<T: ArrayElement> GodotFfiVariant for Array<T> {
10151087
})
10161088
};
10171089

1090+
// Then, check the runtime type of the array.
10181091
array.with_checked_type()
10191092
}
10201093
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -606,7 +606,7 @@ impl<'a> Keys<'a> {
606606
TypedKeys::from_untyped(self)
607607
}
608608

609-
/// Returns an array of the keys
609+
/// Returns an array of the keys.
610610
pub fn array(self) -> VariantArray {
611611
// Can only be called
612612
assert!(self.iter.is_first);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,7 @@ macro_rules! impl_packed_array {
548548
fn export_hint() -> $crate::meta::PropertyHintInfo {
549549
// In 4.3 Godot can (and does) use type hint strings for packed arrays, see https://github.com/godotengine/godot/pull/82952.
550550
if sys::GdextBuild::since_api("4.3") {
551-
$crate::meta::PropertyHintInfo::export_array_element::<$Element>()
551+
$crate::meta::PropertyHintInfo::export_packed_array_element::<$Element>()
552552
} else {
553553
$crate::meta::PropertyHintInfo::type_name::<$PackedArray>()
554554
}

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

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

8-
use crate::builtin::{GString, StringName, VariantDispatch, VariantOperator, VariantType};
8+
use crate::builtin::{
9+
GString, StringName, VariantArray, VariantDispatch, VariantOperator, VariantType,
10+
};
911
use crate::meta::error::ConvertError;
1012
use crate::meta::{ArrayElement, FromGodot, ToGodot};
1113
use godot_ffi as sys;
@@ -448,6 +450,16 @@ impl fmt::Display for Variant {
448450

449451
impl fmt::Debug for Variant {
450452
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
451-
VariantDispatch::from_variant(self).fmt(f)
453+
// Special case for arrays: avoids converting to VariantArray (the only Array type in VariantDispatch), which fails
454+
// for typed arrays and causes a panic. This can cause an infinite loop with Debug, or abort.
455+
// Can be removed if there's ever a "possibly typed" Array type (e.g. OutArray) in the library.
456+
457+
if self.get_type() == VariantType::ARRAY {
458+
// SAFETY: type is checked, and only operation is print (out data flow, no covariant in access).
459+
let array = unsafe { VariantArray::from_variant_unchecked(self) };
460+
array.fmt(f)
461+
} else {
462+
VariantDispatch::from_variant(self).fmt(f)
463+
}
452464
}
453465
}

godot-core/src/meta/array_type_info.rs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,27 @@ use std::fmt;
1616
/// We ignore the `script` parameter because it has no impact on typing in Godot.
1717
#[derive(Eq, PartialEq)]
1818
pub(crate) struct ArrayTypeInfo {
19+
/// The builtin type; always set.
1920
pub variant_type: VariantType,
2021

22+
/// If [`variant_type`] is [`VariantType::OBJECT`], then the class name; otherwise `None`.
23+
///
2124
/// Not a `ClassName` because some values come from Godot engine API.
22-
pub class_name: StringName,
25+
pub class_name: Option<StringName>,
2326
}
2427

2528
impl ArrayTypeInfo {
2629
pub fn of<T: GodotType>() -> Self {
30+
let variant_type = <T::Via as GodotType>::Ffi::variant_type();
31+
let class_name = if variant_type == VariantType::OBJECT {
32+
Some(T::Via::class_name().to_string_name())
33+
} else {
34+
None
35+
};
36+
2737
Self {
28-
variant_type: <T::Via as GodotType>::Ffi::variant_type(),
29-
class_name: T::Via::class_name().to_string_name(),
38+
variant_type,
39+
class_name,
3040
}
3141
}
3242

@@ -38,18 +48,17 @@ impl ArrayTypeInfo {
3848
self.variant_type
3949
}
4050

41-
pub fn class_name(&self) -> &StringName {
42-
&self.class_name
51+
pub fn class_name(&self) -> Option<&StringName> {
52+
self.class_name.as_ref()
4353
}
4454
}
4555

4656
impl fmt::Debug for ArrayTypeInfo {
4757
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48-
let class = self.class_name.to_string();
49-
let class_str = if class.is_empty() {
50-
String::new()
51-
} else {
58+
let class_str = if let Some(class) = &self.class_name {
5259
format!(" (class={class})")
60+
} else {
61+
String::new()
5362
};
5463

5564
write!(f, "{:?}{}", self.variant_type, class_str)

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,19 @@ impl fmt::Display for ErrorKind {
169169
/// Conversion failed during a [`FromGodot`](crate::meta::FromGodot) call.
170170
#[derive(Eq, PartialEq, Debug)]
171171
pub(crate) enum FromGodotError {
172+
/// Destination `Array<T>` has different type than source's runtime type.
172173
BadArrayType {
173174
expected: ArrayTypeInfo,
174175
actual: ArrayTypeInfo,
175176
},
177+
178+
/// Special case of `BadArrayType` where a custom int type such as `i8` cannot hold a dynamic `i64` value.
179+
BadArrayTypeInt { expected: ArrayTypeInfo, value: i64 },
180+
176181
/// InvalidEnum is also used by bitfields.
177182
InvalidEnum,
183+
184+
/// `InstanceId` cannot be 0.
178185
ZeroInstanceId,
179186
}
180187

@@ -208,17 +215,22 @@ impl fmt::Display for FromGodotError {
208215
};
209216
}
210217

218+
let exp_class = expected.class_name().expect("lhs class name present");
219+
let act_class = actual.class_name().expect("rhs class name present");
211220
assert_ne!(
212-
expected.class_name(),
213-
actual.class_name(),
221+
exp_class, act_class,
214222
"BadArrayType with expected == got, this is a gdext bug"
215223
);
216224

217225
write!(
218226
f,
219-
"expected array of class {}, got array of class {}",
220-
expected.class_name(),
221-
actual.class_name()
227+
"expected array of class {exp_class}, got array of class {act_class}"
228+
)
229+
}
230+
Self::BadArrayTypeInt { expected, value } => {
231+
write!(
232+
f,
233+
"integer value {value} does not fit into Array of type {expected:?}"
222234
)
223235
}
224236
Self::InvalidEnum => write!(f, "invalid engine enum value"),

0 commit comments

Comments
 (0)