Skip to content

Commit b9b3f9f

Browse files
authored
Merge pull request #1033 from godot-rust/bugfix/variant-dead-objects
Fix `Variant` -> `Gd` conversions not taking into account dead objects
2 parents d7d2de6 + 6f5383e commit b9b3f9f

File tree

7 files changed

+129
-25
lines changed

7 files changed

+129
-25
lines changed

godot-codegen/src/generator/central_files.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,21 @@ pub fn make_core_central_code(api: &ExtensionApi, ctx: &mut Context) -> TokenStr
7171
#variant_type_traits
7272

7373
#[allow(dead_code)]
74+
// Exhaustive, because only new Godot minor versions add new variants, which need either godot-rust minor bump or `api-*` feature.
7475
pub enum VariantDispatch {
7576
Nil,
7677
#(
7778
#variant_ty_enumerators_pascal(#variant_ty_enumerators_rust),
7879
)*
80+
/// Special case of a `Variant` holding an object that has been destroyed.
81+
FreedObject,
7982
}
8083

8184
impl VariantDispatch {
8285
pub fn from_variant(variant: &Variant) -> Self {
8386
match variant.get_type() {
8487
VariantType::NIL => Self::Nil,
88+
VariantType::OBJECT if !variant.is_object_alive() => Self::FreedObject,
8589
#(
8690
VariantType::#variant_ty_enumerators_shout
8791
=> Self::#variant_ty_enumerators_pascal(variant.to::<#variant_ty_enumerators_rust>()),
@@ -100,6 +104,7 @@ pub fn make_core_central_code(api: &ExtensionApi, ctx: &mut Context) -> TokenStr
100104
#(
101105
Self::#variant_ty_enumerators_pascal(v) => write!(f, "{v:?}"),
102106
)*
107+
Self::FreedObject => write!(f, "<Freed Object>"),
103108
}
104109
}
105110
}

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

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

8-
use crate::arg_into_ref;
98
use crate::builtin::{
109
GString, StringName, VariantArray, VariantDispatch, VariantOperator, VariantType,
1110
};
1211
use crate::meta::error::ConvertError;
13-
use crate::meta::{ArrayElement, AsArg, FromGodot, ToGodot};
12+
use crate::meta::{arg_into_ref, ArrayElement, AsArg, FromGodot, ToGodot};
1413
use godot_ffi as sys;
1514
use std::{fmt, ptr};
1615
use sys::{ffi_methods, interface_fn, GodotFfi};
@@ -231,6 +230,18 @@ impl Variant {
231230
unsafe { interface_fn!(variant_booleanize)(self.var_sys()) != 0 }
232231
}
233232

233+
/// Assuming that this is of type `OBJECT`, checks whether the object is dead.
234+
///
235+
/// Does not check again that the variant has type `OBJECT`.
236+
pub(crate) fn is_object_alive(&self) -> bool {
237+
debug_assert_eq!(self.get_type(), VariantType::OBJECT);
238+
239+
crate::gen::utilities::is_instance_valid(self)
240+
241+
// In case there are ever problems with this approach, alternative implementation:
242+
// self.stringify() != "<Freed Object>".into()
243+
}
244+
234245
// Conversions from/to Godot C++ `Variant*` pointers
235246
ffi_methods! {
236247
type sys::GDExtensionVariantPtr = *mut Self;
@@ -468,16 +479,18 @@ impl fmt::Display for Variant {
468479

469480
impl fmt::Debug for Variant {
470481
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
471-
// Special case for arrays: avoids converting to VariantArray (the only Array type in VariantDispatch), which fails
472-
// for typed arrays and causes a panic. This can cause an infinite loop with Debug, or abort.
473-
// Can be removed if there's ever a "possibly typed" Array type (e.g. OutArray) in the library.
474-
475-
if self.get_type() == VariantType::ARRAY {
476-
// SAFETY: type is checked, and only operation is print (out data flow, no covariant in access).
477-
let array = unsafe { VariantArray::from_variant_unchecked(self) };
478-
array.fmt(f)
479-
} else {
480-
VariantDispatch::from_variant(self).fmt(f)
482+
match self.get_type() {
483+
// Special case for arrays: avoids converting to VariantArray (the only Array type in VariantDispatch),
484+
// which fails for typed arrays and causes a panic. This can cause an infinite loop with Debug, or abort.
485+
// Can be removed if there's ever a "possibly typed" Array type (e.g. OutArray) in the library.
486+
VariantType::ARRAY => {
487+
// SAFETY: type is checked, and only operation is print (out data flow, no covariant in access).
488+
let array = unsafe { VariantArray::from_variant_unchecked(self) };
489+
array.fmt(f)
490+
}
491+
492+
// VariantDispatch also includes dead objects via `FreedObject` enumerator, which maps to "<Freed Object>".
493+
_ => VariantDispatch::from_variant(self).fmt(f),
481494
}
482495
}
483496
}

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

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

8+
use godot_ffi::VariantType;
89
use std::error::Error;
910
use std::fmt;
1011

11-
use godot_ffi::VariantType;
12-
1312
use crate::builtin::Variant;
1413
use crate::meta::{ArrayTypeInfo, ClassName, ToGodot};
1514

@@ -35,6 +34,11 @@ impl ConvertError {
3534
}
3635
}
3736

37+
// /// Create a new custom error for a conversion with the value that failed to convert.
38+
// pub(crate) fn with_kind(kind: ErrorKind) -> Self {
39+
// Self { kind, value: None }
40+
// }
41+
3842
/// Create a new custom error for a conversion with the value that failed to convert.
3943
pub(crate) fn with_kind_value<V>(kind: ErrorKind, value: V) -> Self
4044
where
@@ -323,6 +327,9 @@ pub(crate) enum FromVariantError {
323327
WrongClass {
324328
expected: ClassName,
325329
},
330+
331+
/// Variant holds an object which is no longer alive.
332+
DeadObject,
326333
}
327334

328335
impl FromVariantError {
@@ -345,6 +352,7 @@ impl fmt::Display for FromVariantError {
345352
Self::WrongClass { expected } => {
346353
write!(f, "cannot convert to class {expected}")
347354
}
355+
Self::DeadObject => write!(f, "variant holds object which is no longer alive"),
348356
}
349357
}
350358
}

godot-core/src/meta/godot_convert/mod.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,7 @@ pub trait FromGodot: Sized + GodotConvert {
103103
/// If the conversion fails.
104104
fn from_variant(variant: &Variant) -> Self {
105105
Self::try_from_variant(variant).unwrap_or_else(|err| {
106-
eprintln!("FromGodot::from_variant() failed: {err}");
107-
panic!()
106+
panic!("FromGodot::from_variant() failed -- {err}");
108107
})
109108
}
110109
}

godot-core/src/obj/raw_gd.rs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::{fmt, ptr};
1010
use godot_ffi as sys;
1111
use sys::{interface_fn, GodotFfi, GodotNullableFfi, PtrcallType};
1212

13-
use crate::builtin::Variant;
13+
use crate::builtin::{Variant, VariantType};
1414
use crate::meta::error::{ConvertError, FromVariantError};
1515
use crate::meta::{
1616
CallContext, ClassName, FromGodot, GodotConvert, GodotFfiVariant, GodotType, RefArg, ToGodot,
@@ -42,6 +42,8 @@ impl<T: GodotClass> RawGd<T> {
4242
/// Initializes this `RawGd<T>` from the object pointer as a **weak ref**, meaning it does not
4343
/// initialize/increment the reference counter.
4444
///
45+
/// If `obj` is null or the instance ID query behind the object returns 0, the returned `RawGd<T>` will have the null state.
46+
///
4547
/// # Safety
4648
///
4749
/// `obj` must be a valid object pointer or a null pointer.
@@ -51,8 +53,10 @@ impl<T: GodotClass> RawGd<T> {
5153
} else {
5254
let raw_id = unsafe { interface_fn!(object_get_instance_id)(obj) };
5355

56+
// This happened originally during Variant -> RawGd conversion, but at this point it's too late to detect, and UB has already
57+
// occurred (the Variant holds the object pointer as bytes in an array, which becomes dangling the moment the actual object dies).
5458
let instance_id = InstanceId::try_from_u64(raw_id)
55-
.expect("constructed RawGd weak pointer with instance ID 0");
59+
.expect("null instance ID when constructing object; this very likely causes UB");
5660

5761
// TODO(bromeon): this should query dynamic type of object, which can be different from T (upcast, FromGodot, etc).
5862
// See comment in ObjectRtti.
@@ -576,13 +580,27 @@ impl<T: GodotClass> GodotFfiVariant for RawGd<T> {
576580
}
577581

578582
fn ffi_from_variant(variant: &Variant) -> Result<Self, ConvertError> {
579-
let raw = unsafe {
580-
// TODO(#234) replace Gd::<Object> with Self when Godot stops allowing illegal conversions
581-
// See https://github.com/godot-rust/gdext/issues/158
583+
let variant_type = variant.get_type();
582584

583-
// TODO(uninit) - see if we can use from_sys_init()
585+
// Explicit type check before calling `object_from_variant`, to allow for better error messages.
586+
if variant_type != VariantType::OBJECT {
587+
return Err(FromVariantError::BadType {
588+
expected: VariantType::OBJECT,
589+
actual: variant_type,
590+
}
591+
.into_error(variant.clone()));
592+
}
593+
594+
// Check for dead objects *before* converting. Godot doesn't care if the objects are still alive, and hitting
595+
// RawGd::from_obj_sys_weak() is too late and causes UB.
596+
if !variant.is_object_alive() {
597+
return Err(FromVariantError::DeadObject.into_error(variant.clone()));
598+
}
599+
600+
let raw = unsafe {
601+
// Uses RawGd<Object> and not Self, because Godot still allows illegal conversions. We thus check with manual casting later on.
602+
// See https://github.com/godot-rust/gdext/issues/158.
584603

585-
// raw_object_init?
586604
RawGd::<classes::Object>::new_with_uninit(|self_ptr| {
587605
let converter = sys::builtin_fn!(object_from_variant);
588606
converter(self_ptr, sys::SysPtr::force_mut(variant.var_sys()));
@@ -673,6 +691,9 @@ impl<T: GodotClass> fmt::Debug for RawGd<T> {
673691

674692
/// Runs `init_fn` on the address of a pointer (initialized to null), then returns that pointer, possibly still null.
675693
///
694+
/// This relies on the fact that an object pointer takes up the same space as the FFI representation of an object (`OpaqueObject`).
695+
/// The pointer is thus used as an opaque handle, initialized by `init_fn`, so that it represents a valid Godot object afterwards.
696+
///
676697
/// # Safety
677698
/// `init_fn` must be a function that correctly handles a _type pointer_ pointing to an _object pointer_.
678699
#[doc(hidden)]

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

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,44 @@ fn variant_bad_conversions() {
130130
.expect_err("`nil` should not convert to `Dictionary`");
131131
}
132132

133+
#[itest]
134+
fn variant_dead_object_conversions() {
135+
let obj = Node::new_alloc();
136+
let variant = obj.to_variant();
137+
138+
let result = variant.try_to::<Gd<Node>>();
139+
let gd = result.expect("Variant::to() with live object should succeed");
140+
assert_eq!(gd, obj);
141+
142+
obj.free();
143+
144+
// Verify Display + Debug impl.
145+
assert_eq!(format!("{variant}"), "<Freed Object>");
146+
assert_eq!(format!("{variant:?}"), "<Freed Object>");
147+
148+
// Variant::try_to().
149+
let result = variant.try_to::<Gd<Node>>();
150+
let err = result.expect_err("Variant::try_to::<Gd>() with dead object should fail");
151+
assert_eq!(
152+
err.to_string(),
153+
"variant holds object which is no longer alive: <Freed Object>"
154+
);
155+
156+
// Variant::to().
157+
expect_panic("Variant::to() with dead object should panic", || {
158+
let _: Gd<Node> = variant.to();
159+
});
160+
161+
// Variant::try_to() -> Option<Gd>.
162+
// This conversion does *not* return `None` for dead objects, but an error. `None` is reserved for NIL variants, see object_test.rs.
163+
let result = variant.try_to::<Option<Gd<Node>>>();
164+
let err = result.expect_err("Variant::try_to::<Option<Gd>>() with dead object should fail");
165+
assert_eq!(
166+
err.to_string(),
167+
"variant holds object which is no longer alive: <Freed Object>"
168+
);
169+
}
170+
133171
#[itest]
134172
fn variant_bad_conversion_error_message() {
135173
let variant = 123.to_variant();
@@ -139,11 +177,10 @@ fn variant_bad_conversion_error_message() {
139177
.expect_err("i32 -> GString conversion should fail");
140178
assert_eq!(err.to_string(), "cannot convert from INT to STRING: 123");
141179

142-
// TODO this error isn't great, but unclear whether it can be improved. If not, document.
143180
let err = variant
144181
.try_to::<Gd<Node>>()
145182
.expect_err("i32 -> Gd<Node> conversion should fail");
146-
assert_eq!(err.to_string(), "`Gd` cannot be null: null");
183+
assert_eq!(err.to_string(), "cannot convert from INT to OBJECT: 123");
147184
}
148185

149186
#[itest]

itest/rust/src/object_tests/object_test.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,27 @@ fn object_engine_convert_variant_error() {
535535
);
536536
}
537537

538+
#[itest]
539+
fn object_convert_variant_option() {
540+
let refc = RefCounted::new_gd();
541+
let variant = refc.to_variant();
542+
543+
// Variant -> Option<Gd>.
544+
let gd = Option::<Gd<RefCounted>>::from_variant(&variant);
545+
assert_eq!(gd, Some(refc.clone()));
546+
547+
let nil = Variant::nil();
548+
let gd = Option::<Gd<RefCounted>>::from_variant(&nil);
549+
assert_eq!(gd, None);
550+
551+
// Option<Gd> -> Variant.
552+
let back = Some(refc).to_variant();
553+
assert_eq!(back, variant);
554+
555+
let back = None::<Gd<RefCounted>>.to_variant();
556+
assert_eq!(back, Variant::nil());
557+
}
558+
538559
#[itest]
539560
fn object_engine_returned_refcount() {
540561
let Some(file) = FileAccess::open("res://itest.gdextension", file_access::ModeFlags::READ)

0 commit comments

Comments
 (0)