Skip to content

Commit db78f6d

Browse files
committed
Fix access to transient object data in Debug log
While C++ destructors are run, virtual functions on C++ Object will no longer dispatch correctly. These are however relied upon for functionality like casting, dynamic class query, etc.
1 parent 2d84535 commit db78f6d

File tree

8 files changed

+181
-73
lines changed

8 files changed

+181
-73
lines changed

godot-core/src/classes/class_runtime.rs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,21 @@ 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+
2529
let class: StringName = obj.dynamic_class_string();
2630

2731
let mut builder = f.debug_struct(ty);
28-
builder.field("id", &id).field("class", &class);
29-
if let Some(refcount) = obj.maybe_refcount() {
32+
builder
33+
.field("id", &id.to_i64())
34+
.field("class", &format_args!("{class}"));
35+
36+
if let Some(refcount) = refcount {
3037
builder.field("refc", &refcount);
3138
}
39+
3240
builder.finish()
3341
} else {
3442
write!(f, "{ty} {{ freed obj }}")
@@ -46,8 +54,8 @@ pub(crate) fn debug_string_nullable<T: GodotClass>(
4654
// Unsafety introduced here to avoid creating a new Gd<T> (which can have all sorts of side effects, logs, refcounts etc.)
4755
// *and* pushing down all high-level Gd<T> functions to RawGd<T> as pure delegates.
4856

49-
// SAFETY: layout of Gd<T> is currently equivalent to RawGd<T>.
50-
let obj: &Gd<T> = unsafe { std::mem::transmute::<&RawGd<T>, &Gd<T>>(obj) };
57+
// SAFETY: checked non-null.
58+
let obj = unsafe { obj.as_non_null() };
5159
debug_string(obj, f, ty)
5260
}
5361
}
@@ -59,16 +67,21 @@ pub(crate) fn debug_string_with_trait<T: GodotClass>(
5967
trt: &str,
6068
) -> std::fmt::Result {
6169
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+
6273
let class: StringName = obj.dynamic_class_string();
6374

6475
let mut builder = f.debug_struct(ty);
6576
builder
66-
.field("id", &id)
67-
.field("class", &class)
68-
.field("trait", &trt);
69-
if let Some(refcount) = obj.maybe_refcount() {
77+
.field("id", &id.to_i64())
78+
.field("class", &format_args!("{class}"))
79+
.field("trait", &format_args!("{trt}"));
80+
81+
if let Some(refcount) = refcount {
7082
builder.field("refc", &refcount);
7183
}
84+
7285
builder.finish()
7386
} else {
7487
write!(f, "{ty} {{ freed obj }}")

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/gd.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,12 +305,18 @@ impl<T: GodotClass> Gd<T> {
305305
/// Returns the reference count, if the dynamic object inherits `RefCounted`; and `None` otherwise.
306306
pub(crate) fn maybe_refcount(&self) -> Option<usize> {
307307
// Fast check if ref-counted without downcast.
308-
self.instance_id_unchecked().is_ref_counted().then(|| {
308+
self.instance_id().is_ref_counted().then(|| {
309309
let rc = self.raw.with_ref_counted(|refc| refc.get_reference_count());
310310
rc as usize
311311
})
312312
}
313313

314+
#[cfg(feature = "trace")] // itest only.
315+
#[doc(hidden)]
316+
pub fn test_refcount(&self) -> Option<usize> {
317+
self.maybe_refcount()
318+
}
319+
314320
/// **Upcast:** convert into a smart pointer to a base class. Always succeeds.
315321
///
316322
/// Moves out of this value. If you want to create _another_ smart pointer instance,

godot-core/src/obj/raw_gd.rs

Lines changed: 50 additions & 12 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

@@ -147,10 +147,15 @@ impl<T: GodotClass> RawGd<T> {
147147
///
148148
/// On success, you'll get a `CastSuccess<T, U>` instance, which holds a weak `RawGd<U>`. You can only extract that one by trading
149149
/// 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++.
150153
pub(super) fn ffi_cast<U>(&self) -> Result<CastSuccess<T, U>, ()>
151154
where
152155
U: GodotClass,
153156
{
157+
//eprintln!("ffi_cast: {} (dyn {}) -> {}", T::class_name(), self.as_non_null().dynamic_class_string(), U::class_name());
158+
154159
// `self` may be null when we convert a null-variant into a `Option<Gd<T>>`, since we use `ffi_cast`
155160
// in the `ffi_from_variant` conversion function to ensure type-correctness. So the chain would be as follows:
156161
// - Variant::nil()
@@ -183,24 +188,57 @@ impl<T: GodotClass> RawGd<T> {
183188
Ok(CastSuccess::from_weak(weak))
184189
}
185190

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.
186198
pub(crate) fn with_ref_counted<R>(&self, apply: impl Fn(&mut classes::RefCounted) -> R) -> R {
187199
// Note: this previously called Declarer::scoped_mut() - however, no need to go through bind() for changes in base RefCounted.
188200
// 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+
};
189222

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

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

200-
// TODO replace the above with this -- last time caused UB; investigate.
201-
// pub(crate) unsafe fn as_ref_counted_unchecked(&mut self) -> &mut classes::RefCounted {
202-
// self.as_target_mut()
203-
// }
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+
}
204242

205243
pub(crate) fn as_object_ref(&self) -> &classes::Object {
206244
// SAFETY: Object is always a valid upcast target.

godot-core/src/storage/instance_storage.rs

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ use godot_cell::panicking::{InaccessibleGuard, MutGuard, RefGuard};
1515
#[cfg(feature = "experimental-threads")]
1616
use godot_cell::blocking::{InaccessibleGuard, MutGuard, RefGuard};
1717

18+
use crate::godot_error;
1819
use crate::obj::{Base, Gd, GodotClass, Inherits};
19-
use crate::{godot_error, out};
20+
use crate::storage::log_pre_drop;
2021

2122
#[derive(Copy, Clone, Debug)]
2223
pub enum Lifecycle {
@@ -134,20 +135,12 @@ pub unsafe trait Storage {
134135
}
135136

136137
fn mark_destroyed_by_godot(&self) {
137-
out!(
138-
" Storage::mark_destroyed_by_godot", // -- {:?}",
139-
//self.user_instance
140-
);
141138
self.set_lifecycle(Lifecycle::Destroying);
142-
out!(
143-
" mark; self={:?}, val={:?}, obj={:?}",
144-
self as *const _,
145-
self.get_lifecycle(),
146-
self.base(),
147-
);
139+
140+
log_pre_drop(self);
148141
}
149142

150-
#[inline(always)]
143+
/*#[inline(always)]
151144
fn destroyed_by_godot(&self) -> bool {
152145
out!(
153146
" is_d; self={:?}, val={:?}, obj={:?}",
@@ -156,11 +149,12 @@ pub unsafe trait Storage {
156149
self.base(),
157150
);
158151
matches!(self.get_lifecycle(), Lifecycle::Destroying)
159-
}
152+
}*/
160153
}
161154

162155
/// An internal trait for keeping track of reference counts for a storage.
163156
pub(crate) trait StorageRefCounted: Storage {
157+
#[allow(dead_code)] // used in `debug-log` feature. Don't micromanage.
164158
fn godot_ref_count(&self) -> u32;
165159

166160
fn on_inc_ref(&self);

godot-core/src/storage/mod.rs

Lines changed: 64 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ mod multi_threaded;
1111
#[cfg_attr(feature = "experimental-threads", allow(dead_code))]
1212
mod single_threaded;
1313

14-
use godot_ffi::out;
1514
pub use instance_storage::*;
1615
use std::any::type_name;
1716

@@ -53,37 +52,74 @@ fn bug_inaccessible<T>(err: Box<dyn std::error::Error>) -> ! {
5352
)
5453
}
5554

56-
fn log_construct<T: GodotClass>(base: &Base<T::Base>) {
57-
out!(
58-
" Storage::construct: {base:?} (T={ty})",
59-
ty = type_name::<T>()
60-
);
61-
}
55+
#[cfg(feature = "debug-log")]
56+
use log_active::*;
6257

63-
fn log_inc_ref<T: StorageRefCounted>(storage: &T) {
64-
out!(
65-
" Storage::on_inc_ref (rc={rc}): {base:?} (T={ty})",
66-
rc = T::godot_ref_count(storage),
67-
base = storage.base(),
68-
ty = type_name::<T>(),
69-
);
70-
}
58+
#[cfg(not(feature = "debug-log"))]
59+
use log_inactive::*;
60+
61+
#[cfg(feature = "debug-log")]
62+
mod log_active {
63+
use super::*;
64+
use godot_ffi::out;
65+
66+
pub fn log_construct<T: GodotClass>(base: &Base<T::Base>) {
67+
out!(
68+
" Storage::construct: {base:?} (T={ty})",
69+
ty = type_name::<T>()
70+
);
71+
}
72+
73+
pub fn log_inc_ref<T: StorageRefCounted>(storage: &T) {
74+
out!(
75+
" Storage::on_inc_ref (rc={rc}): {base:?} (T={ty})",
76+
rc = T::godot_ref_count(storage),
77+
base = storage.base(),
78+
ty = type_name::<T>(),
79+
);
80+
}
7181

72-
fn log_dec_ref<T: StorageRefCounted>(storage: &T) {
73-
out!(
74-
" | Storage::on_dec_ref (rc={rc}): {base:?} (T={ty})",
75-
rc = T::godot_ref_count(storage),
76-
base = storage.base(),
77-
ty = type_name::<T>(),
78-
);
82+
pub fn log_dec_ref<T: StorageRefCounted>(storage: &T) {
83+
out!(
84+
" | Storage::on_dec_ref (rc={rc}): {base:?} (T={ty})",
85+
rc = T::godot_ref_count(storage),
86+
base = storage.base(),
87+
ty = type_name::<T>(),
88+
);
89+
}
90+
91+
pub fn log_pre_drop<T: Storage + ?Sized>(storage: &T) {
92+
// Do not Debug-fmt `self.base()` object here, as the C++ destructor may already be running. Debug::fmt fetches dynamic object information
93+
// (class type, virtual object_cast_to(), ...), but virtual dispatch won't run in active C++ destructors, thus causing weird behavior.
94+
95+
out!(
96+
" Storage::mark_destroyed_by_godot: {base_id} (lcy={lifecycle:?})",
97+
base_id = storage.base().debug_instance_id(),
98+
lifecycle = storage.get_lifecycle(),
99+
);
100+
}
101+
102+
pub fn log_drop<T: StorageRefCounted>(storage: &T) {
103+
// Do not Debug-fmt `self.base()` object here, see above.
104+
105+
out!(
106+
" Storage::drop (rc={rc}): {base_id}",
107+
rc = storage.godot_ref_count(),
108+
base_id = storage.base().debug_instance_id(),
109+
);
110+
}
79111
}
80112

81-
fn log_drop<T: StorageRefCounted>(storage: &T) {
82-
out!(
83-
" Storage::drop (rc={rc}): {base:?}",
84-
rc = storage.godot_ref_count(),
85-
base = storage.base(),
86-
);
113+
// out! macro still mentions arguments in all cfgs, so they must exist (and may or may not be optimized away).
114+
#[cfg(not(feature = "debug-log"))]
115+
mod log_inactive {
116+
use super::*;
117+
118+
pub fn log_construct<T: GodotClass>(_base: &Base<T::Base>) {}
119+
pub fn log_inc_ref<T: StorageRefCounted>(_storage: &T) {}
120+
pub fn log_dec_ref<T: StorageRefCounted>(_storage: &T) {}
121+
pub fn log_pre_drop<T: Storage + ?Sized>(_storage: &T) {}
122+
pub fn log_drop<T: StorageRefCounted>(_storage: &T) {}
87123
}
88124

89125
// ----------------------------------------------------------------------------------------------------------------------------------------------

itest/rust/src/object_tests/dyn_gd_test.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -398,20 +398,28 @@ fn dyn_gd_error_unimplemented_trait() {
398398
let back = variant.try_to::<DynGd<RefCounted, dyn Health>>();
399399

400400
let err = back.expect_err("DynGd::try_to() should have failed");
401-
assert_eq!(
402-
err.to_string(),
403-
format!("none of the classes derived from `RefCounted` have been linked to trait `dyn Health` with #[godot_dyn]: {obj:?}")
404-
);
401+
402+
let refc_id = obj.instance_id().to_i64();
403+
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:?}
408+
assert_eq!(err.to_string(), expected_debug);
405409

406410
let node = foreign::NodeHealth::new_alloc();
407411
let variant = node.to_variant();
408412
let back = variant.try_to::<DynGd<foreign::NodeHealth, dyn InstanceIdProvider<Id = f32>>>();
409413

410414
let err = back.expect_err("DynGd::try_to() should have failed");
411-
assert_eq!(
412-
err.to_string(),
413-
format!("none of the classes derived from `NodeHealth` have been linked to trait `dyn InstanceIdProvider<Id = f32>` with #[godot_dyn]: {node:?}")
415+
416+
// NodeHealth is manually managed (inherits Node), so no refcount in debug output.
417+
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 }}"
414421
);
422+
assert_eq!(err.to_string(), expected_debug);
415423

416424
node.free();
417425
}

0 commit comments

Comments
 (0)