Skip to content

Commit 77a8739

Browse files
committed
Debug impl for Variant now holds objects, to not modify ref-count
Previously, Debug for Variant converted objects by creating a new Gd<T> pointer. This is subject to the observer effect, and changes the reference count of objects being inspected. Use an alternative direct way to fetch data, using Variant::call().
1 parent db78f6d commit 77a8739

File tree

5 files changed

+99
-46
lines changed

5 files changed

+99
-46
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use crate::builtin::{
99
GString, StringName, VariantArray, VariantDispatch, VariantOperator, VariantType,
1010
};
11+
use crate::classes;
1112
use crate::meta::error::{ConvertError, FromVariantError};
1213
use crate::meta::{
1314
arg_into_ref, ffi_variant_type, ArrayElement, AsArg, ExtVariantType, FromGodot, GodotType,
@@ -583,6 +584,10 @@ impl fmt::Debug for Variant {
583584
array.fmt(f)
584585
}
585586

587+
// Converting to objects before printing causes their refcount to increment, leading to an Observer effect
588+
// where `Debug` actually changes the object statistics. As such, fetch information without instantiating Gd<T>.
589+
VariantType::OBJECT => classes::debug_string_variant(self, f, "VariantGd"),
590+
586591
// VariantDispatch also includes dead objects via `FreedObject` enumerator, which maps to "<Freed Object>".
587592
_ => VariantDispatch::from_variant(self).fmt(f),
588593
}

godot-core/src/classes/class_runtime.rs

Lines changed: 79 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
//! Runtime checks and inspection of Godot classes.
99
10-
use crate::builtin::{GString, StringName};
10+
use crate::builtin::{GString, StringName, Variant};
1111
#[cfg(debug_assertions)]
1212
use crate::classes::{ClassDb, Object};
1313
use crate::meta::CallContext;
@@ -22,27 +22,67 @@ pub(crate) fn debug_string<T: GodotClass>(
2222
ty: &str,
2323
) -> std::fmt::Result {
2424
if let Some(id) = obj.instance_id_or_none() {
25-
// Sample the reference count FIRST, before any other operations that might affect it.
26-
// This avoids the +1 temporary reference from subsequent FFI calls during debug formatting.
27-
let refcount = obj.maybe_refcount();
28-
2925
let class: StringName = obj.dynamic_class_string();
26+
debug_string_parts(f, ty, id, class, obj.maybe_refcount(), None)
27+
} else {
28+
write!(f, "{ty} {{ freed obj }}")
29+
}
30+
}
3031

31-
let mut builder = f.debug_struct(ty);
32-
builder
33-
.field("id", &id.to_i64())
34-
.field("class", &format_args!("{class}"));
35-
36-
if let Some(refcount) = refcount {
37-
builder.field("refc", &refcount);
38-
}
39-
40-
builder.finish()
32+
#[cfg(since_api = "4.4")]
33+
pub(crate) fn debug_string_variant(
34+
obj: &Variant,
35+
f: &mut std::fmt::Formatter<'_>,
36+
ty: &str,
37+
) -> std::fmt::Result {
38+
let id = obj
39+
.object_id_unchecked()
40+
.expect("Variant must be of type OBJECT");
41+
42+
if id.lookup_validity() {
43+
// Object::get_class() currently returns String, but this is future-proof if the return type changes to StringName.
44+
let class = obj
45+
.call("get_class", &[])
46+
.try_to_relaxed::<StringName>()
47+
.expect("get_class() must be compatible with StringName");
48+
49+
let refcount = id.is_ref_counted().then(|| {
50+
obj.call("get_reference_count", &[])
51+
.try_to_relaxed::<i32>()
52+
.expect("get_reference_count() must return integer") as usize
53+
});
54+
55+
debug_string_parts(f, ty, id, class, refcount, None)
4156
} else {
4257
write!(f, "{ty} {{ freed obj }}")
4358
}
4459
}
4560

61+
// Polyfill for Godot < 4.4, where Variant::object_id_unchecked() is not available.
62+
#[cfg(before_api = "4.4")]
63+
pub(crate) fn debug_string_variant(
64+
obj: &Variant,
65+
f: &mut std::fmt::Formatter<'_>,
66+
ty: &str,
67+
) -> std::fmt::Result {
68+
debug_assert_eq!(obj.get_type(), VariantType::OBJECT);
69+
70+
match obj.try_to::<Gd<crate::classes::Object>>() {
71+
Ok(obj) => {
72+
let id = obj.instance_id(); // Guaranteed valid, since conversion would have failed otherwise.
73+
let class = obj.dynamic_class_string();
74+
75+
// Refcount is off-by-one due to now-created Gd<T> from conversion; correct by -1.
76+
let refcount = obj.maybe_refcount().map(|rc| rc.saturating_sub(1));
77+
78+
debug_string_parts(f, ty, id, class, refcount, None)
79+
}
80+
Err(_) => {
81+
write!(f, "{ty} {{ freed obj }}")
82+
}
83+
}
84+
}
85+
4686
pub(crate) fn debug_string_nullable<T: GodotClass>(
4787
obj: &RawGd<T>,
4888
f: &mut std::fmt::Formatter<'_>,
@@ -67,25 +107,35 @@ pub(crate) fn debug_string_with_trait<T: GodotClass>(
67107
trt: &str,
68108
) -> std::fmt::Result {
69109
if let Some(id) = obj.instance_id_or_none() {
70-
// Sample the reference count FIRST, before any other operations that might affect it.
71-
let refcount = obj.maybe_refcount();
72-
73110
let class: StringName = obj.dynamic_class_string();
111+
debug_string_parts(f, ty, id, class, obj.maybe_refcount(), Some(trt))
112+
} else {
113+
write!(f, "{ty} {{ freed obj }}")
114+
}
115+
}
74116

75-
let mut builder = f.debug_struct(ty);
76-
builder
77-
.field("id", &id.to_i64())
78-
.field("class", &format_args!("{class}"))
79-
.field("trait", &format_args!("{trt}"));
117+
fn debug_string_parts(
118+
f: &mut std::fmt::Formatter<'_>,
119+
ty: &str,
120+
id: InstanceId,
121+
class: StringName,
122+
refcount: Option<usize>,
123+
trait_name: Option<&str>,
124+
) -> std::fmt::Result {
125+
let mut builder = f.debug_struct(ty);
126+
builder
127+
.field("id", &id.to_i64())
128+
.field("class", &format_args!("{class}"));
80129

81-
if let Some(refcount) = refcount {
82-
builder.field("refc", &refcount);
83-
}
130+
if let Some(trait_name) = trait_name {
131+
builder.field("trait", &format_args!("{trait_name}"));
132+
}
84133

85-
builder.finish()
86-
} else {
87-
write!(f, "{ty} {{ freed obj }}")
134+
if let Some(refcount) = refcount {
135+
builder.field("refc", &refcount);
88136
}
137+
138+
builder.finish()
89139
}
90140

91141
pub(crate) fn display_string<T: GodotClass>(

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,14 +261,14 @@ fn variant_dead_object_conversions() {
261261

262262
// Verify Display + Debug impl.
263263
assert_eq!(format!("{variant}"), "<Freed Object>");
264-
assert_eq!(format!("{variant:?}"), "<Freed Object>");
264+
assert_eq!(format!("{variant:?}"), "VariantGd { freed obj }");
265265

266266
// Variant::try_to().
267267
let result = variant.try_to::<Gd<Node>>();
268268
let err = result.expect_err("Variant::try_to::<Gd>() with dead object should fail");
269269
assert_eq!(
270270
err.to_string(),
271-
"variant holds object which is no longer alive: <Freed Object>"
271+
"variant holds object which is no longer alive: VariantGd { freed obj }"
272272
);
273273

274274
// Variant::to().
@@ -282,7 +282,7 @@ fn variant_dead_object_conversions() {
282282
let err = result.expect_err("Variant::try_to::<Option<Gd>>() with dead object should fail");
283283
assert_eq!(
284284
err.to_string(),
285-
"variant holds object which is no longer alive: <Freed Object>"
285+
"variant holds object which is no longer alive: VariantGd { freed obj }"
286286
);
287287
}
288288

itest/rust/src/object_tests/dyn_gd_test.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -375,15 +375,16 @@ fn dyn_gd_error_unregistered_trait() {
375375
let node = node.into_gd();
376376

377377
let err = back.expect_err("DynGd::try_to() should have failed");
378-
let expected_err =
379-
format!("trait `dyn UnrelatedTrait` has not been registered with #[godot_dyn]: {node:?}");
378+
let expected_err = // Variant Debug uses "VariantGd" prefix.
379+
format!("trait `dyn UnrelatedTrait` has not been registered with #[godot_dyn]: Variant{node:?}");
380380

381381
assert_eq!(err.to_string(), expected_err);
382382

383383
let back = variant.try_to::<DynGd<foreign::NodeHealth, dyn InstanceIdProvider<Id = i32>>>();
384384

385+
// Variant Debug uses "VariantGd" prefix.
385386
let err = back.expect_err("DynGd::try_to() should have failed");
386-
let expected_err = format!("trait `dyn InstanceIdProvider<Id = i32>` has not been registered with #[godot_dyn]: {node:?}");
387+
let expected_err = format!("trait `dyn InstanceIdProvider<Id = i32>` has not been registered with #[godot_dyn]: Variant{node:?}");
387388

388389
assert_eq!(err.to_string(), expected_err);
389390

@@ -401,10 +402,9 @@ fn dyn_gd_error_unimplemented_trait() {
401402

402403
let refc_id = obj.instance_id().to_i64();
403404
let expected_debug = format!(
404-
"none of the classes derived from `RefCounted` have been linked to trait `dyn Health` with #[godot_dyn]: \
405-
Gd {{ id: {refc_id}, class: RefCounted, refc: 3 }}")
406-
;
407-
// was: {obj:?}
405+
"none of the classes derived from `RefCounted` have been linked to trait `dyn Health` with #[godot_dyn]: \
406+
VariantGd {{ id: {refc_id}, class: RefCounted, refc: 3 }}"
407+
);
408408
assert_eq!(err.to_string(), expected_debug);
409409

410410
let node = foreign::NodeHealth::new_alloc();
@@ -415,9 +415,9 @@ fn dyn_gd_error_unimplemented_trait() {
415415

416416
// NodeHealth is manually managed (inherits Node), so no refcount in debug output.
417417
let node_id = node.instance_id().to_i64();
418-
let expected_debug = format!(
419-
"none of the classes derived from `NodeHealth` have been linked to trait `dyn InstanceIdProvider<Id = f32>` with #[godot_dyn]: \
420-
Gd {{ id: {node_id}, class: NodeHealth }}"
418+
let expected_debug = format!(
419+
"none of the classes derived from `NodeHealth` have been linked to trait `dyn InstanceIdProvider<Id = f32>` with #[godot_dyn]: \
420+
VariantGd {{ id: {node_id}, class: NodeHealth }}"
421421
);
422422
assert_eq!(err.to_string(), expected_debug);
423423

itest/rust/src/object_tests/object_test.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -496,11 +496,10 @@ fn object_engine_convert_variant_nil() {
496496
});
497497
}
498498

499-
#[itest(focus)]
499+
#[itest]
500500
fn object_engine_convert_variant_error() {
501501
let refc = RefCounted::new_gd();
502502
let variant = refc.to_variant();
503-
504503
assert_eq!(refc.test_refcount(), Some(2));
505504

506505
let err = Gd::<Node2D>::try_from_variant(&variant)
@@ -510,11 +509,10 @@ fn object_engine_convert_variant_error() {
510509
assert_eq!(refc.test_refcount(), Some(3));
511510

512511
let expected_debug = format!(
513-
"cannot convert to class Node2D: Gd {{ id: {}, class: RefCounted, refc: 3 }}",
512+
"cannot convert to class Node2D: VariantGd {{ id: {}, class: RefCounted, refc: 3 }}",
514513
refc.instance_id().to_i64()
515514
);
516515
assert_eq!(err.to_string(), expected_debug);
517-
// was: format!("cannot convert to class Node2D: {refc:?}")
518516
}
519517

520518
#[itest]

0 commit comments

Comments
 (0)