Skip to content

Commit 4902b34

Browse files
authored
Merge pull request #1227 from godot-rust/qol/gd-debug
Improve `Debug` impl for objects
2 parents 8268cff + 9d02970 commit 4902b34

File tree

13 files changed

+290
-91
lines changed

13 files changed

+290
-91
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: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77

88
//! Runtime checks and inspection of Godot classes.
99
10-
use crate::builtin::{GString, StringName};
10+
use crate::builtin::{GString, StringName, Variant, VariantType};
1111
#[cfg(debug_assertions)]
1212
use crate::classes::{ClassDb, Object};
1313
use crate::meta::CallContext;
1414
#[cfg(debug_assertions)]
1515
use crate::meta::ClassName;
16-
use crate::obj::{bounds, Bounds, Gd, GodotClass, InstanceId};
16+
use crate::obj::{bounds, Bounds, Gd, GodotClass, InstanceId, RawGd};
1717
use crate::sys;
1818

1919
pub(crate) fn debug_string<T: GodotClass>(
@@ -23,12 +23,85 @@ pub(crate) fn debug_string<T: GodotClass>(
2323
) -> std::fmt::Result {
2424
if let Some(id) = obj.instance_id_or_none() {
2525
let class: StringName = obj.dynamic_class_string();
26-
write!(f, "{ty} {{ id: {id}, class: {class} }}")
26+
debug_string_parts(f, ty, id, class, obj.maybe_refcount(), None)
2727
} else {
2828
write!(f, "{ty} {{ freed obj }}")
2929
}
3030
}
3131

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+
debug_assert_eq!(obj.get_type(), VariantType::OBJECT);
39+
40+
let id = obj
41+
.object_id_unchecked()
42+
.expect("Variant must be of type OBJECT");
43+
44+
if id.lookup_validity() {
45+
// Object::get_class() currently returns String, but this is future-proof if the return type changes to StringName.
46+
let class = obj
47+
.call("get_class", &[])
48+
.try_to_relaxed::<StringName>()
49+
.expect("get_class() must be compatible with StringName");
50+
51+
let refcount = id.is_ref_counted().then(|| {
52+
obj.call("get_reference_count", &[])
53+
.try_to_relaxed::<i32>()
54+
.expect("get_reference_count() must return integer") as usize
55+
});
56+
57+
debug_string_parts(f, ty, id, class, refcount, None)
58+
} else {
59+
write!(f, "{ty} {{ freed obj }}")
60+
}
61+
}
62+
63+
// Polyfill for Godot < 4.4, where Variant::object_id_unchecked() is not available.
64+
#[cfg(before_api = "4.4")]
65+
pub(crate) fn debug_string_variant(
66+
obj: &Variant,
67+
f: &mut std::fmt::Formatter<'_>,
68+
ty: &str,
69+
) -> std::fmt::Result {
70+
debug_assert_eq!(obj.get_type(), VariantType::OBJECT);
71+
72+
match obj.try_to::<Gd<crate::classes::Object>>() {
73+
Ok(obj) => {
74+
let id = obj.instance_id(); // Guaranteed valid, since conversion would have failed otherwise.
75+
let class = obj.dynamic_class_string();
76+
77+
// Refcount is off-by-one due to now-created Gd<T> from conversion; correct by -1.
78+
let refcount = obj.maybe_refcount().map(|rc| rc.saturating_sub(1));
79+
80+
debug_string_parts(f, ty, id, class, refcount, None)
81+
}
82+
Err(_) => {
83+
write!(f, "{ty} {{ freed obj }}")
84+
}
85+
}
86+
}
87+
88+
pub(crate) fn debug_string_nullable<T: GodotClass>(
89+
obj: &RawGd<T>,
90+
f: &mut std::fmt::Formatter<'_>,
91+
ty: &str,
92+
) -> std::fmt::Result {
93+
if obj.is_null() {
94+
write!(f, "{ty} {{ null }}")
95+
} else {
96+
// Unsafety introduced here to avoid creating a new Gd<T> (which can have all sorts of side effects, logs, refcounts etc.)
97+
// *and* pushing down all high-level Gd<T> functions to RawGd<T> as pure delegates.
98+
99+
// SAFETY: checked non-null.
100+
let obj = unsafe { obj.as_non_null() };
101+
debug_string(obj, f, ty)
102+
}
103+
}
104+
32105
pub(crate) fn debug_string_with_trait<T: GodotClass>(
33106
obj: &Gd<T>,
34107
f: &mut std::fmt::Formatter<'_>,
@@ -37,12 +110,36 @@ pub(crate) fn debug_string_with_trait<T: GodotClass>(
37110
) -> std::fmt::Result {
38111
if let Some(id) = obj.instance_id_or_none() {
39112
let class: StringName = obj.dynamic_class_string();
40-
write!(f, "{ty} {{ id: {id}, class: {class}, trait: {trt} }}")
113+
debug_string_parts(f, ty, id, class, obj.maybe_refcount(), Some(trt))
41114
} else {
42115
write!(f, "{ty} {{ freed obj }}")
43116
}
44117
}
45118

119+
fn debug_string_parts(
120+
f: &mut std::fmt::Formatter<'_>,
121+
ty: &str,
122+
id: InstanceId,
123+
class: StringName,
124+
refcount: Option<usize>,
125+
trait_name: Option<&str>,
126+
) -> std::fmt::Result {
127+
let mut builder = f.debug_struct(ty);
128+
builder
129+
.field("id", &id.to_i64())
130+
.field("class", &format_args!("{class}"));
131+
132+
if let Some(trait_name) = trait_name {
133+
builder.field("trait", &format_args!("{trait_name}"));
134+
}
135+
136+
if let Some(refcount) = refcount {
137+
builder.field("refc", &refcount);
138+
}
139+
140+
builder.finish()
141+
}
142+
46143
pub(crate) fn display_string<T: GodotClass>(
47144
obj: &Gd<T>,
48145
f: &mut std::fmt::Formatter<'_>,
@@ -83,7 +180,7 @@ pub(crate) fn ensure_object_alive(
83180
// namely in PR https://github.com/godotengine/godot/pull/36189. Double-check to make sure.
84181
assert_eq!(
85182
new_object_ptr, old_object_ptr,
86-
"{call_ctx}: instance ID {instance_id} points to a stale, reused object. Please report this to gdext maintainers."
183+
"{call_ctx}: instance ID {instance_id} points to a stale, reused object. Please report this to godot-rust maintainers."
87184
);
88185
}
89186

godot-core/src/obj/base.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ impl<T: GodotClass> Base<T> {
102102
pub fn obj_sys(&self) -> sys::GDExtensionObjectPtr {
103103
self.obj.obj_sys()
104104
}
105+
106+
// Internal use only, do not make public.
107+
#[cfg(feature = "debug-log")]
108+
pub(crate) fn debug_instance_id(&self) -> crate::obj::InstanceId {
109+
self.obj.instance_id()
110+
}
105111
}
106112

107113
impl<T: GodotClass> Debug for Base<T> {

godot-core/src/obj/bounds.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ impl Sealed for MemRefCounted {}
208208
impl Memory for MemRefCounted {}
209209
impl DynMemory for MemRefCounted {
210210
fn maybe_init_ref<T: GodotClass>(obj: &mut RawGd<T>) {
211-
out!(" MemRefc::init <{}>", std::any::type_name::<T>());
211+
out!(" MemRefc::init: {obj:?}");
212212
if obj.is_null() {
213213
return;
214214
}
@@ -226,7 +226,7 @@ impl DynMemory for MemRefCounted {
226226
}
227227

228228
fn maybe_inc_ref<T: GodotClass>(obj: &mut RawGd<T>) {
229-
out!(" MemRefc::inc <{}>", std::any::type_name::<T>());
229+
out!(" MemRefc::inc: {obj:?}");
230230
if obj.is_null() {
231231
return;
232232
}
@@ -237,7 +237,7 @@ impl DynMemory for MemRefCounted {
237237
}
238238

239239
unsafe fn maybe_dec_ref<T: GodotClass>(obj: &mut RawGd<T>) -> bool {
240-
out!(" MemRefc::dec <{}>", std::any::type_name::<T>());
240+
out!(" MemRefc::dec: {obj:?}");
241241
if obj.is_null() {
242242
return false;
243243
}
@@ -278,7 +278,7 @@ impl MemDynamic {
278278
impl Sealed for MemDynamic {}
279279
impl DynMemory for MemDynamic {
280280
fn maybe_init_ref<T: GodotClass>(obj: &mut RawGd<T>) {
281-
out!(" MemDyn::init <{}>", std::any::type_name::<T>());
281+
out!(" MemDyn::init: {obj:?}");
282282
if Self::inherits_refcounted(obj) {
283283
// Will call `RefCounted::init_ref()` which checks for liveness.
284284
out!(" MemDyn -> MemRefc");
@@ -289,15 +289,15 @@ impl DynMemory for MemDynamic {
289289
}
290290

291291
fn maybe_inc_ref<T: GodotClass>(obj: &mut RawGd<T>) {
292-
out!(" MemDyn::inc <{}>", std::any::type_name::<T>());
292+
out!(" MemDyn::inc: {obj:?}");
293293
if Self::inherits_refcounted(obj) {
294294
// Will call `RefCounted::reference()` which checks for liveness.
295295
MemRefCounted::maybe_inc_ref(obj)
296296
}
297297
}
298298

299299
unsafe fn maybe_dec_ref<T: GodotClass>(obj: &mut RawGd<T>) -> bool {
300-
out!(" MemDyn::dec <{}>", std::any::type_name::<T>());
300+
out!(" MemDyn::dec: {obj:?}");
301301
if obj
302302
.instance_id_unchecked()
303303
.is_some_and(|id| id.is_ref_counted())

godot-core/src/obj/gd.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,21 @@ impl<T: GodotClass> Gd<T> {
302302
}
303303
}
304304

305+
/// Returns the reference count, if the dynamic object inherits `RefCounted`; and `None` otherwise.
306+
pub(crate) fn maybe_refcount(&self) -> Option<usize> {
307+
// Fast check if ref-counted without downcast.
308+
self.instance_id().is_ref_counted().then(|| {
309+
let rc = self.raw.with_ref_counted(|refc| refc.get_reference_count());
310+
rc as usize
311+
})
312+
}
313+
314+
#[cfg(feature = "trace")] // itest only.
315+
#[doc(hidden)]
316+
pub fn test_refcount(&self) -> Option<usize> {
317+
self.maybe_refcount()
318+
}
319+
305320
/// **Upcast:** convert into a smart pointer to a base class. Always succeeds.
306321
///
307322
/// Moves out of this value. If you want to create _another_ smart pointer instance,

godot-core/src/obj/raw_gd.rs

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::meta::{
1818
use crate::obj::bounds::{Declarer, DynMemory as _};
1919
use crate::obj::casts::CastSuccess;
2020
use crate::obj::rtti::ObjectRtti;
21-
use crate::obj::{bounds, Bounds, GdDerefTarget, GdMut, GdRef, GodotClass, InstanceId};
21+
use crate::obj::{bounds, Bounds, Gd, GdDerefTarget, GdMut, GdRef, GodotClass, InstanceId};
2222
use crate::storage::{InstanceCache, InstanceStorage, Storage};
2323
use crate::{classes, out};
2424

@@ -95,8 +95,7 @@ impl<T: GodotClass> RawGd<T> {
9595

9696
/// Returns `true` if the object is null.
9797
///
98-
/// This does not check if the object is dead, for that use
99-
/// [`instance_id_or_none()`](Self::instance_id_or_none).
98+
/// This does not check if the object is dead. For that, use [`is_instance_valid()`](Self::is_instance_valid).
10099
pub(crate) fn is_null(&self) -> bool {
101100
self.obj.is_null() || self.cached_rtti.is_none()
102101
}
@@ -148,10 +147,15 @@ impl<T: GodotClass> RawGd<T> {
148147
///
149148
/// On success, you'll get a `CastSuccess<T, U>` instance, which holds a weak `RawGd<U>`. You can only extract that one by trading
150149
/// a strong `RawGd<T>` for it, to maintain the balance.
150+
///
151+
/// This function is unreliable when invoked _during_ destruction (e.g. C++ `~RefCounted()` destructor). This can occur when debug-logging
152+
/// instances during cleanups. `Object::object_cast_to()` is a virtual function, but virtual dispatch during destructor doesn't work in C++.
151153
pub(super) fn ffi_cast<U>(&self) -> Result<CastSuccess<T, U>, ()>
152154
where
153155
U: GodotClass,
154156
{
157+
//eprintln!("ffi_cast: {} (dyn {}) -> {}", T::class_name(), self.as_non_null().dynamic_class_string(), U::class_name());
158+
155159
// `self` may be null when we convert a null-variant into a `Option<Gd<T>>`, since we use `ffi_cast`
156160
// in the `ffi_from_variant` conversion function to ensure type-correctness. So the chain would be as follows:
157161
// - Variant::nil()
@@ -184,24 +188,57 @@ impl<T: GodotClass> RawGd<T> {
184188
Ok(CastSuccess::from_weak(weak))
185189
}
186190

191+
/// Executes a function, assuming that `self` inherits `RefCounted`.
192+
///
193+
/// This function is unreliable when invoked _during_ destruction (e.g. C++ `~RefCounted()` destructor). This can occur when debug-logging
194+
/// instances during cleanups. `Object::object_cast_to()` is a virtual function, but virtual dispatch during destructor doesn't work in C++.
195+
///
196+
/// # Panics
197+
/// If `self` does not inherit `RefCounted` or is null.
187198
pub(crate) fn with_ref_counted<R>(&self, apply: impl Fn(&mut classes::RefCounted) -> R) -> R {
188199
// Note: this previously called Declarer::scoped_mut() - however, no need to go through bind() for changes in base RefCounted.
189200
// Any accesses to user objects (e.g. destruction if refc=0) would bind anyway.
201+
//
202+
// Might change implementation as follows -- but last time caused UB; investigate.
203+
// pub(crate) unsafe fn as_ref_counted_unchecked(&mut self) -> &mut classes::RefCounted {
204+
// self.as_target_mut()
205+
// }
206+
207+
let mut ref_counted = match self.ffi_cast::<classes::RefCounted>() {
208+
Ok(cast_success) => cast_success,
209+
Err(()) if self.is_null() => {
210+
panic!("RawGd::with_ref_counted(): expected to inherit RefCounted, encountered null pointer");
211+
}
212+
Err(()) => {
213+
// SAFETY: this branch implies non-null.
214+
let gd_ref = unsafe { self.as_non_null() };
215+
let class = gd_ref.dynamic_class_string();
216+
217+
// One way how this may panic is when invoked during destruction of a RefCounted object. The C++ `Object::object_cast_to()`
218+
// function is virtual but cannot be dynamically dispatched in a C++ destructor.
219+
panic!("RawGd::with_ref_counted(): expected to inherit RefCounted, but encountered {class}");
220+
}
221+
};
190222

191-
let mut cast_obj = self
192-
.ffi_cast::<classes::RefCounted>()
193-
.expect("object expected to inherit RefCounted");
223+
let return_val = apply(ref_counted.as_dest_mut().as_target_mut());
194224

195-
// Using as_dest_mut() ensures that there is no refcount increment happening, i.e. any apply() function happens on *current* object.
196-
// Apart from performance considerations, this is relevant when examining RefCounted::get_reference_count() -- otherwise we have an
197-
// Observer effect, where reading the RefCounted object changes its reference count -- e.g. in Debug impl.
198-
apply(cast_obj.as_dest_mut().as_target_mut())
225+
// CastSuccess is forgotten when dropped, so no ownership transfer.
226+
return_val
199227
}
200228

201-
// TODO replace the above with this -- last time caused UB; investigate.
202-
// pub(crate) unsafe fn as_ref_counted_unchecked(&mut self) -> &mut classes::RefCounted {
203-
// self.as_target_mut()
204-
// }
229+
/// Enables outer `Gd` APIs or bypasses additional null checks, in cases where `RawGd` is guaranteed non-null.
230+
///
231+
/// # Safety
232+
/// `self` must not be null.
233+
pub(crate) unsafe fn as_non_null(&self) -> &Gd<T> {
234+
debug_assert!(
235+
!self.is_null(),
236+
"RawGd::as_non_null() called on null pointer; this is UB"
237+
);
238+
239+
// SAFETY: layout of Gd<T> is currently equivalent to RawGd<T>.
240+
unsafe { std::mem::transmute::<&RawGd<T>, &Gd<T>>(self) }
241+
}
205242

206243
pub(crate) fn as_object_ref(&self) -> &classes::Object {
207244
// SAFETY: Object is always a valid upcast target.
@@ -654,7 +691,7 @@ impl<T: GodotClass> Drop for RawGd<T> {
654691
fn drop(&mut self) {
655692
// No-op for manually managed objects
656693

657-
out!("RawGd::drop <{}>", std::any::type_name::<T>());
694+
out!("RawGd::drop: {self:?}");
658695

659696
// SAFETY: This `Gd` won't be dropped again after this.
660697
// If destruction is triggered by Godot, Storage already knows about it, no need to notify it
@@ -669,7 +706,7 @@ impl<T: GodotClass> Drop for RawGd<T> {
669706

670707
impl<T: GodotClass> Clone for RawGd<T> {
671708
fn clone(&self) -> Self {
672-
out!("RawGd::clone");
709+
out!("RawGd::clone: {self:?} (before clone)");
673710

674711
if self.is_null() {
675712
Self::null()
@@ -689,12 +726,7 @@ impl<T: GodotClass> Clone for RawGd<T> {
689726

690727
impl<T: GodotClass> fmt::Debug for RawGd<T> {
691728
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
692-
if self.is_null() {
693-
return write!(f, "{} {{ null obj }}", std::any::type_name::<T>());
694-
}
695-
696-
let gd = super::Gd::from_ffi(self.clone());
697-
write!(f, "{gd:?}")
729+
classes::debug_string_nullable(self, f, "RawGd")
698730
}
699731
}
700732

0 commit comments

Comments
 (0)