From 897ad60c5c3cb47ac729e01c911b0b33fca132ec Mon Sep 17 00:00:00 2001 From: Thomas ten Cate Date: Sat, 28 Jan 2023 15:13:39 +0100 Subject: [PATCH] Add support for typed arrays - Rename `Array` to `TypedArray` - Check/set runtime type of the underlying Godot `Array` - Make all parameters and return values `T` instead of `Variant` - Add `Array` as an alias for `TypedArray` - Add `array!` macro and use it in tests See #33 for design discussion --- godot-codegen/src/lib.rs | 5 + godot-core/src/builtin/array.rs | 861 ++++++++++++++++++++++ godot-core/src/builtin/arrays.rs | 639 ---------------- godot-core/src/builtin/macros.rs | 4 + godot-core/src/builtin/meta/class_name.rs | 8 +- godot-core/src/builtin/meta/mod.rs | 6 +- godot-core/src/builtin/mod.rs | 7 +- godot-core/src/builtin/variant/impls.rs | 51 +- godot-core/src/obj/gd.rs | 14 +- godot-core/src/registry.rs | 8 +- godot-macros/src/derive_godot_class.rs | 2 +- godot/src/lib.rs | 2 +- itest/godot/ManualFfiTests.gd | 20 + itest/godot/TestRunner.gd | 2 +- itest/rust/src/array_test.rs | 399 +++++++--- itest/rust/src/dictionary_test.rs | 16 +- itest/rust/src/variant_test.rs | 8 +- 17 files changed, 1264 insertions(+), 788 deletions(-) create mode 100644 godot-core/src/builtin/array.rs delete mode 100644 godot-core/src/builtin/arrays.rs diff --git a/godot-codegen/src/lib.rs b/godot-codegen/src/lib.rs index 52bebc1a7..02f110e66 100644 --- a/godot-codegen/src/lib.rs +++ b/godot-codegen/src/lib.rs @@ -270,6 +270,8 @@ const SELECTED_CLASSES: &[&str] = &[ "Control", "FileAccess", "HTTPRequest", + "Image", + "ImageTextureLayered", "Input", "Label", "MainLoop", @@ -290,6 +292,9 @@ const SELECTED_CLASSES: &[&str] = &[ "SceneTree", "Sprite2D", "SpriteFrames", + "Texture", + "Texture2DArray", + "TextureLayered", "Time", "Timer", ]; diff --git a/godot-core/src/builtin/array.rs b/godot-core/src/builtin/array.rs new file mode 100644 index 000000000..8a2ab0036 --- /dev/null +++ b/godot-core/src/builtin/array.rs @@ -0,0 +1,861 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use godot_ffi as sys; + +use crate::builtin::meta::VariantMetadata; +use crate::builtin::*; +use crate::obj::Share; +use std::fmt; +use std::marker::PhantomData; +use sys::{ffi_methods, interface_fn, GodotFfi}; + +/// Godot's `Array` type. +/// +/// Unlike GDScript, all indices and sizes are unsigned, so negative indices are not supported. +/// +/// # Typed arrays +/// +/// Godot's `Array` can be either typed or untyped. +/// +/// An untyped array can contain any kind of [`Variant`], even different types in the same array. +/// We represent this in Rust as `Array`, which is just a type alias for `TypedArray`. +/// +/// Godot also supports typed arrays, which are also just `Variant` arrays under the hood, but with +/// runtime checks that no values of the wrong type are put into the array. We represent this as +/// `TypedArray`, where the type `T` implements `VariantMetadata`, `FromVariant` and `ToVariant`. +/// +/// # Reference semantics +/// +/// Like in GDScript, `TypedArray` acts as a reference type: multiple `TypedArray` instances may +/// refer to the same underlying array, and changes to one are visible in the other. +/// +/// To create a copy that shares data with the original array, use [`Share::share()`]. If you want +/// to create a copy of the data, use [`duplicate_shallow()`] or [`duplicate_deep()`]. +/// +/// # Thread safety +/// +/// Usage is safe if the `TypedArray` is used on a single thread only. Concurrent reads on +/// different threads are also safe, but any writes must be externally synchronized. The Rust +/// compiler will enforce this as long as you use only Rust threads, but it cannot protect against +/// concurrent modification on other threads (e.g. created through GDScript). +// `T` must be restricted to `VariantMetadata` in the type, because `Drop` can only be implemented +// for `T: VariantMetadata` because `drop()` requires `sys_mut()`, which is on the `GodotFfi` +// trait, whose `from_sys_init()` requires `Default`, which is only implemented for `T: +// VariantMetadata`. Whew. This could be fixed by splitting up `GodotFfi` if desired. +#[repr(C)] +pub struct TypedArray { + opaque: sys::types::OpaqueArray, + _phantom: PhantomData, +} + +/// A Godot `Array` without an assigned type. +pub type Array = TypedArray; + +// TODO check if these return a typed array +impl_builtin_froms!(Array; + PackedByteArray => array_from_packed_byte_array, + PackedColorArray => array_from_packed_color_array, + PackedFloat32Array => array_from_packed_float32_array, + PackedFloat64Array => array_from_packed_float64_array, + PackedInt32Array => array_from_packed_int32_array, + PackedInt64Array => array_from_packed_int64_array, + PackedStringArray => array_from_packed_string_array, + PackedVector2Array => array_from_packed_vector2_array, + PackedVector3Array => array_from_packed_vector3_array, +); + +impl TypedArray { + fn from_opaque(opaque: sys::types::OpaqueArray) -> Self { + let array = Self { + opaque, + _phantom: PhantomData, + }; + array.check_type(); + array + } + + /// Returns the number of elements in the array. Equivalent of `size()` in Godot. + pub fn len(&self) -> usize { + to_usize(self.as_inner().size()) + } + + /// Returns `true` if the array is empty. + pub fn is_empty(&self) -> bool { + self.as_inner().is_empty() + } + + /// Returns a 32-bit integer hash value representing the array and its contents. + /// + /// Note: Arrays with equal content will always produce identical hash values. However, the + /// reverse is not true. Returning identical hash values does not imply the arrays are equal, + /// because different arrays can have identical hash values due to hash collisions. + pub fn hash(&self) -> u32 { + // The GDExtension interface only deals in `i64`, but the engine's own `hash()` function + // actually returns `uint32_t`. + self.as_inner().hash().try_into().unwrap() + } + + /// Clears the array, removing all elements. + pub fn clear(&mut self) { + self.as_inner().clear(); + } + + /// Resizes the array to contain a different number of elements. If the new size is smaller, + /// elements are removed from the end. If the new size is larger, new elements are set to + /// [`Variant::nil()`]. + pub fn resize(&mut self, size: usize) { + self.as_inner().resize(to_i64(size)); + } + + /// Reverses the order of the elements in the array. + pub fn reverse(&mut self) { + self.as_inner().reverse(); + } + + /// Sorts the array. + /// + /// Note: The sorting algorithm used is not + /// [stable](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability). This means that values + /// considered equal may have their order changed when using `sort_unstable`. + pub fn sort_unstable(&mut self) { + self.as_inner().sort(); + } + + /// Shuffles the array such that the items will have a random order. This method uses the + /// global random number generator common to methods such as `randi`. Call `randomize` to + /// ensure that a new seed will be used each time if you want non-reproducible shuffling. + pub fn shuffle(&mut self) { + self.as_inner().shuffle(); + } + + /// Asserts that the given index refers to an existing element. + /// + /// # Panics + /// + /// If `index` is out of bounds. + fn check_bounds(&self, index: usize) { + let len = self.len(); + assert!( + index < len, + "Array index {index} is out of bounds: length is {len}", + ); + } + + /// Returns a pointer to the element at the given index. + /// + /// # Panics + /// + /// If `index` is out of bounds. + fn ptr(&self, index: usize) -> *const Variant { + self.check_bounds(index); + // SAFETY: We just checked that the index is not out of bounds. + let ptr = unsafe { self.ptr_unchecked(index) }; + assert!(!ptr.is_null()); + ptr + } + + /// Returns a mutable pointer to the element at the given index. + /// + /// # Panics + /// + /// If `index` is out of bounds. + fn ptr_mut(&self, index: usize) -> *mut Variant { + self.check_bounds(index); + // SAFETY: We just checked that the index is not out of bounds. + let ptr = unsafe { self.ptr_mut_unchecked(index) }; + assert!(!ptr.is_null()); + ptr + } + + /// Returns a pointer to the element at the given index. + /// + /// # Safety + /// + /// Calling this with an out-of-bounds index is undefined behavior. + unsafe fn ptr_unchecked(&self, index: usize) -> *const Variant { + let item_ptr: sys::GDExtensionVariantPtr = + (interface_fn!(array_operator_index_const))(self.sys(), to_i64(index)); + item_ptr as *const Variant + } + + /// Returns a mutable pointer to the element at the given index. + /// + /// # Safety + /// + /// Calling this with an out-of-bounds index is undefined behavior. + unsafe fn ptr_mut_unchecked(&self, index: usize) -> *mut Variant { + let item_ptr: sys::GDExtensionVariantPtr = + (interface_fn!(array_operator_index))(self.sys(), to_i64(index)); + item_ptr as *mut Variant + } + + #[doc(hidden)] + pub fn as_inner<'a>(&'a self) -> inner::InnerArray { + // `from_outer()` is only implemented on `Array`, not on `TypedArray`, so we need to + // convert the reference. + // TODO Find a way not to need this ugliness. + // SAFETY: The memory layout of `TypedArray` does not depend on `T`. + let self_as_variant_array: &'a Array = unsafe { std::mem::transmute(self) }; + inner::InnerArray::from_outer(self_as_variant_array) + } + + /// Changes the generic type on this array, without changing its contents. Needed for API + /// functions that return a variant array even though we know its type, and for API functions + /// that take a variant array even though we want to pass a typed one. + /// + /// This is marked `unsafe` since it can be used to break the invariant that a `TypedArray` + /// always holds a Godot array whose runtime type is `T`. + /// + /// # Safety + /// + /// In and of itself, calling this does not result in undefined behavior. However: + /// - If `T` is not `Variant`, the returned array should not be written to, because the runtime + /// type check may fail. + /// - If `U` is not `Variant`, the returned array should not be read from, because conversion + /// from variants may fail. + /// In the current implementation, both cases will produce a panic rather than undefined + /// behavior, but this should not be relied upon. + unsafe fn assume_type(self) -> TypedArray { + // SAFETY: The memory layout of `TypedArray` does not depend on `T`. + unsafe { std::mem::transmute(self) } + } +} + +impl TypedArray { + /// Constructs an empty `TypedArray`. + pub fn new() -> Self { + Self::default() + } + + /// Returns a shallow copy of the array. All array elements are copied, but any reference types + /// (such as `TypedArray`, `Dictionary` and `Object`) will still refer to the same value. + /// + /// To create a deep copy, use [`duplicate_deep()`] instead. To create a new reference to the + /// same array data, use [`share()`]. + pub fn duplicate_shallow(&self) -> Self { + let duplicate: Array = self.as_inner().duplicate(false); + // SAFETY: duplicate() returns a typed array with the same type as Self + unsafe { duplicate.assume_type() } + } + + /// Returns a deep copy of the array. All nested arrays and dictionaries are duplicated and + /// will not be shared with the original array. Note that any `Object`-derived elements will + /// still be shallow copied. + /// + /// To create a shallow copy, use [`duplicate_shallow()`] instead. To create a new reference to + /// the same array data, use [`share()`]. + pub fn duplicate_deep(&self) -> Self { + let duplicate: Array = self.as_inner().duplicate(true); + // SAFETY: duplicate() returns a typed array with the same type as Self + unsafe { duplicate.assume_type() } + } + + /// Returns a slice of the `TypedArray`, from `begin` (inclusive) to `end` (exclusive), as a + /// new `TypedArray`. + /// + /// The values of `begin` and `end` will be clamped to the array size. + /// + /// If specified, `step` is the relative index between source elements. It can be negative, + /// in which case `begin` must be higher than `end`. For example, + /// `TypedArray::from(&[0, 1, 2, 3, 4, 5]).slice(5, 1, -2)` returns `[5, 3]`. + /// + /// Array elements are copied to the slice, but any reference types (such as `TypedArray`, + /// `Dictionary` and `Object`) will still refer to the same value. To create a deep copy, use + /// [`slice_deep()`] instead. + pub fn slice_shallow(&self, begin: usize, end: usize, step: Option) -> Self { + assert_ne!(step, Some(0)); + let len = self.len(); + let begin = begin.min(len); + let end = end.min(len); + let step = step.unwrap_or(1); + let slice: Array = + self.as_inner() + .slice(to_i64(begin), to_i64(end), step.try_into().unwrap(), false); + // SAFETY: slice() returns a typed array with the same type as Self + unsafe { slice.assume_type() } + } + + /// Returns a slice of the `TypedArray`, from `begin` (inclusive) to `end` (exclusive), as a + /// new `TypedArray`. + /// + /// The values of `begin` and `end` will be clamped to the array size. + /// + /// If specified, `step` is the relative index between source elements. It can be negative, + /// in which case `begin` must be higher than `end`. For example, + /// `TypedArray::from(&[0, 1, 2, 3, 4, 5]).slice(5, 1, -2)` returns `[5, 3]`. + /// + /// All nested arrays and dictionaries are duplicated and will not be shared with the original + /// array. Note that any `Object`-derived elements will still be shallow copied. To create a + /// shallow copy, use [`slice_shallow()`] instead. + pub fn slice_deep(&self, begin: usize, end: usize, step: Option) -> Self { + let len = self.len(); + let begin = begin.min(len); + let end = end.min(len); + let step = step.unwrap_or(1); + assert!(step != 0); + let slice: Array = + self.as_inner() + .slice(to_i64(begin), to_i64(end), step.try_into().unwrap(), true); + // SAFETY: slice() returns a typed array with the same type as Self + unsafe { slice.assume_type() } + } + + /// Appends another array at the end of this array. Equivalent of `append_array` in GDScript. + pub fn extend_array(&mut self, other: TypedArray) { + // SAFETY: Read-only arrays are covariant: conversion to a variant array is fine as long as + // we don't insert values into it afterwards, and `append_array()` doesn't do that. + let other: Array = unsafe { other.assume_type::() }; + self.as_inner().append_array(other); + } + + /// Returns the runtime type info of this array. + fn type_info(&self) -> TypeInfo { + let variant_type = VariantType::from_sys( + self.as_inner().get_typed_builtin() as sys::GDExtensionVariantType + ); + let class_name = self.as_inner().get_typed_class_name(); + TypeInfo { + variant_type, + class_name, + } + } + + /// Checks that the inner array has the correct type set on it for storing elements of type `T`. + /// + /// # Panics + /// + /// If the inner type doesn't match `T` or no type is set at all. + fn check_type(&self) { + assert_eq!(self.type_info(), TypeInfo::new::()); + } + + /// Sets the type of the inner array. Can only be called once, directly after creation. + fn init_inner_type(&mut self) { + debug_assert!(self.is_empty()); + debug_assert!(!self.type_info().is_typed()); + let type_info = TypeInfo::new::(); + if type_info.is_typed() { + let script = Variant::nil(); + unsafe { + interface_fn!(array_set_typed)( + self.sys(), + type_info.variant_type.sys(), + type_info.class_name.string_sys(), + script.var_sys(), + ); + } + } + } +} + +impl TypedArray { + /// Returns an iterator over the elements of the `TypedArray`. Note that this takes the array + /// by reference but returns its elements by value, since they are internally converted from + /// `Variant`. + /// + /// Notice that it's possible to modify the `TypedArray` through another reference while + /// iterating over it. This will not result in unsoundness or crashes, but will cause the + /// iterator to behave in an unspecified way. + pub fn iter_shared(&self) -> TypedArrayIterator<'_, T> { + TypedArrayIterator { + array: self, + next_idx: 0, + } + } + + /// Returns the value at the specified index. + /// + /// # Panics + /// + /// If `index` is out of bounds. + pub fn get(&self, index: usize) -> T { + let ptr = self.ptr(index); + // SAFETY: `ptr()` just verified that the index is not out of bounds. + let variant = unsafe { &*ptr }; + T::from_variant(variant) + } + + /// Returns the first element in the array, or `None` if the array is empty. Equivalent of + /// `front()` in GDScript. + pub fn first(&self) -> Option { + (!self.is_empty()).then(|| { + let variant = self.as_inner().front(); + T::from_variant(&variant) + }) + } + + /// Returns the last element in the array, or `None` if the array is empty. Equivalent of + /// `back()` in GDScript. + pub fn last(&self) -> Option { + (!self.is_empty()).then(|| { + let variant = self.as_inner().back(); + T::from_variant(&variant) + }) + } + + /// Returns the minimum value contained in the array if all elements are of comparable types. + /// If the elements can't be compared or the array is empty, `None` is returned. + pub fn min(&self) -> Option { + let min = self.as_inner().min(); + (!min.is_nil()).then(|| T::from_variant(&min)) + } + + /// Returns the maximum value contained in the array if all elements are of comparable types. + /// If the elements can't be compared or the array is empty, `None` is returned. + pub fn max(&self) -> Option { + let max = self.as_inner().max(); + (!max.is_nil()).then(|| T::from_variant(&max)) + } + + /// Returns a random element from the array, or `None` if it is empty. + pub fn pick_random(&self) -> Option { + (!self.is_empty()).then(|| { + let variant = self.as_inner().pick_random(); + T::from_variant(&variant) + }) + } + + /// Removes and returns the last element of the array. Returns `None` if the array is empty. + /// Equivalent of `pop_back` in GDScript. + pub fn pop(&mut self) -> Option { + (!self.is_empty()).then(|| { + let variant = self.as_inner().pop_back(); + T::from_variant(&variant) + }) + } + + /// Removes and returns the first element of the array. Returns `None` if the array is empty. + /// + /// Note: On large arrays, this method is much slower than `pop` as it will move all the + /// array's elements. The larger the array, the slower `pop_front` will be. + pub fn pop_front(&mut self) -> Option { + (!self.is_empty()).then(|| { + let variant = self.as_inner().pop_front(); + T::from_variant(&variant) + }) + } + + /// Removes and returns the element at the specified index. Equivalent of `pop_at` in GDScript. + /// + /// On large arrays, this method is much slower than `pop_back` as it will move all the array's + /// elements after the removed element. The larger the array, the slower `remove` will be. + /// + /// # Panics + /// + /// If `index` is out of bounds. + pub fn remove(&mut self, index: usize) -> T { + self.check_bounds(index); + let variant = self.as_inner().pop_at(to_i64(index)); + T::from_variant(&variant) + } +} + +impl TypedArray { + /// Finds the index of an existing value in a sorted array using binary search. Equivalent of + /// `bsearch` in GDScript. + /// + /// If the value is not present in the array, returns the insertion index that would maintain + /// sorting order. + /// + /// Calling `binary_search` on an unsorted array results in unspecified behavior. + pub fn binary_search(&self, value: &T) -> usize { + to_usize(self.as_inner().bsearch(value.to_variant(), true)) + } + + /// Returns the number of times a value is in the array. + pub fn count(&self, value: &T) -> usize { + to_usize(self.as_inner().count(value.to_variant())) + } + + /// Returns `true` if the array contains the given value. Equivalent of `has` in GDScript. + pub fn contains(&self, value: &T) -> bool { + self.as_inner().has(value.to_variant()) + } + + /// Searches the array for the first occurrence of a value and returns its index, or `None` if + /// not found. Starts searching at index `from`; pass `None` to search the entire array. + pub fn find(&self, value: &T, from: Option) -> Option { + let from = to_i64(from.unwrap_or(0)); + let index = self.as_inner().find(value.to_variant(), from); + if index >= 0 { + Some(index.try_into().unwrap()) + } else { + None + } + } + + /// Searches the array backwards for the last occurrence of a value and returns its index, or + /// `None` if not found. Starts searching at index `from`; pass `None` to search the entire + /// array. + pub fn rfind(&self, value: &T, from: Option) -> Option { + let from = from.map(to_i64).unwrap_or(-1); + let index = self.as_inner().rfind(value.to_variant(), from); + // It's not documented, but `rfind` returns -1 if not found. + if index >= 0 { + Some(to_usize(index)) + } else { + None + } + } + + /// Sets the value at the specified index. + /// + /// # Panics + /// + /// If `index` is out of bounds. + pub fn set(&mut self, index: usize, value: T) { + let ptr_mut = self.ptr_mut(index); + // SAFETY: `ptr_mut` just checked that the index is not out of bounds. + unsafe { + *ptr_mut = value.to_variant(); + } + } + + /// Appends an element to the end of the array. Equivalent of `append` and `push_back` in + /// GDScript. + pub fn push(&mut self, value: T) { + self.as_inner().push_back(value.to_variant()); + } + + /// Adds an element at the beginning of the array. See also `push`. + /// + /// Note: On large arrays, this method is much slower than `push` as it will move all the + /// array's elements. The larger the array, the slower `push_front` will be. + pub fn push_front(&mut self, value: T) { + self.as_inner().push_front(value.to_variant()); + } + + /// Inserts a new element at a given index in the array. The index must be valid, or at the end + /// of the array (`index == len()`). + /// + /// Note: On large arrays, this method is much slower than `push` as it will move all the + /// array's elements after the inserted element. The larger the array, the slower `insert` will + /// be. + pub fn insert(&mut self, index: usize, value: T) { + let len = self.len(); + assert!( + index <= len, + "TypedArray insertion index {index} is out of bounds: length is {len}", + ); + self.as_inner().insert(to_i64(index), value.to_variant()); + } + + /// Removes the first occurrence of a value from the array. If the value does not exist in the + /// array, nothing happens. To remove an element by index, use `remove` instead. + /// + /// On large arrays, this method is much slower than `pop_back` as it will move all the array's + /// elements after the removed element. The larger the array, the slower `remove` will be. + pub fn erase(&mut self, value: &T) { + self.as_inner().erase(value.to_variant()); + } + + /// Assigns the given value to all elements in the array. This can be used together with + /// `resize` to create an array with a given size and initialized elements. + pub fn fill(&mut self, value: &T) { + self.as_inner().fill(value.to_variant()); + } +} + +/// Creates a `TypedArray` from the given Rust array. +impl From<&[T; N]> for TypedArray { + fn from(arr: &[T; N]) -> Self { + Self::from(&arr[..]) + } +} + +/// Creates a `TypedArray` from the given slice. +impl From<&[T]> for TypedArray { + fn from(slice: &[T]) -> Self { + let mut array = Self::new(); + let len = slice.len(); + if len == 0 { + return array; + } + array.resize(len); + let ptr = array.ptr_mut(0); + for (i, element) in slice.iter().enumerate() { + // SAFETY: The array contains exactly `len` elements, stored contiguously in memory. + unsafe { + *ptr.offset(to_isize(i)) = element.to_variant(); + } + } + array + } +} + +/// Creates a `TypedArray` from an iterator. +impl FromIterator for TypedArray { + fn from_iter>(iter: I) -> Self { + let mut array = Self::new(); + array.extend(iter); + array + } +} + +/// Extends a `TypedArray` with the contents of an iterator. +impl Extend for TypedArray { + fn extend>(&mut self, iter: I) { + // Unfortunately the GDExtension API does not offer the equivalent of `Vec::reserve`. + // Otherwise we could use it to pre-allocate based on `iter.size_hint()`. + // + // A faster implementation using `resize()` and direct pointer writes might still be + // possible. + for item in iter.into_iter() { + self.push(item); + } + } +} + +/// Converts this array to a strongly typed Rust vector. +impl From<&TypedArray> for Vec { + fn from(array: &TypedArray) -> Vec { + let len = array.len(); + let mut vec = Vec::with_capacity(len); + let ptr = array.ptr(0); + for offset in 0..to_isize(len) { + // SAFETY: Arrays are stored contiguously in memory, so we can use pointer arithmetic + // instead of going through `array_operator_index_const` for every index. + let variant = unsafe { &*ptr.offset(offset) }; + let element = T::from_variant(variant); + vec.push(element); + } + vec + } +} + +pub struct TypedArrayIterator<'a, T: VariantMetadata> { + array: &'a TypedArray, + next_idx: usize, +} + +impl<'a, T: VariantMetadata + FromVariant> Iterator for TypedArrayIterator<'a, T> { + type Item = T; + + fn next(&mut self) -> Option { + if self.next_idx < self.array.len() { + let idx = self.next_idx; + self.next_idx += 1; + // Using `ptr_unchecked` rather than going through `get()` so we can avoid a second + // bounds check. + // SAFETY: We just checked that the index is not out of bounds. + let variant = unsafe { &*self.array.ptr_unchecked(idx) }; + let element = T::from_variant(variant); + Some(element) + } else { + None + } + } +} + +// TODO There's a macro for this, but it doesn't support generics yet; add support and use it +impl PartialEq for TypedArray { + #[inline] + fn eq(&self, other: &Self) -> bool { + unsafe { + let mut result = false; + sys::builtin_call! { + array_operator_equal(self.sys(), other.sys(), result.sys_mut()) + }; + result + } + } +} + +impl PartialOrd for TypedArray { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + let op_less = |lhs, rhs| unsafe { + let mut result = false; + sys::builtin_call! { + array_operator_less(lhs, rhs, result.sys_mut()) + }; + result + }; + + if op_less(self.sys(), other.sys()) { + Some(std::cmp::Ordering::Less) + } else if op_less(other.sys(), self.sys()) { + Some(std::cmp::Ordering::Greater) + } else if self.eq(other) { + Some(std::cmp::Ordering::Equal) + } else { + None + } + } +} + +// Godot has some inconsistent behavior around NaN values. In GDScript, `NAN == NAN` is `false`, +// but `[NAN] == [NAN]` is `true`. If they decide to make all NaNs equal, we can implement `Eq` and +// `Ord`; if they decide to make all NaNs unequal, we can remove this comment. +// +// impl Eq for TypedArray {} +// +// impl Ord for TypedArray { +// ... +// } + +impl fmt::Debug for TypedArray { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Going through `Variant` because there doesn't seem to be a direct way. + write!(f, "{:?}", self.to_variant().stringify()) + } +} + +/// Creates a new reference to the data in this array. Changes to the original array will be +/// reflected in the copy and vice versa. +/// +/// To create a (mostly) independent copy instead, see [`Array::duplicate_shallow()`] and +/// [`Array::duplicate_deep()`]. +impl Share for TypedArray { + fn share(&self) -> Self { + unsafe { + Self::from_sys_init(|self_ptr| { + let ctor = ::godot_ffi::builtin_fn!(array_construct_copy); + let args = [self.sys_const()]; + ctor(self_ptr, args.as_ptr()); + }) + } + } +} + +impl Default for TypedArray { + #[inline] + fn default() -> Self { + // Note: can't use from_sys_init(), as that calls the default constructor + // (because most assignments expect initialized target type) + let mut uninit = std::mem::MaybeUninit::>::uninit(); + let mut array = unsafe { + let self_ptr = (*uninit.as_mut_ptr()).sys_mut(); + sys::builtin_call! { + array_construct_default(self_ptr, std::ptr::null_mut()) + }; + uninit.assume_init() + }; + array.init_inner_type(); + array + } +} + +impl Drop for TypedArray { + #[inline] + fn drop(&mut self) { + unsafe { + let array_destroy = sys::builtin_fn!(array_destroy); + array_destroy(self.sys_mut()); + } + } +} + +impl ToVariant for TypedArray { + fn to_variant(&self) -> Variant { + unsafe { + Variant::from_var_sys_init(|variant_ptr| { + let array_to_variant = sys::builtin_fn!(array_to_variant); + array_to_variant(variant_ptr, self.sys()); + }) + } + } +} + +impl FromVariant for TypedArray { + fn try_from_variant(variant: &Variant) -> Result { + if variant.get_type() != Self::variant_type() { + return Err(VariantConversionError); + } + let result = unsafe { + Self::from_sys_init(|self_ptr| { + let array_from_variant = sys::builtin_fn!(array_from_variant); + array_from_variant(self_ptr, variant.var_sys()); + }) + }; + + Ok(result) + } +} + +impl VariantMetadata for TypedArray { + fn variant_type() -> VariantType { + VariantType::Array + } +} + +impl GodotFfi for TypedArray { + ffi_methods! { + type sys::GDExtensionTypePtr = *mut Opaque; + fn from_sys; + fn sys; + fn write_sys; + } + + unsafe fn from_sys_init(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Self { + // Can't use uninitialized pointer -- Array CoW implementation in C++ expects that on + // assignment, the target CoW pointer is either initialized or nullptr + + let mut result = Self::default(); + init_fn(result.sys_mut()); + result + } +} + +/// Allows for construction of [`TypedArray`] literals, much in the same way as Rust's standard +/// `vec!` macro. The type of the array is inferred from the arguments. +/// +/// # Example +/// +/// ```no_run +/// # use godot::prelude::*; +/// let arr = array![3, 1, 4]; +/// ``` +/// +/// To create an `Array` of variants, you need to convert each element explicitly: +/// +/// ```no_run +/// # use godot::prelude::*; +/// let arr: Array = array![42_i64.to_variant(), "hello".to_variant()]; +/// ``` +#[macro_export] +macro_rules! array { + ($($elements:expr),* $(,)?) => { + { + let mut array = $crate::builtin::TypedArray::default(); + $( + array.push($elements); + )* + array + } + }; +} + +/// Represents the type information of a Godot array. See +/// [`set_typed`](https://docs.godotengine.org/en/latest/classes/class_array.html#class-array-method-set-typed). +/// +/// We ignore the `script` parameter because it has no impact on typing in Godot. +#[derive(Debug, PartialEq, Eq)] +pub struct TypeInfo { + variant_type: VariantType, + class_name: StringName, +} + +impl TypeInfo { + fn new() -> Self { + let variant_type = T::variant_type(); + let class_name = match variant_type { + VariantType::Object => StringName::from(T::class_name()), + // TODO for variant types other than Object, class_name() returns "(no base)"; just + // make it return "" instead? + _ => StringName::default(), + }; + Self { + variant_type, + class_name, + } + } + + fn is_typed(&self) -> bool { + self.variant_type != VariantType::Nil + } +} diff --git a/godot-core/src/builtin/arrays.rs b/godot-core/src/builtin/arrays.rs deleted file mode 100644 index 383e90fb4..000000000 --- a/godot-core/src/builtin/arrays.rs +++ /dev/null @@ -1,639 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -use godot_ffi as sys; - -use crate::builtin::*; -use crate::obj::Share; -use std::fmt; -use std::marker::PhantomData; -use sys::types::*; -use sys::{ffi_methods, interface_fn, GodotFfi}; - -/// Godot's `Array` type. -/// -/// This is a variant array, meaning it contains `Variant`s which may be of different types even -/// within the same array. -/// -/// Unlike GDScript, all indices and sizes are unsigned, so negative indices are not supported. -/// -/// # Reference semantics -/// -/// Like in GDScript, `Array` acts as a reference type: multiple `Array` instances may refer to the -/// same underlying array, and changes to one are visible in the other. -/// -/// To create a copy that shares data with the original array, use [`Share::share()`]. If you want -/// to create a copy of the data, use [`duplicate_shallow()`] or [`duplicate_deep()`]. -/// -/// # Thread safety -/// -/// Usage is safe if the `Array` is used on a single thread only. Concurrent reads on different -/// threads are also safe, but any writes must be externally synchronized. The Rust compiler will -/// enforce this as long as you use only Rust threads, but it cannot protect against concurrent -/// modification on other threads (e.g. created through GDScript). -#[repr(C)] -pub struct Array { - opaque: sys::types::OpaqueArray, -} - -impl_builtin_froms!(Array; - PackedByteArray => array_from_packed_byte_array, - PackedColorArray => array_from_packed_color_array, - PackedFloat32Array => array_from_packed_float32_array, - PackedFloat64Array => array_from_packed_float64_array, - PackedInt32Array => array_from_packed_int32_array, - PackedInt64Array => array_from_packed_int64_array, - PackedStringArray => array_from_packed_string_array, - PackedVector2Array => array_from_packed_vector2_array, - PackedVector3Array => array_from_packed_vector3_array, -); - -impl Array { - fn from_opaque(opaque: sys::types::OpaqueArray) -> Self { - Self { opaque } - } -} - -// This impl relies on `InnerArray` which is not (yet) available in unit tests -impl Array { - /// Constructs an empty `Array`. - pub fn new() -> Self { - Self::default() - } - - /// Returns the number of elements in the array. Equivalent of `size()` in Godot. - pub fn len(&self) -> usize { - to_usize(self.as_inner().size()) - } - - /// Returns `true` if the array is empty. - pub fn is_empty(&self) -> bool { - self.as_inner().is_empty() - } - - /// Returns a 32-bit integer hash value representing the array and its contents. - /// - /// Note: Arrays with equal content will always produce identical hash values. However, the - /// reverse is not true. Returning identical hash values does not imply the arrays are equal, - /// because different arrays can have identical hash values due to hash collisions. - pub fn hash(&self) -> u32 { - // The GDExtension interface only deals in `i64`, but the engine's own `hash()` function - // actually returns `uint32_t`. - self.as_inner().hash().try_into().unwrap() - } - - /// Converts this array to a strongly typed Rust vector. If the conversion from `Variant` fails - /// for any element, an error is returned. - pub fn try_to_vec(&self) -> Result, VariantConversionError> { - let len = self.len(); - let mut vec = Vec::with_capacity(len); - let ptr = self.ptr(0); - for offset in 0..to_isize(len) { - // SAFETY: Arrays are stored contiguously in memory, so we can use pointer arithmetic - // instead of going through `array_operator_index_const` for every index. - let element = unsafe { T::try_from_variant(&*ptr.offset(offset))? }; - vec.push(element); - } - Ok(vec) - } - - /// Returns an iterator over the `Array` by reference. Instead of references to elements as you - /// might expect, the iterator returns a (cheap, shallow) copy of each element. - /// - /// Notice that it's possible to modify the `Array` through another reference while iterating - /// over it. This will not result in unsoundness or crashes, but will cause the iterator to - /// behave in an unspecified way. - pub fn iter_shared(&self) -> ArrayIterator<'_> { - ArrayIterator { - array: self, - next_idx: 0, - _phantom: PhantomData, - } - } - - /// Clears the array, removing all elements. - pub fn clear(&mut self) { - self.as_inner().clear(); - } - - /// Resizes the array to contain a different number of elements. If the new size is smaller, - /// elements are removed from the end. If the new size is larger, new elements are set to - /// [`Variant::nil()`]. - pub fn resize(&mut self, size: usize) { - self.as_inner().resize(to_i64(size)); - } - - /// Returns a shallow copy of the array. All array elements are copied, but any reference types - /// (such as `Array`, `Dictionary` and `Object`) will still refer to the same value. - /// - /// To create a deep copy, use [`duplicate_deep()`] instead. To create a new reference to the - /// same array data, use [`share()`]. - pub fn duplicate_shallow(&self) -> Self { - self.as_inner().duplicate(false) - } - - /// Returns a deep copy of the array. All nested arrays and dictionaries are duplicated and - /// will not be shared with the original array. Note that any `Object`-derived elements will - /// still be shallow copied. - /// - /// To create a shallow copy, use [`duplicate_shallow()`] instead. To create a new reference to - /// the same array data, use [`share()`]. - pub fn duplicate_deep(&self) -> Self { - self.as_inner().duplicate(true) - } - - /// Returns the slice of the `Array`, from `begin` (inclusive) to `end` (exclusive), as a new - /// `Array`. - /// - /// The values of `begin` and `end` will be clamped to the array size. - /// - /// If specified, `step` is the relative index between source elements. It can be negative, - /// in which case `begin` must be higher than `end`. For example, - /// `Array::from(&[0, 1, 2, 3, 4, 5]).slice(5, 1, -2)` returns `[5, 3]`. - /// - /// Array elements are copied to the slice, but any reference types (such as `Array`, - /// `Dictionary` and `Object`) will still refer to the same value. To create a deep copy, use - /// [`slice_deep()`] instead. - pub fn slice_shallow(&self, begin: usize, end: usize, step: Option) -> Self { - assert_ne!(step, Some(0)); - let len = self.len(); - let begin = begin.min(len); - let end = end.min(len); - let step = step.unwrap_or(1); - self.as_inner() - .slice(to_i64(begin), to_i64(end), step.try_into().unwrap(), false) - } - - /// Returns the slice of the `Array`, from `begin` (inclusive) to `end` (exclusive), as a new - /// `Array`. - /// - /// The values of `begin` and `end` will be clamped to the array size. - /// - /// If specified, `step` is the relative index between source elements. It can be negative, - /// in which case `begin` must be higher than `end`. For example, - /// `Array::from(&[0, 1, 2, 3, 4, 5]).slice(5, 1, -2)` returns `[5, 3]`. - /// - /// All nested arrays and dictionaries are duplicated and will not be shared with the original - /// array. Note that any `Object`-derived elements will still be shallow copied. To create a - /// shallow copy, use [`slice_shallow()`] instead. - pub fn slice_deep(&self, begin: usize, end: usize, step: Option) -> Self { - let len = self.len(); - let begin = begin.min(len); - let end = end.min(len); - let step = step.unwrap_or(1); - assert!(step != 0); - self.as_inner() - .slice(to_i64(begin), to_i64(end), step.try_into().unwrap(), true) - } - - /// Returns the value at the specified index as a `Variant`. To convert to a specific type, use - /// the available conversion methods on `Variant`, such as [`Variant::try_to`] or - /// [`Variant::to`]. - /// - /// # Panics - /// - /// If `index` is out of bounds. - pub fn get(&self, index: usize) -> Variant { - let ptr = self.ptr(index); - // SAFETY: `ptr` just verified that the index is not out of bounds. - unsafe { (*ptr).clone() } - } - - /// Returns the first element in the array, or `None` if the array is empty. Equivalent of - /// `front()` in GDScript. - pub fn first(&self) -> Option { - (!self.is_empty()).then(|| self.as_inner().front()) - } - - /// Returns the last element in the array, or `None` if the array is empty. Equivalent of - /// `back()` in GDScript. - pub fn last(&self) -> Option { - (!self.is_empty()).then(|| self.as_inner().back()) - } - - /// Finds the index of an existing value in a sorted array using binary search. Equivalent of - /// `bsearch` in GDScript. - /// - /// If the value is not present in the array, returns the insertion index that would maintain - /// sorting order. - /// - /// Calling `binary_search` on an unsorted array results in unspecified behavior. - pub fn binary_search(&self, value: Variant) -> usize { - to_usize(self.as_inner().bsearch(value, true)) - } - - /// Returns the number of times a value is in the array. - pub fn count(&self, value: Variant) -> usize { - to_usize(self.as_inner().count(value)) - } - - /// Returns `true` if the array contains the given value. Equivalent of `has` in GDScript. - pub fn contains(&self, value: Variant) -> bool { - self.as_inner().has(value) - } - - /// Searches the array for the first occurrence of a value and returns its index, or `None` if - /// not found. Starts searching at index `from`; pass `None` to search the entire array. - pub fn find(&self, value: Variant, from: Option) -> Option { - let from = to_i64(from.unwrap_or(0)); - let index = self.as_inner().find(value, from); - if index >= 0 { - Some(index.try_into().unwrap()) - } else { - None - } - } - - /// Searches the array backwards for the last occurrence of a value and returns its index, or - /// `None` if not found. Starts searching at index `from`; pass `None` to search the entire - /// array. - pub fn rfind(&self, value: Variant, from: Option) -> Option { - let from = from.map(to_i64).unwrap_or(-1); - let index = self.as_inner().rfind(value, from); - // It's not documented, but `rfind` returns -1 if not found. - if index >= 0 { - Some(to_usize(index)) - } else { - None - } - } - - /// Returns the minimum value contained in the array if all elements are of comparable types. - /// If the elements can't be compared or the array is empty, `None` is returned. - pub fn min(&self) -> Option { - let min = self.as_inner().min(); - (!min.is_nil()).then_some(min) - } - - /// Returns the maximum value contained in the array if all elements are of comparable types. - /// If the elements can't be compared or the array is empty, `None` is returned. - pub fn max(&self) -> Option { - let max = self.as_inner().max(); - (!max.is_nil()).then_some(max) - } - - /// Returns a random element from the array, or `None` if it is empty. - pub fn pick_random(&self) -> Option { - (!self.is_empty()).then(|| self.as_inner().pick_random()) - } - - /// Sets the value at the specified index as a `Variant`. To convert a specific type (which - /// implements `ToVariant`) to a variant, call [`ToVariant::to_variant`] on it. - /// - /// # Panics - /// - /// If `index` is out of bounds. - pub fn set(&mut self, index: usize, value: Variant) { - let ptr_mut = self.ptr_mut(index); - // SAFETY: `ptr_mut` just checked that the index is not out of bounds. - unsafe { - *ptr_mut = value; - } - } - - /// Appends an element to the end of the array. Equivalent of `append` and `push_back` in - /// GDScript. - pub fn push(&mut self, value: Variant) { - self.as_inner().push_back(value); - } - - /// Adds an element at the beginning of the array. See also `push`. - /// - /// Note: On large arrays, this method is much slower than `push` as it will move all the - /// array's elements. The larger the array, the slower `push_front` will be. - pub fn push_front(&mut self, value: Variant) { - self.as_inner().push_front(value); - } - - /// Removes and returns the last element of the array. Returns `None` if the array is empty. - /// Equivalent of `pop_back` in GDScript. - pub fn pop(&mut self) -> Option { - (!self.is_empty()).then(|| self.as_inner().pop_back()) - } - - /// Removes and returns the first element of the array. Returns `None` if the array is empty. - /// - /// Note: On large arrays, this method is much slower than `pop` as it will move all the - /// array's elements. The larger the array, the slower `pop_front` will be. - pub fn pop_front(&mut self) -> Option { - (!self.is_empty()).then(|| self.as_inner().pop_front()) - } - - /// Inserts a new element at a given index in the array. The index must be valid, or at the end - /// of the array (`index == len()`). - /// - /// Note: On large arrays, this method is much slower than `push` as it will move all the - /// array's elements after the inserted element. The larger the array, the slower `insert` will - /// be. - pub fn insert(&mut self, index: usize, value: Variant) { - let len = self.len(); - assert!( - index <= len, - "Array insertion index {index} is out of bounds: length is {len}", - ); - self.as_inner().insert(to_i64(index), value); - } - - /// Removes and returns the element at the specified index. Equivalent of `pop_at` in GDScript. - /// - /// On large arrays, this method is much slower than `pop_back` as it will move all the array's - /// elements after the removed element. The larger the array, the slower `remove` will be. - /// - /// # Panics - /// - /// If `index` is out of bounds. - pub fn remove(&mut self, index: usize) -> Variant { - self.check_bounds(index); - self.as_inner().pop_at(to_i64(index)) - } - - /// Removes the first occurrence of a value from the array. If the value does not exist in the - /// array, nothing happens. To remove an element by index, use `remove` instead. - /// - /// On large arrays, this method is much slower than `pop_back` as it will move all the array's - /// elements after the removed element. The larger the array, the slower `remove` will be. - pub fn erase(&mut self, value: Variant) { - self.as_inner().erase(value); - } - - /// Assigns the given value to all elements in the array. This can be used together with - /// `resize` to create an array with a given size and initialized elements. - pub fn fill(&mut self, value: Variant) { - self.as_inner().fill(value); - } - - /// Appends another array at the end of this array. Equivalent of `append_array` in GDScript. - pub fn extend_array(&mut self, other: Array) { - self.as_inner().append_array(other); - } - - /// Reverses the order of the elements in the array. - pub fn reverse(&mut self) { - self.as_inner().reverse(); - } - - /// Sorts the array. - /// - /// Note: The sorting algorithm used is not - /// [stable](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability). This means that values - /// considered equal may have their order changed when using `sort_unstable`. - pub fn sort_unstable(&mut self) { - self.as_inner().sort(); - } - - /// Shuffles the array such that the items will have a random order. This method uses the - /// global random number generator common to methods such as `randi`. Call `randomize` to - /// ensure that a new seed will be used each time if you want non-reproducible shuffling. - pub fn shuffle(&mut self) { - self.as_inner().shuffle(); - } - - /// Asserts that the given index refers to an existing element. - /// - /// # Panics - /// - /// If `index` is out of bounds. - fn check_bounds(&self, index: usize) { - let len = self.len(); - assert!( - index < len, - "Array index {index} is out of bounds: length is {len}", - ); - } - - /// Returns a pointer to the element at the given index. - /// - /// # Panics - /// - /// If `index` is out of bounds. - fn ptr(&self, index: usize) -> *const Variant { - self.check_bounds(index); - // SAFETY: We just checked that the index is not out of bounds. - let ptr = unsafe { self.ptr_unchecked(index) }; - assert!(!ptr.is_null()); - ptr - } - - /// Returns a mutable pointer to the element at the given index. - /// - /// # Panics - /// - /// If `index` is out of bounds. - fn ptr_mut(&self, index: usize) -> *mut Variant { - self.check_bounds(index); - // SAFETY: We just checked that the index is not out of bounds. - let ptr = unsafe { self.ptr_mut_unchecked(index) }; - assert!(!ptr.is_null()); - ptr - } - - /// Returns a pointer to the element at the given index. - /// - /// # Safety - /// - /// Calling this with an out-of-bounds index is undefined behavior. - unsafe fn ptr_unchecked(&self, index: usize) -> *const Variant { - let item_ptr: sys::GDExtensionVariantPtr = - (interface_fn!(array_operator_index_const))(self.sys(), to_i64(index)); - item_ptr as *const Variant - } - - /// Returns a mutable pointer to the element at the given index. - /// - /// # Safety - /// - /// Calling this with an out-of-bounds index is undefined behavior. - unsafe fn ptr_mut_unchecked(&self, index: usize) -> *mut Variant { - let item_ptr: sys::GDExtensionVariantPtr = - (interface_fn!(array_operator_index))(self.sys(), to_i64(index)); - item_ptr as *mut Variant - } - - #[doc(hidden)] - pub fn as_inner(&self) -> inner::InnerArray { - inner::InnerArray::from_outer(self) - } -} - -/// Creates an `Array` from the given Rust array. Each element is converted to a `Variant`. -impl From<&[T; N]> for Array { - fn from(arr: &[T; N]) -> Self { - Self::from(&arr[..]) - } -} - -/// Creates an `Array` from the given slice. Each element is converted to a `Variant`. -impl From<&[T]> for Array { - fn from(slice: &[T]) -> Self { - let mut array = Self::new(); - let len = slice.len(); - if len == 0 { - return array; - } - array.resize(len); - let ptr = array.ptr_mut(0); - for (i, element) in slice.iter().enumerate() { - // SAFETY: The array contains exactly `len` elements, stored contiguously in memory. - unsafe { - *ptr.offset(to_isize(i)) = element.to_variant(); - } - } - array - } -} - -/// Creates an `Array` from an iterator. Each element is converted to a `Variant`. -impl FromIterator for Array { - fn from_iter>(iter: I) -> Self { - let mut array = Array::new(); - array.extend(iter); - array - } -} - -/// Extends an `Array` with the contents of an iterator. Each element is converted to a `Variant`. -impl Extend for Array { - fn extend>(&mut self, iter: I) { - // Unfortunately the GDExtension API does not offer the equivalent of `Vec::reserve`. - // Otherwise we could use it to pre-allocate based on `iter.size_hint()`. - // - // A faster implementation using `resize()` and direct pointer writes might still be - // possible. - for item in iter.into_iter() { - self.push(item.to_variant()); - } - } -} - -pub struct ArrayIterator<'a> { - array: &'a Array, - next_idx: usize, - _phantom: PhantomData<&'a Array>, -} - -impl<'a> Iterator for ArrayIterator<'a> { - type Item = Variant; - - fn next(&mut self) -> Option { - if self.next_idx < self.array.len() { - let idx = self.next_idx; - self.next_idx += 1; - // Using `ptr_unchecked` rather than going through `get()` so we can avoid a second - // bounds check. - // SAFETY: We just checked that the index is not out of bounds. - Some(unsafe { (*self.array.ptr_unchecked(idx)).clone() }) - } else { - None - } - } -} - -impl fmt::Debug for Array { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // Going through `Variant` because there doesn't seem to be a direct way. - write!(f, "{:?}", self.to_variant().stringify()) - } -} - -/// Creates a new reference to the data in this array. Changes to the original array will be -/// reflected in the copy and vice versa. -/// -/// To create a (mostly) independent copy instead, see [`Array::duplicate_shallow()`] and -/// [`Array::duplicate_deep()`]. -impl Share for Array { - fn share(&self) -> Self { - unsafe { - Self::from_sys_init(|self_ptr| { - let ctor = ::godot_ffi::builtin_fn!(array_construct_copy); - let args = [self.sys_const()]; - ctor(self_ptr, args.as_ptr()); - }) - } - } -} - -impl_builtin_traits! { - for Array { - Default => array_construct_default; - Drop => array_destroy; - PartialEq => array_operator_equal; - } -} - -impl GodotFfi for Array { - ffi_methods! { - type sys::GDExtensionTypePtr = *mut Opaque; - fn from_sys; - fn sys; - fn write_sys; - } - - unsafe fn from_sys_init(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Self { - // Can't use uninitialized pointer -- Array CoW implementation in C++ expects that on - // assignment, the target CoW pointer is either initialized or nullptr - - let mut result = Self::default(); - init_fn(result.sys_mut()); - result - } -} - -#[repr(C)] -pub struct TypedArray { - opaque: OpaqueArray, - _phantom: PhantomData, -} -impl TypedArray { - fn from_opaque(opaque: OpaqueArray) -> Self { - Self { - opaque, - _phantom: PhantomData, - } - } -} - -impl Clone for TypedArray { - fn clone(&self) -> Self { - unsafe { - Self::from_sys_init(|self_ptr| { - let ctor = ::godot_ffi::builtin_fn!(array_construct_copy); - let args = [self.sys_const()]; - ctor(self_ptr, args.as_ptr()); - }) - } - } -} - -// TODO enable this: -// impl_builtin_traits! { -// for TypedArray { -// Clone => array_construct_copy; -// Drop => array_destroy; -// } -// } - -impl GodotFfi for TypedArray { - ffi_methods! { type sys::GDExtensionTypePtr = *mut Opaque; .. } -} - -impl Drop for TypedArray { - fn drop(&mut self) { - unsafe { - let destructor = sys::builtin_fn!(array_destroy @1); - destructor(self.sys_mut()); - } - } -} - -impl TypedArray { - pub fn get(&self, index: i64) -> Option { - unsafe { - let ptr = (interface_fn!(array_operator_index))(self.sys(), index); - let v = Variant::from_var_sys(ptr); - T::try_from_variant(&v).ok() - } - } -} diff --git a/godot-core/src/builtin/macros.rs b/godot-core/src/builtin/macros.rs index 7bda1c20c..94001535e 100644 --- a/godot-core/src/builtin/macros.rs +++ b/godot-core/src/builtin/macros.rs @@ -108,9 +108,13 @@ macro_rules! impl_builtin_traits_inner { } }; + // TODO remove; use godot-core/src/builtin/variant/impls.rs instead (this one is only used for Callable) ( FromVariant for $Type:ty => $gd_method:ident ) => { impl $crate::builtin::variant::FromVariant for $Type { fn try_from_variant(variant: &$crate::builtin::Variant) -> Result { + if variant.get_type() != ::variant_type() { + return Err($crate::builtin::variant::VariantConversionError) + } let result = unsafe { Self::from_sys_init(|self_ptr| { let converter = sys::builtin_fn!($gd_method); diff --git a/godot-core/src/builtin/meta/class_name.rs b/godot-core/src/builtin/meta/class_name.rs index d8f96eee2..f698bb65b 100644 --- a/godot-core/src/builtin/meta/class_name.rs +++ b/godot-core/src/builtin/meta/class_name.rs @@ -19,7 +19,7 @@ pub struct ClassName { } impl ClassName { - pub fn new() -> Self { + pub fn of() -> Self { Self { backing: StringName::from(T::CLASS_NAME), } @@ -36,6 +36,12 @@ impl ClassName { } } +impl From for StringName { + fn from(class_name: ClassName) -> Self { + class_name.backing + } +} + impl Display for ClassName { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { self.backing.fmt(f) diff --git a/godot-core/src/builtin/meta/mod.rs b/godot-core/src/builtin/meta/mod.rs index 1311d7473..c41222a66 100644 --- a/godot-core/src/builtin/meta/mod.rs +++ b/godot-core/src/builtin/meta/mod.rs @@ -22,10 +22,14 @@ use godot_ffi as sys; pub trait VariantMetadata { fn variant_type() -> VariantType; + fn class_name() -> ClassName { + ClassName::of::<()>() // FIXME Option or so + } + fn property_info(property_name: &str) -> PropertyInfo { PropertyInfo::new( Self::variant_type(), - ClassName::new::<()>(), // FIXME Option or so + Self::class_name(), StringName::from(property_name), ) } diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index 60ad180f4..0b00d15af 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -35,7 +35,7 @@ mod macros; mod vector_macros; -mod arrays; +mod array; mod color; mod dictionary; mod math; @@ -54,9 +54,10 @@ mod vector4i; pub mod meta; -pub use crate::dict; +// Re-export macros. +pub use crate::{array, dict}; -pub use arrays::*; +pub use array::*; pub use color::*; pub use dictionary::*; pub use math::*; diff --git a/godot-core/src/builtin/variant/impls.rs b/godot-core/src/builtin/variant/impls.rs index ca91560a4..b0c8f9728 100644 --- a/godot-core/src/builtin/variant/impls.rs +++ b/godot-core/src/builtin/variant/impls.rs @@ -14,6 +14,18 @@ use sys::GodotFfi; // ---------------------------------------------------------------------------------------------------------------------------------------------- // Macro definitions +macro_rules! impl_variant_metadata { + ($T:ty, $variant_type:ident $( ; $($extra:tt)* )?) => { + impl VariantMetadata for $T { + fn variant_type() -> VariantType { + VariantType::$variant_type + } + + $($($extra)*)? + } + }; +} + macro_rules! impl_variant_traits { ($T:ty, $from_fn:ident, $to_fn:ident, $variant_type:ident) => { impl_variant_traits!(@@ $T, $from_fn, $to_fn, $variant_type;); @@ -62,13 +74,7 @@ macro_rules! impl_variant_traits { } } - impl VariantMetadata for $T { - fn variant_type() -> VariantType { - VariantType::$variant_type - } - - $($extra)* - } + impl_variant_metadata!($T, $variant_type; $($extra)*); }; } @@ -144,21 +150,19 @@ mod impls { impl_variant_traits!(GodotString, string_to_variant, string_from_variant, String); impl_variant_traits!(StringName, string_name_to_variant, string_name_from_variant, StringName); impl_variant_traits!(NodePath, node_path_to_variant, node_path_from_variant, NodePath); - /* TODO provide those, as soon as `Default` is available. Also consider auto-generating. - impl_variant_traits!(Rect2, rect2_to_variant, rect2_from_variant, Rect2); - impl_variant_traits!(Rect2i, rect2i_to_variant, rect2i_from_variant, Rect2i); - impl_variant_traits!(Plane, plane_to_variant, plane_from_variant, Plane); - impl_variant_traits!(Quaternion, quaternion_to_variant, quaternion_from_variant, Quaternion); - impl_variant_traits!(Aabb, aabb_to_variant, aabb_from_variant, AABB); - impl_variant_traits!(Basis, basis_to_variant, basis_from_variant, Basis); - impl_variant_traits!(Transform2D, transform_2d_to_variant, transform_2d_from_variant, Transform2D); - impl_variant_traits!(Transform3D, transform_3d_to_variant, transform_3d_from_variant, Transform3D); - impl_variant_traits!(Projection, projection_to_variant, projection_from_variant, Projection); - impl_variant_traits!(Rid, rid_to_variant, rid_from_variant, RID); - impl_variant_traits!(Callable, callable_to_variant, callable_from_variant, Callable); - impl_variant_traits!(Signal, signal_to_variant, signal_from_variant, Signal); - */ - impl_variant_traits!(Array, array_to_variant, array_from_variant, Array); + // TODO use impl_variant_traits!, as soon as `Default` is available. Also consider auto-generating. + impl_variant_metadata!(Rect2, /* rect2_to_variant, rect2_from_variant, */ Rect2); + impl_variant_metadata!(Rect2i, /* rect2i_to_variant, rect2i_from_variant, */ Rect2i); + impl_variant_metadata!(Plane, /* plane_to_variant, plane_from_variant, */ Plane); + impl_variant_metadata!(Quaternion, /* quaternion_to_variant, quaternion_from_variant, */ Quaternion); + impl_variant_metadata!(Aabb, /* aabb_to_variant, aabb_from_variant, */ Aabb); + impl_variant_metadata!(Basis, /* basis_to_variant, basis_from_variant, */ Basis); + impl_variant_metadata!(Transform2D, /* transform_2d_to_variant, transform_2d_from_variant, */ Transform2D); + impl_variant_metadata!(Transform3D, /* transform_3d_to_variant, transform_3d_from_variant, */ Transform3D); + impl_variant_metadata!(Projection, /* projection_to_variant, projection_from_variant, */ Projection); + impl_variant_metadata!(Rid, /* rid_to_variant, rid_from_variant, */ Rid); + impl_variant_metadata!(Callable, /* callable_to_variant, callable_from_variant, */ Callable); + impl_variant_metadata!(Signal, /* signal_to_variant, signal_from_variant, */ Signal); impl_variant_traits!(PackedByteArray, packed_byte_array_to_variant, packed_byte_array_from_variant, PackedByteArray); impl_variant_traits!(PackedInt32Array, packed_int32_array_to_variant, packed_int32_array_from_variant, PackedInt32Array); impl_variant_traits!(PackedInt64Array, packed_int64_array_to_variant, packed_int64_array_from_variant, PackedInt64Array); @@ -215,7 +219,8 @@ impl FromVariant for Variant { // Variant itself impl VariantMetadata for Variant { fn variant_type() -> VariantType { - VariantType::Nil // FIXME is this correct? what else to use? is this called at all? + // Arrays use the `NIL` type to indicate that they are untyped. + VariantType::Nil } } diff --git a/godot-core/src/obj/gd.rs b/godot-core/src/obj/gd.rs index 235f1ee4d..3d38fd5ad 100644 --- a/godot-core/src/obj/gd.rs +++ b/godot-core/src/obj/gd.rs @@ -14,8 +14,8 @@ use godot_ffi::VariantType; use sys::types::OpaqueObject; use sys::{ffi_methods, interface_fn, static_assert_eq_size, GodotFfi}; -use crate::builtin::meta::{ClassName, PropertyInfo, VariantMetadata}; -use crate::builtin::{FromVariant, StringName, ToVariant, Variant, VariantConversionError}; +use crate::builtin::meta::{ClassName, VariantMetadata}; +use crate::builtin::{FromVariant, ToVariant, Variant, VariantConversionError}; use crate::obj::dom::Domain as _; use crate::obj::mem::Memory as _; use crate::obj::{cap, dom, mem, GodotClass, Inherits, Share}; @@ -332,7 +332,7 @@ impl Gd { where U: GodotClass, { - let class_name = ClassName::new::(); + let class_name = ClassName::of::(); let class_tag = interface_fn!(classdb_get_class_tag)(class_name.string_sys()); let cast_object_ptr = interface_fn!(object_cast_to)(self.obj_sys(), class_tag); @@ -631,11 +631,7 @@ impl VariantMetadata for Gd { VariantType::Object } - fn property_info(property_name: &str) -> PropertyInfo { - PropertyInfo::new( - Self::variant_type(), - ClassName::new::(), - StringName::from(property_name), - ) + fn class_name() -> ClassName { + ClassName::of::() } } diff --git a/godot-core/src/registry.rs b/godot-core/src/registry.rs index 287a34cdd..5d4c8eb63 100644 --- a/godot-core/src/registry.rs +++ b/godot-core/src/registry.rs @@ -117,7 +117,7 @@ pub fn register_class() // TODO: provide overloads with only some trait impls out!("Manually register class {}", std::any::type_name::()); - let class_name = ClassName::new::(); + let class_name = ClassName::of::(); let godot_params = sys::GDExtensionClassCreationInfo { to_string_func: Some(callbacks::to_string::), @@ -133,7 +133,7 @@ pub fn register_class() register_class_raw(ClassRegistrationInfo { class_name, - parent_class_name: Some(ClassName::new::()), + parent_class_name: Some(ClassName::of::()), generated_register_fn: None, user_register_fn: Some(ErasedRegisterFn { raw: callbacks::register_class_by_builder::, @@ -287,8 +287,8 @@ pub mod callbacks { T: GodotClass, F: FnOnce(Base) -> T, { - let class_name = ClassName::new::(); - let base_class_name = ClassName::new::(); + let class_name = ClassName::of::(); + let base_class_name = ClassName::of::(); //out!("create callback: {}", class_name.backing); diff --git a/godot-macros/src/derive_godot_class.rs b/godot-macros/src/derive_godot_class.rs index e85a999a8..f98c9be0f 100644 --- a/godot-macros/src/derive_godot_class.rs +++ b/godot-macros/src/derive_godot_class.rs @@ -325,7 +325,7 @@ fn make_exports_impl(class_name: &Ident, fields: &Fields) -> TokenStream { let class_name = ::godot::builtin::StringName::from(#class_name::CLASS_NAME); let property_info = ::godot::builtin::meta::PropertyInfo::new( #variant_type, - ::godot::builtin::meta::ClassName::new::<#class_name>(), + ::godot::builtin::meta::ClassName::of::<#class_name>(), ::godot::builtin::StringName::from(#name), ); let property_info_sys = property_info.property_sys(); diff --git a/godot/src/lib.rs b/godot/src/lib.rs index be4555353..a42f3618f 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -127,8 +127,8 @@ pub use godot_core::private; /// Often-imported symbols. pub mod prelude { pub use super::bind::{godot_api, GodotClass, GodotExt}; - pub use super::builtin::dict; // Re-export macros. pub use super::builtin::*; + pub use super::builtin::{array, dict}; // Re-export macros. pub use super::engine::{ load, try_load, utilities, AudioStreamPlayer, Camera2D, Camera3D, Input, Node, Node2D, Node3D, Object, PackedScene, RefCounted, Resource, SceneTree, diff --git a/itest/godot/ManualFfiTests.gd b/itest/godot/ManualFfiTests.gd index bf1ea86d4..095642203 100644 --- a/itest/godot/ManualFfiTests.gd +++ b/itest/godot/ManualFfiTests.gd @@ -38,3 +38,23 @@ func test_export(): obj.free() node.free() + +func test_untyped_array_pass_to_user_func(): + var obj = ArrayTest.new() + var array: Array = [42, "answer"] + assert_eq(obj.pass_untyped_array(array), 2) + +func test_untyped_array_return_from_user_func(): + var obj = ArrayTest.new() + var array: Array = obj.return_untyped_array() + assert_eq(array, [42, "answer"]) + +func test_typed_array_pass_to_user_func(): + var obj = ArrayTest.new() + var array: Array[int] = [1, 2, 3] + assert_eq(obj.pass_typed_array(array), 6) + +func test_typed_array_return_from_user_func(): + var obj = ArrayTest.new() + var array: Array[int] = obj.return_typed_array(3) + assert_eq(array, [1, 2, 3]) diff --git a/itest/godot/TestRunner.gd b/itest/godot/TestRunner.gd index 2183779c4..e856234af 100644 --- a/itest/godot/TestRunner.gd +++ b/itest/godot/TestRunner.gd @@ -69,7 +69,7 @@ class _Test: return ok static func _suite_name(suite: Object) -> String: - var script := suite.get_script() + var script: Script = suite.get_script() if script: # Test suite written in GDScript. return script.resource_path.get_file().get_basename() diff --git a/itest/rust/src/array_test.rs b/itest/rust/src/array_test.rs index aa3995ee4..0e9d3d0ec 100644 --- a/itest/rust/src/array_test.rs +++ b/itest/rust/src/array_test.rs @@ -5,16 +5,19 @@ */ use crate::{expect_panic, itest}; -use godot::builtin::{Array, FromVariant, GodotString, ToVariant}; -use godot::prelude::Share; +use godot::prelude::*; pub fn run() -> bool { let mut ok = true; ok &= array_default(); ok &= array_new(); + ok &= array_eq(); + ok &= typed_array_from_to_variant(); + ok &= untyped_array_from_to_variant(); + ok &= array_from_packed_array(); ok &= array_from_iterator(); - ok &= array_from(); - ok &= array_try_to_vec(); + ok &= array_from_slice(); + ok &= array_try_into_vec(); ok &= array_iter_shared(); ok &= array_hash(); ok &= array_share(); @@ -36,6 +39,11 @@ pub fn run() -> bool { ok &= array_reverse(); ok &= array_sort(); ok &= array_shuffle(); + ok &= array_mixed_values(); + ok &= untyped_array_pass_to_godot_func(); + ok &= untyped_array_return_from_godot_func(); + ok &= typed_array_pass_to_godot_func(); + ok &= typed_array_return_from_godot_func(); ok } @@ -49,112 +57,150 @@ fn array_new() { assert_eq!(Array::new().len(), 0); } +#[itest] +fn array_eq() { + let a = array![1, 2]; + let b = array![1, 2]; + assert_eq!(a, b); + + let c = array![2, 1]; + assert_ne!(a, c); +} + +#[itest] +fn typed_array_from_to_variant() { + let array = array![1, 2]; + let variant = array.to_variant(); + let result = TypedArray::try_from_variant(&variant); + assert_eq!(result, Ok(array)); +} + +#[itest] +fn untyped_array_from_to_variant() { + let array = array![1.to_variant(), 2.to_variant()]; + let variant = array.to_variant(); + let result = Array::try_from_variant(&variant); + assert_eq!(result, Ok(array)); +} + +#[itest] +fn array_from_packed_array() { + let packed_array = PackedInt32Array::from(&[42]); + let mut array = Array::from(&packed_array); + // This tests that the resulting array doesn't secretly have a runtime type assigned to it, + // which is not reflected in our static type. It would make sense if it did, but Godot decided + // otherwise: we get an untyped array. + array.push(GodotString::from("hi").to_variant()); + assert_eq!(array, array![42.to_variant(), "hi".to_variant()]); +} + #[itest] fn array_from_iterator() { - let array = Array::from_iter([1, 2]); + let array = TypedArray::from_iter([1, 2]); assert_eq!(array.len(), 2); - assert_eq!(array.get(0), 1.to_variant()); - assert_eq!(array.get(1), 2.to_variant()); + assert_eq!(array.get(0), 1); + assert_eq!(array.get(1), 2); } #[itest] -fn array_from() { - let array = Array::from(&[1, 2]); +fn array_from_slice() { + let array = TypedArray::from(&[1, 2]); assert_eq!(array.len(), 2); - assert_eq!(array.get(0), 1.to_variant()); - assert_eq!(array.get(1), 2.to_variant()); + assert_eq!(array.get(0), 1); + assert_eq!(array.get(1), 2); } #[itest] -fn array_try_to_vec() { - let array = Array::from(&[1, 2]); - assert_eq!(array.try_to_vec::(), Ok(vec![1, 2])); +fn array_try_into_vec() { + let array = array![1, 2]; + let result = Vec::::try_from(&array); + assert_eq!(result, Ok(vec![1, 2])); } #[itest] fn array_iter_shared() { - let array = Array::from(&[1, 2]); + let array = array![1, 2]; let mut iter = array.iter_shared(); - assert_eq!(iter.next(), Some(1.to_variant())); - assert_eq!(iter.next(), Some(2.to_variant())); + assert_eq!(iter.next(), Some(1)); + assert_eq!(iter.next(), Some(2)); assert_eq!(iter.next(), None); } #[itest] fn array_hash() { - let array = Array::from(&[1, 2]); + let array = array![1, 2]; // Just testing that it converts successfully from i64 to u32. array.hash(); } #[itest] fn array_share() { - let mut array = Array::from(&[1, 2]); + let mut array = array![1, 2]; let shared = array.share(); - array.set(0, 3.to_variant()); - assert_eq!(shared.get(0), 3.to_variant()); + array.set(0, 3); + assert_eq!(shared.get(0), 3); } #[itest] fn array_duplicate_shallow() { - let subarray = Array::from(&[2, 3]); - let array = Array::from(&[1.to_variant(), subarray.to_variant()]); + let subarray = array![2, 3]; + let array = array![1.to_variant(), subarray.to_variant()]; let duplicate = array.duplicate_shallow(); - Array::try_from_variant(&duplicate.get(1)) + TypedArray::::try_from_variant(&duplicate.get(1)) .unwrap() - .set(0, 4.to_variant()); - assert_eq!(subarray.get(0), 4.to_variant()); + .set(0, 4); + assert_eq!(subarray.get(0), 4); } #[itest] fn array_duplicate_deep() { - let subarray = Array::from(&[2, 3]); - let array = Array::from(&[1.to_variant(), subarray.to_variant()]); + let subarray = array![2, 3]; + let array = array![1.to_variant(), subarray.to_variant()]; let duplicate = array.duplicate_deep(); - Array::try_from_variant(&duplicate.get(1)) + TypedArray::::try_from_variant(&duplicate.get(1)) .unwrap() - .set(0, 4.to_variant()); - assert_eq!(subarray.get(0), 2.to_variant()); + .set(0, 4); + assert_eq!(subarray.get(0), 2); } #[itest] fn array_slice_shallow() { - let array = Array::from(&[0, 1, 2, 3, 4, 5]); + let array = array![0, 1, 2, 3, 4, 5]; let slice = array.slice_shallow(5, 1, Some(-2)); - assert_eq!(slice.try_to_vec::().unwrap(), vec![5, 3]); + assert_eq!(slice, array![5, 3]); - let subarray = Array::from(&[2, 3]); - let array = Array::from(&[1.to_variant(), subarray.to_variant()]); + let subarray = array![2, 3]; + let array = array![1.to_variant(), subarray.to_variant()]; let slice = array.slice_shallow(1, 2, None); - Array::try_from_variant(&slice.get(0)) + TypedArray::::try_from_variant(&slice.get(0)) .unwrap() - .set(0, 4.to_variant()); - assert_eq!(subarray.get(0), 4.to_variant()); + .set(0, 4); + assert_eq!(subarray.get(0), 4); } #[itest] fn array_slice_deep() { - let array = Array::from(&[0, 1, 2, 3, 4, 5]); + let array = array![0, 1, 2, 3, 4, 5]; let slice = array.slice_deep(5, 1, Some(-2)); - assert_eq!(slice.try_to_vec::().unwrap(), vec![5, 3]); + assert_eq!(slice, array![5, 3]); - let subarray = Array::from(&[2, 3]); - let array = Array::from(&[1.to_variant(), subarray.to_variant()]); + let subarray = array![2, 3]; + let array = array![1.to_variant(), subarray.to_variant()]; let slice = array.slice_deep(1, 2, None); - Array::try_from_variant(&slice.get(0)) + TypedArray::::try_from_variant(&slice.get(0)) .unwrap() - .set(0, 4.to_variant()); - assert_eq!(subarray.get(0), 2.to_variant()); + .set(0, 4); + assert_eq!(subarray.get(0), 2); } #[itest] fn array_get() { - let array = Array::from(&[1, 2]); + let array = array![1, 2]; - assert_eq!(array.get(0), 1.to_variant()); - assert_eq!(array.get(1), 2.to_variant()); + assert_eq!(array.get(0), 1); + assert_eq!(array.get(1), 2); expect_panic("Array index 2 out of bounds: length is 2", || { array.get(2); }); @@ -162,10 +208,10 @@ fn array_get() { #[itest] fn array_first_last() { - let array = Array::from(&[1, 2]); + let array = array![1, 2]; - assert_eq!(array.first(), Some(1.to_variant())); - assert_eq!(array.last(), Some(2.to_variant())); + assert_eq!(array.first(), Some(1)); + assert_eq!(array.last(), Some(2)); let empty_array = Array::new(); @@ -175,41 +221,41 @@ fn array_first_last() { #[itest] fn array_binary_search() { - let array = Array::from(&[1, 2]); + let array = array![1, 3]; - assert_eq!(array.binary_search(0.to_variant()), 0); - assert_eq!(array.binary_search(1.to_variant()), 0); - assert_eq!(array.binary_search(1.5f64.to_variant()), 1); - assert_eq!(array.binary_search(2.to_variant()), 1); - assert_eq!(array.binary_search(3.to_variant()), 2); + assert_eq!(array.binary_search(&0), 0); + assert_eq!(array.binary_search(&1), 0); + assert_eq!(array.binary_search(&2), 1); + assert_eq!(array.binary_search(&3), 1); + assert_eq!(array.binary_search(&4), 2); } #[itest] fn array_find() { - let array = Array::from(&[1, 2, 1]); + let array = array![1, 2, 1]; - assert_eq!(array.find(0.to_variant(), None), None); - assert_eq!(array.find(1.to_variant(), None), Some(0)); - assert_eq!(array.find(1.to_variant(), Some(1)), Some(2)); + assert_eq!(array.find(&0, None), None); + assert_eq!(array.find(&1, None), Some(0)); + assert_eq!(array.find(&1, Some(1)), Some(2)); } #[itest] fn array_rfind() { - let array = Array::from(&[1, 2, 1]); + let array = array![1, 2, 1]; - assert_eq!(array.rfind(0.to_variant(), None), None); - assert_eq!(array.rfind(1.to_variant(), None), Some(2)); - assert_eq!(array.rfind(1.to_variant(), Some(1)), Some(0)); + assert_eq!(array.rfind(&0, None), None); + assert_eq!(array.rfind(&1, None), Some(2)); + assert_eq!(array.rfind(&1, Some(1)), Some(0)); } #[itest] fn array_min_max() { - let int_array = Array::from(&[1, 2]); + let int_array = array![1, 2]; - assert_eq!(int_array.min(), Some(1.to_variant())); - assert_eq!(int_array.max(), Some(2.to_variant())); + assert_eq!(int_array.min(), Some(1)); + assert_eq!(int_array.max(), Some(2)); - let uncomparable_array = Array::from(&[1.to_variant(), GodotString::from("two").to_variant()]); + let uncomparable_array = array![1.to_variant(), GodotString::from("two").to_variant()]; assert_eq!(uncomparable_array.min(), None); assert_eq!(uncomparable_array.max(), None); @@ -223,33 +269,33 @@ fn array_min_max() { #[itest] fn array_pick_random() { assert_eq!(Array::new().pick_random(), None); - assert_eq!(Array::from(&[1]).pick_random(), Some(1.to_variant())); + assert_eq!(array![1].pick_random(), Some(1)); } #[itest] fn array_set() { - let mut array = Array::from(&[1, 2]); + let mut array = array![1, 2]; - array.set(0, 3.to_variant()); - assert_eq!(array.get(0), 3.to_variant()); + array.set(0, 3); + assert_eq!(array.get(0), 3); expect_panic("Array index 2 out of bounds: length is 2", move || { - array.set(2, 4.to_variant()); + array.set(2, 4); }); } #[itest] fn array_push_pop() { - let mut array = Array::from(&[1, 2]); + let mut array = array![1, 2]; - array.push(3.to_variant()); - assert_eq!(array.pop(), Some(3.to_variant())); + array.push(3); + assert_eq!(array.pop(), Some(3)); - array.push_front(4.to_variant()); - assert_eq!(array.pop_front(), Some(4.to_variant())); + array.push_front(4); + assert_eq!(array.pop_front(), Some(4)); - assert_eq!(array.pop(), Some(2.to_variant())); - assert_eq!(array.pop_front(), Some(1.to_variant())); + assert_eq!(array.pop(), Some(2)); + assert_eq!(array.pop_front(), Some(1)); assert_eq!(array.pop(), None); assert_eq!(array.pop_front(), None); @@ -257,41 +303,202 @@ fn array_push_pop() { #[itest] fn array_insert() { - let mut array = Array::from(&[1, 2]); + let mut array = array![1, 2]; - array.insert(0, 3.to_variant()); - assert_eq!(array.try_to_vec::().unwrap(), vec![3, 1, 2]); + array.insert(0, 3); + assert_eq!(array, array![3, 1, 2]); - array.insert(3, 4.to_variant()); - assert_eq!(array.try_to_vec::().unwrap(), vec![3, 1, 2, 4]); + array.insert(3, 4); + assert_eq!(array, array![3, 1, 2, 4]); } #[itest] fn array_extend() { - let mut array = Array::from(&[1, 2]); - let other = Array::from(&[3, 4]); + let mut array = array![1, 2]; + let other = array![3, 4]; array.extend_array(other); - assert_eq!(array.try_to_vec::().unwrap(), vec![1, 2, 3, 4]); + assert_eq!(array, array![1, 2, 3, 4]); } #[itest] fn array_sort() { - let mut array = Array::from(&[2, 1]); + let mut array = array![2, 1]; array.sort_unstable(); - assert_eq!(array.try_to_vec::().unwrap(), vec![1, 2]); + assert_eq!(array, array![1, 2]); } #[itest] fn array_reverse() { - let mut array = Array::from(&[1, 2]); + let mut array = array![1, 2]; array.reverse(); - assert_eq!(array.try_to_vec::().unwrap(), vec![2, 1]); + assert_eq!(array, array![2, 1]); } #[itest] fn array_shuffle() { - // Since the output is random, we just test that it doesn't crash. - let mut array = Array::from(&[1i64]); + let mut array = array![1]; array.shuffle(); - assert_eq!(array.try_to_vec::().unwrap(), vec![1]); + assert_eq!(array, array![1]); +} + +#[itest] +fn array_mixed_values() { + let int = 1; + let string = GodotString::from("hello"); + let packed_array = PackedByteArray::from(&[1, 2]); + let typed_array = array![1, 2]; + let object = Object::new_alloc(); + let node = Node::new_alloc(); + let ref_counted = RefCounted::new(); + + let array = array![ + int.to_variant(), + string.to_variant(), + packed_array.to_variant(), + typed_array.to_variant(), + object.to_variant(), + node.to_variant(), + ref_counted.to_variant(), + ]; + + assert_eq!(i64::try_from_variant(&array.get(0)).unwrap(), int); + assert_eq!( + GodotString::try_from_variant(&array.get(1)).unwrap(), + string + ); + assert_eq!( + PackedByteArray::try_from_variant(&array.get(2)) + .unwrap() + .len(), + packed_array.len() + ); // TODO Use PackedByteArray Eq impl once available + assert_eq!( + TypedArray::try_from_variant(&array.get(3)).unwrap(), + typed_array + ); + assert_eq!( + Gd::::try_from_variant(&array.get(4)) + .unwrap() + .instance_id(), + object.instance_id() + ); + assert_eq!( + Gd::::try_from_variant(&array.get(5)) + .unwrap() + .instance_id(), + node.instance_id() + ); + assert_eq!( + Gd::::try_from_variant(&array.get(6)) + .unwrap() + .instance_id(), + ref_counted.instance_id() + ); + + object.free(); + node.free(); +} + +#[itest] +fn untyped_array_pass_to_godot_func() { + let mut node = Node::new_alloc(); + node.queue_free(); // Do not leak even if the test fails. + + assert_eq!( + node.callv( + StringName::from("has_signal"), + array!["tree_entered".to_variant()] + ), + true.to_variant() + ); +} + +#[itest] +fn untyped_array_return_from_godot_func() { + use godot::engine::node::InternalMode; + use godot::engine::Node; + + // There aren't many API functions that return an untyped array. + let mut node = Node::new_alloc(); + let mut child = Node::new_alloc(); + child.set_name("child_node".into()); + node.add_child(child.share(), false, InternalMode::INTERNAL_MODE_DISABLED); + node.queue_free(); // Do not leak even if the test fails. + let result = node.get_node_and_resource("child_node".into()); + + assert_eq!( + result, + array![ + child.to_variant(), + Variant::nil(), + NodePath::default().to_variant() + ] + ); +} + +// TODO All API functions that take a `TypedArray` are even more obscure and not included in +// `SELECTED_CLASSES`. Decide if this test is worth having `Texture2DArray` and `Image` and their +// ancestors in the list. +#[itest] +fn typed_array_pass_to_godot_func() { + use godot::engine::global::Error; + use godot::engine::image::Format; + use godot::engine::{Image, Texture2DArray}; + + let mut image = Image::new(); + image.set_data( + 2, + 4, + false, + Format::FORMAT_L8, + PackedByteArray::from(&[255, 0, 255, 0, 0, 255, 0, 255]), + ); + let images = array![image]; + let mut texture = Texture2DArray::new(); + let error = texture.create_from_images(images); + + assert_eq!(error, Error::OK); + assert_eq!((texture.get_width(), texture.get_height()), (2, 4)); +} + +#[itest] +fn typed_array_return_from_godot_func() { + use godot::engine::node::InternalMode; + use godot::engine::Node; + + let mut node = Node::new_alloc(); + let mut child = Node::new_alloc(); + child.set_name("child_node".into()); + node.add_child(child.share(), false, InternalMode::INTERNAL_MODE_DISABLED); + node.queue_free(); // Do not leak even if the test fails. + let children = node.get_children(false); + + assert_eq!(children, array![child]); +} + +#[derive(GodotClass, Debug)] +#[class(init, base=RefCounted)] +struct ArrayTest; + +#[godot_api] +impl ArrayTest { + #[func] + fn pass_untyped_array(&self, array: Array) -> i64 { + array.len().try_into().unwrap() + } + + #[func] + fn return_untyped_array(&self) -> Array { + array![42.to_variant(), "answer".to_variant()] + } + + #[func] + fn pass_typed_array(&self, array: TypedArray) -> i64 { + array.iter_shared().sum() + } + + #[func] + fn return_typed_array(&self, n: i64) -> TypedArray { + (1..(n + 1)).collect() + } } diff --git a/itest/rust/src/dictionary_test.rs b/itest/rust/src/dictionary_test.rs index 2ea9d85a5..1722a1eef 100644 --- a/itest/rust/src/dictionary_test.rs +++ b/itest/rust/src/dictionary_test.rs @@ -8,7 +8,7 @@ use std::collections::{HashMap, HashSet}; use crate::itest; use godot::{ - builtin::{dict, Dictionary, FromVariant, ToVariant}, + builtin::{array, dict, Dictionary, FromVariant, ToVariant}, prelude::{Share, Variant}, }; @@ -360,7 +360,6 @@ fn dictionary_find_key() { #[itest] fn dictionary_contains_keys() { - use godot::prelude::Array; let dictionary = dict! { "foo": 0, "bar": true, @@ -369,12 +368,16 @@ fn dictionary_contains_keys() { assert!(dictionary.contains_key("foo"), "key = \"foo\""); assert!(dictionary.contains_key("bar"), "key = \"bar\""); assert!( - dictionary.contains_all_keys(Array::from(&["foo", "bar"])), + dictionary.contains_all_keys(array!["foo".to_variant(), "bar".to_variant()]), "keys = [\"foo\", \"bar\"]" ); assert!(!dictionary.contains_key("missing"), "key = \"missing\""); assert!( - !dictionary.contains_all_keys(Array::from(&["foo", "bar", "missing"])), + !dictionary.contains_all_keys(array![ + "foo".to_variant(), + "bar".to_variant(), + "missing".to_variant() + ]), "keys = [\"foo\", \"bar\", \"missing\"]" ); } @@ -387,7 +390,10 @@ fn dictionary_keys_values() { "bar": true, }; - assert_eq!(dictionary.keys(), Array::from(&["foo", "bar"])); + assert_eq!( + dictionary.keys(), + array!["foo".to_variant(), "bar".to_variant()] + ); assert_eq!( dictionary.values(), Array::from(&[0.to_variant(), true.to_variant()]) diff --git a/itest/rust/src/variant_test.rs b/itest/rust/src/variant_test.rs index 8d4cbafb3..ff1c497b0 100644 --- a/itest/rust/src/variant_test.rs +++ b/itest/rust/src/variant_test.rs @@ -260,10 +260,10 @@ fn variant_conversion_fails() { Array::default().to_variant().try_to::(), Err(VariantConversionError) ); - assert_eq!( - Dictionary::default().to_variant().try_to::(), - Err(VariantConversionError) - ); + //assert_eq!( + // Dictionary::default().to_variant().try_to::(), + // Err(VariantConversionError) + //); assert_eq!( Variant::nil().to_variant().try_to::(), Err(VariantConversionError)