Skip to content

Commit 01fc5d4

Browse files
committed
Add Variant::try_to_relaxed()
1 parent 551c164 commit 01fc5d4

File tree

4 files changed

+269
-8
lines changed

4 files changed

+269
-8
lines changed

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

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
use crate::builtin::{
99
GString, StringName, VariantArray, VariantDispatch, VariantOperator, VariantType,
1010
};
11-
use crate::meta::error::ConvertError;
12-
use crate::meta::{arg_into_ref, ArrayElement, AsArg, FromGodot, ToGodot};
11+
use crate::meta::error::{ConvertError, FromVariantError};
12+
use crate::meta::{
13+
arg_into_ref, ffi_variant_type, ArrayElement, AsArg, FromGodot, GodotType, ToGodot,
14+
};
1315
use godot_ffi as sys;
1416
use std::{fmt, ptr};
1517
use sys::{ffi_methods, interface_fn, GodotFfi};
@@ -63,11 +65,55 @@ impl Variant {
6365

6466
/// Convert to type `T`, returning `Err` on failure.
6567
///
68+
/// The conversion only succeeds if the type stored in the variant matches `T`'s FFI representation.
69+
/// For lenient conversions like in GDScript, use [`try_to_relaxed()`](Self::try_to_relaxed) instead.
70+
///
6671
/// Equivalent to [`T::try_from_variant(&self)`][FromGodot::try_from_variant].
6772
pub fn try_to<T: FromGodot>(&self) -> Result<T, ConvertError> {
6873
T::try_from_variant(self)
6974
}
7075

76+
/// Convert to `T` using Godot's less strict conversion rules.
77+
///
78+
/// More lenient than [`try_to()`](Self::try_to), which only allows exact type matches.
79+
/// Enables conversions between related types that Godot considers compatible under its conversion rules.
80+
///
81+
/// Precisely matches GDScript's behavior to converts arguments, when a function declares a parameter of different type.
82+
///
83+
/// # Conversion diagram
84+
/// Exhaustive list of all possible conversions, as of Godot 4.4. The arrow `──►` means "converts to".
85+
///
86+
/// ```text
87+
/// * ───► Variant
88+
/// * ───► itself (reflexive)
89+
/// float StringName
90+
/// ▲ ▲ ▲ Vector2 ◄───► Vector2i
91+
/// ╱ ╲ │ Vector3 ◄───► Vector3i
92+
/// ▼ ▼ ▼ Vector4 ◄───► Vector4i
93+
/// bool ◄───► int GString ◄───► NodePath Rect2 ◄───► Rect2i
94+
/// ╲ ╱
95+
/// ╲ ╱ Array ◄───► Packed*Array
96+
/// ▼ ▼
97+
/// Color Gd<T> ───► Rid
98+
/// nil ───► Option<Gd<T>>
99+
///
100+
/// Basis ◄───► Quaternion
101+
/// ╲ ╱
102+
/// ╲ ╱
103+
/// ▼ ▼
104+
/// Transform2D ◄───► Transform3D ◄───► Projection
105+
/// ```
106+
///
107+
/// # Godot implementation details
108+
/// See [GDExtension interface](https://github.com/godotengine/godot/blob/4.4-stable/core/extension/gdextension_interface.h#L1353-L1364)
109+
/// and [C++ implementation](https://github.com/godotengine/godot/blob/4.4-stable/core/variant/variant.cpp#L532) (Godot 4.4 at the time of
110+
/// writing). The "strict" part refers to excluding certain conversions, such as between `int` and `GString`.
111+
///
112+
// ASCII arsenal: / ╱ ⟋ ⧸ ⁄ ╱ ↗ ╲ \ ╲ ⟍ ⧹ ∖
113+
pub fn try_to_relaxed<T: FromGodot>(&self) -> Result<T, ConvertError> {
114+
try_from_variant_relaxed(self)
115+
}
116+
71117
/// Checks whether the variant is empty (`null` value in GDScript).
72118
///
73119
/// See also [`get_type()`][Self::get_type].
@@ -530,3 +576,61 @@ impl fmt::Debug for Variant {
530576
}
531577
}
532578
}
579+
580+
fn try_from_variant_relaxed<T: FromGodot>(variant: &Variant) -> Result<T, ConvertError> {
581+
// See C++ implementation mentioned in RustDoc comment of `try_to_relaxed()`.
582+
583+
// First check if the current type can be converted using strict rules.
584+
let from_type = variant.get_type();
585+
let to_type = ffi_variant_type::<T>();
586+
587+
// If types are the same, use the regular conversion.
588+
// This is both an optimization (avoids FFI calls) and ensures consistency
589+
// between strict and relaxed conversions for identical types.
590+
if from_type == to_type {
591+
return T::try_from_variant(variant);
592+
}
593+
594+
// Non-NIL types can technically be converted to NIL according to `variant_can_convert_strict()`, however that makes no sense -- from
595+
// neither a type perspective (NIL is unit, not never type), nor a practical one. Disallow any such conversions.
596+
if to_type == VariantType::NIL || !can_convert_godot_strict(from_type, to_type) {
597+
return Err(FromVariantError::BadType {
598+
expected: to_type,
599+
actual: from_type,
600+
}
601+
.into_error(variant.clone()));
602+
}
603+
604+
// Find correct from->to conversion constructor.
605+
let converter = unsafe {
606+
let get_constructor = interface_fn!(get_variant_to_type_constructor);
607+
get_constructor(to_type.sys())
608+
};
609+
610+
// Must be available, since we checked with `variant_can_convert_strict`.
611+
let converter =
612+
converter.unwrap_or_else(|| panic!("missing converter for {from_type:?} -> {to_type:?}"));
613+
614+
// Perform actual conversion on the FFI types. The GDExtension conversion constructor only works with types supported
615+
// by Godot (i.e. GodotType), not GodotConvert (like i8).
616+
let ffi_result = unsafe {
617+
<<T::Via as GodotType>::Ffi as GodotFfi>::new_with_uninit(|result_ptr| {
618+
converter(result_ptr, sys::SysPtr::force_mut(variant.var_sys()));
619+
})
620+
};
621+
622+
// Try to convert the FFI types back to the user type. Can still fail, e.g. i64 -> i8.
623+
let via = <T::Via as GodotType>::try_from_ffi(ffi_result)?;
624+
let concrete = T::try_from_godot(via)?;
625+
626+
Ok(concrete)
627+
}
628+
629+
fn can_convert_godot_strict(from_type: VariantType, to_type: VariantType) -> bool {
630+
// Godot "strict" conversion is still quite permissive.
631+
// See Variant::can_convert_strict() in C++, https://github.com/godotengine/godot/blob/master/core/variant/variant.cpp#L532-L532.
632+
unsafe {
633+
let can_convert_fn = interface_fn!(variant_can_convert_strict);
634+
can_convert_fn(from_type.sys(), to_type.sys()) == sys::conv::SYS_TRUE
635+
}
636+
}

godot-core/src/meta/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ pub use uniform_object_deref::UniformObjectDeref;
6767

6868
pub(crate) use array_type_info::ArrayTypeInfo;
6969
pub(crate) use traits::{
70-
element_godot_type_name, element_variant_type, GodotFfiVariant, GodotNullableFfi,
70+
element_godot_type_name, ffi_variant_type, element_variant_type, GodotFfiVariant, GodotNullableFfi,
7171
};
7272

7373
use crate::registry::method::MethodParamOrReturnInfo;

godot-core/src/meta/traits.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,12 @@ pub trait ArrayElement: ToGodot + FromGodot + sealed::Sealed + meta::ParamType {
195195
// Non-polymorphic helper functions, to avoid constant `<T::Via as GodotType>::` in the code.
196196

197197
#[doc(hidden)]
198-
pub(crate) fn element_variant_type<T: ArrayElement>() -> VariantType {
198+
pub(crate) const fn element_variant_type<T: ArrayElement>() -> VariantType {
199+
<T::Via as GodotType>::Ffi::VARIANT_TYPE
200+
}
201+
202+
#[doc(hidden)]
203+
pub(crate) const fn ffi_variant_type<T: GodotConvert>() -> VariantType {
199204
<T::Via as GodotType>::Ffi::VARIANT_TYPE
200205
}
201206

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

Lines changed: 156 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,18 @@
66
*/
77

88
use std::cmp::Ordering;
9+
use std::fmt;
910
use std::fmt::Display;
1011

1112
use godot::builtin::{
12-
array, dict, varray, vslice, Array, GString, NodePath, Signal, StringName, Variant, Vector2,
13-
Vector3,
13+
array, dict, varray, vslice, Array, Color, GString, NodePath, PackedInt32Array,
14+
PackedStringArray, Projection, Quaternion, Signal, StringName, Transform2D, Transform3D,
15+
Variant, Vector2, Vector2i, Vector3, Vector3i,
1416
};
1517
use godot::builtin::{Basis, Dictionary, VariantArray, VariantOperator, VariantType};
16-
use godot::classes::{Node, Node2D};
18+
use godot::classes::{Node, Node2D, Resource};
1719
use godot::meta::{FromGodot, ToGodot};
18-
use godot::obj::{Gd, InstanceId, NewAlloc};
20+
use godot::obj::{Gd, InstanceId, NewAlloc, NewGd};
1921
use godot::sys::GodotFfi;
2022

2123
use crate::common::roundtrip;
@@ -75,6 +77,117 @@ fn variant_conversions() {
7577
roundtrip(Signal::invalid());
7678
}
7779

80+
#[itest]
81+
fn variant_relaxed_conversions() {
82+
// See https://github.com/godotengine/godot/blob/4.4-stable/core/variant/variant.cpp#L532.
83+
84+
let obj = Node::new_alloc();
85+
86+
// reflexive
87+
convert_relaxed_to(-22i8, -22i8);
88+
convert_relaxed_to("some str", GString::from("some str"));
89+
convert_relaxed_to(TEST_BASIS, TEST_BASIS);
90+
convert_relaxed_to(obj.clone(), obj.clone());
91+
92+
// int <-> float
93+
convert_relaxed_to(1234567890i64, 1234567890f64);
94+
convert_relaxed_to(1234567890f64, 1234567890i64);
95+
96+
// int <-> bool
97+
convert_relaxed_to(-123, true);
98+
convert_relaxed_to(0, false);
99+
convert_relaxed_to(true, 1);
100+
convert_relaxed_to(false, 0);
101+
102+
// float <-> bool
103+
convert_relaxed_to(123.45, true);
104+
convert_relaxed_to(0.0, false);
105+
convert_relaxed_to(true, 1.0);
106+
convert_relaxed_to(false, 0.0);
107+
108+
// GString <-> StringName
109+
convert_relaxed_to("hello", StringName::from("hello"));
110+
convert_relaxed_to(StringName::from("hello"), GString::from("hello"));
111+
112+
// GString <-> NodePath
113+
convert_relaxed_to("hello", NodePath::from("hello"));
114+
convert_relaxed_to(NodePath::from("hello"), GString::from("hello"));
115+
116+
// anything -> nil
117+
convert_relaxed_to(Variant::nil(), Variant::nil());
118+
convert_relaxed_to((), Variant::nil());
119+
convert_relaxed_fail::<()>(obj.clone());
120+
convert_relaxed_fail::<()>(123.45);
121+
convert_relaxed_fail::<()>(Vector3i::new(1, 2, 3));
122+
123+
// nil -> anything (except Variant) - fails
124+
convert_relaxed_fail::<i64>(Variant::nil());
125+
convert_relaxed_fail::<GString>(Variant::nil());
126+
convert_relaxed_fail::<Gd<Node>>(Variant::nil());
127+
convert_relaxed_fail::<VariantArray>(Variant::nil());
128+
convert_relaxed_fail::<Dictionary>(Variant::nil());
129+
130+
// Array -> Packed*Array
131+
let packed_ints = PackedInt32Array::from([1, 2, 3]);
132+
let packed_strings = PackedStringArray::from(["a".into(), "bb".into()]);
133+
let strings: Array<GString> = array!["a", "bb"];
134+
135+
convert_relaxed_to(array![1, 2, 3], packed_ints.clone());
136+
convert_relaxed_to(varray![1, 2, 3], packed_ints.clone());
137+
convert_relaxed_to(strings.clone(), packed_strings.clone());
138+
convert_relaxed_to(varray!["a", "bb"], packed_strings.clone());
139+
140+
// Packed*Array -> Array
141+
convert_relaxed_to(packed_ints.clone(), array![1, 2, 3]);
142+
convert_relaxed_to(packed_ints, varray![1, 2, 3]);
143+
convert_relaxed_to(packed_strings.clone(), strings);
144+
convert_relaxed_to(packed_strings, varray!["a", "bb"]);
145+
146+
// Object|nil -> optional Object
147+
convert_relaxed_to(obj.clone(), Some(obj.clone()));
148+
convert_relaxed_to(Variant::nil(), Option::<Gd<Node>>::None);
149+
150+
// Object -> Rid
151+
let res = Resource::new_gd();
152+
let rid = res.get_rid();
153+
convert_relaxed_to(res.clone(), rid);
154+
155+
// Vector2 <-> Vector2i
156+
convert_relaxed_to(Vector2::new(1.0, 2.0), Vector2i::new(1, 2));
157+
convert_relaxed_to(Vector2i::new(1, 2), Vector2::new(1.0, 2.0));
158+
159+
// int|String -> Color (don't use float colors due to rounding errors / 255-vs-256 imprecision).
160+
convert_relaxed_to(0xFF_80_00_40u32, Color::from_rgba8(255, 128, 0, 64));
161+
convert_relaxed_to("MEDIUM_AQUAMARINE", Color::MEDIUM_AQUAMARINE);
162+
163+
// Everything -> Transform3D
164+
convert_relaxed_to(Transform2D::IDENTITY, Transform3D::IDENTITY);
165+
convert_relaxed_to(Basis::IDENTITY, Transform3D::IDENTITY);
166+
convert_relaxed_to(Quaternion::IDENTITY, Transform3D::IDENTITY);
167+
168+
// Projection <-> Transform3D
169+
convert_relaxed_to(Projection::IDENTITY, Transform3D::IDENTITY);
170+
convert_relaxed_to(Transform3D::IDENTITY, Projection::IDENTITY);
171+
172+
// Quaternion <-> Basis
173+
convert_relaxed_to(Basis::IDENTITY, Quaternion::IDENTITY);
174+
convert_relaxed_to(Quaternion::IDENTITY, Basis::IDENTITY);
175+
176+
// Other geometric conversions between the above fail.
177+
convert_relaxed_fail::<Transform2D>(Projection::IDENTITY);
178+
convert_relaxed_fail::<Transform2D>(Quaternion::IDENTITY);
179+
convert_relaxed_fail::<Transform2D>(Basis::IDENTITY);
180+
convert_relaxed_fail::<Projection>(Transform2D::IDENTITY);
181+
convert_relaxed_fail::<Projection>(Quaternion::IDENTITY);
182+
convert_relaxed_fail::<Projection>(Basis::IDENTITY);
183+
convert_relaxed_fail::<Quaternion>(Transform2D::IDENTITY);
184+
convert_relaxed_fail::<Quaternion>(Projection::IDENTITY);
185+
convert_relaxed_fail::<Basis>(Transform2D::IDENTITY);
186+
convert_relaxed_fail::<Basis>(Projection::IDENTITY);
187+
188+
obj.free();
189+
}
190+
78191
#[itest]
79192
fn variant_bad_integer_conversions() {
80193
truncate_bad::<i8>(128);
@@ -550,6 +663,45 @@ fn variant_hash() {
550663

551664
// ----------------------------------------------------------------------------------------------------------------------------------------------
552665

666+
fn convert_relaxed_to<T, U>(from: T, expected_to: U)
667+
where
668+
T: ToGodot + fmt::Debug,
669+
U: FromGodot + PartialEq + fmt::Debug,
670+
{
671+
let variant = from.to_variant();
672+
let result = variant.try_to_relaxed::<U>();
673+
674+
match result {
675+
Ok(to) => {
676+
assert_eq!(
677+
to, expected_to,
678+
"converting {from:?} to {to:?} resulted in unexpected value"
679+
);
680+
}
681+
Err(err) => {
682+
panic!("Conversion from {from:?} to {expected_to:?} failed: {err}");
683+
}
684+
}
685+
}
686+
687+
fn convert_relaxed_fail<U>(from: impl ToGodot + fmt::Debug)
688+
where
689+
U: FromGodot + PartialEq + fmt::Debug,
690+
{
691+
let variant = from.to_variant();
692+
let result = variant.try_to_relaxed::<U>();
693+
694+
match result {
695+
Ok(to) => {
696+
let to_type = godot::sys::short_type_name::<U>();
697+
panic!(
698+
"Conversion from {from:?} to {to_type:?} unexpectedly succeeded with value: {to:?}"
699+
);
700+
}
701+
Err(_err) => {}
702+
}
703+
}
704+
553705
fn truncate_bad<T>(original_value: i64)
554706
where
555707
T: FromGodot + Display,

0 commit comments

Comments
 (0)