Skip to content

Commit a0dede3

Browse files
committed
Capture bind/bind_mut backtraces + print on borrow errors
1 parent 6d88430 commit a0dede3

File tree

4 files changed

+191
-19
lines changed

4 files changed

+191
-19
lines changed

godot-core/src/private.rs

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,17 @@ pub use sys::out;
1717

1818
#[cfg(feature = "trace")]
1919
pub use crate::meta::trace;
20+
#[cfg(debug_assertions)]
21+
use std::cell::RefCell;
2022

2123
use crate::global::godot_error;
2224
use crate::meta::error::CallError;
2325
use crate::meta::CallContext;
2426
use crate::sys;
25-
use std::cell::RefCell;
2627
use std::io::Write;
2728
use std::sync::atomic;
2829
use sys::Global;
30+
2931
// ----------------------------------------------------------------------------------------------------------------------------------------------
3032
// Global variables
3133

@@ -249,6 +251,41 @@ pub fn format_panic_message(panic_info: &std::panic::PanicHookInfo) -> String {
249251
}
250252
}
251253

254+
// Macro instead of function, to avoid 1 extra frame in backtrace.
255+
#[cfg(debug_assertions)]
256+
#[macro_export]
257+
macro_rules! format_backtrace {
258+
($prefix:expr, $backtrace:expr) => {{
259+
use std::backtrace::BacktraceStatus;
260+
261+
let backtrace = $backtrace;
262+
263+
match backtrace.status() {
264+
BacktraceStatus::Captured => format!("\n[{}]\n{}\n", $prefix, backtrace),
265+
BacktraceStatus::Disabled => {
266+
"(backtrace disabled, run application with `RUST_BACKTRACE=1` environment variable)"
267+
.to_string()
268+
}
269+
BacktraceStatus::Unsupported => {
270+
"(backtrace unsupported for current platform)".to_string()
271+
}
272+
_ => "(backtrace status unknown)".to_string(),
273+
}
274+
}};
275+
276+
($prefix:expr) => {
277+
$crate::format_backtrace!($prefix, std::backtrace::Backtrace::capture())
278+
};
279+
}
280+
281+
#[cfg(not(debug_assertions))]
282+
#[macro_export]
283+
macro_rules! format_backtrace {
284+
($prefix:expr $(, $backtrace:expr)? ) => {
285+
String::new()
286+
};
287+
}
288+
252289
pub fn set_gdext_hook<F>(godot_print: F)
253290
where
254291
F: Fn() -> bool + Send + Sync + 'static,
@@ -261,9 +298,9 @@ where
261298
if godot_print() {
262299
godot_error!("{message}");
263300
}
264-
eprintln!("{message}");
265-
#[cfg(debug_assertions)]
266-
eprintln!("{}", std::backtrace::Backtrace::capture());
301+
302+
let backtrace = format_backtrace!("panic backtrace");
303+
eprintln!("{message}{backtrace}");
267304
let _ignored_result = std::io::stderr().flush();
268305
}));
269306
}
@@ -322,6 +359,7 @@ thread_local! {
322359
pub fn get_gdext_panic_context() -> Option<String> {
323360
#[cfg(debug_assertions)]
324361
return ERROR_CONTEXT_STACK.with(|cell| cell.borrow().get_last());
362+
325363
#[cfg(not(debug_assertions))]
326364
None
327365
}
@@ -337,13 +375,18 @@ where
337375
E: Fn() -> String,
338376
F: FnOnce() -> R + std::panic::UnwindSafe,
339377
{
378+
#[cfg(not(debug_assertions))]
379+
let _ = error_context; // Unused in Release.
380+
340381
#[cfg(debug_assertions)]
341382
ERROR_CONTEXT_STACK.with(|cell| unsafe {
342383
// SAFETY: &error_context is valid for lifetime of function, and is removed from LAST_ERROR_CONTEXT before end of function.
343384
cell.borrow_mut().push_function(&error_context)
344385
});
386+
345387
let result =
346388
std::panic::catch_unwind(code).map_err(|payload| extract_panic_message(payload.as_ref()));
389+
347390
#[cfg(debug_assertions)]
348391
ERROR_CONTEXT_STACK.with(|cell| cell.borrow_mut().pop_function());
349392
result

godot-core/src/storage/mod.rs

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,23 @@ use std::any::type_name;
1818
// ----------------------------------------------------------------------------------------------------------------------------------------------
1919
// Shared code for submodules
2020

21-
fn bind_failed<T>(err: Box<dyn std::error::Error>) -> ! {
21+
fn bind_failed<T>(err: Box<dyn std::error::Error>, tracker: &DebugBorrowTracker) -> ! {
2222
let ty = type_name::<T>();
23+
24+
eprint!("{tracker}");
25+
2326
panic!(
2427
"Gd<T>::bind() failed, already bound; T = {ty}.\n \
2528
Make sure to use `self.base_mut()` or `self.base()` instead of `self.to_gd()` when possible.\n \
2629
Details: {err}."
2730
)
2831
}
2932

30-
fn bind_mut_failed<T>(err: Box<dyn std::error::Error>) -> ! {
33+
fn bind_mut_failed<T>(err: Box<dyn std::error::Error>, tracker: &DebugBorrowTracker) -> ! {
3134
let ty = type_name::<T>();
35+
36+
eprint!("{tracker}");
37+
3238
panic!(
3339
"Gd<T>::bind_mut() failed, already bound; T = {ty}.\n \
3440
Make sure to use `self.base_mut()` instead of `self.to_gd()` when possible.\n \
@@ -37,9 +43,9 @@ fn bind_mut_failed<T>(err: Box<dyn std::error::Error>) -> ! {
3743
}
3844

3945
fn bug_inaccessible<T>(err: Box<dyn std::error::Error>) -> ! {
40-
// We should never hit this, except maybe in extreme cases like having more than
41-
// `usize::MAX` borrows.
46+
// We should never hit this, except maybe in extreme cases like having more than `usize::MAX` borrows.
4247
let ty = type_name::<T>();
48+
4349
panic!(
4450
"`base_mut()` failed for type T = {ty}.\n \
4551
This is most likely a bug, please report it.\n \
@@ -79,3 +85,100 @@ fn log_drop<T: StorageRefCounted>(storage: &T) {
7985
base = storage.base(),
8086
);
8187
}
88+
89+
// ----------------------------------------------------------------------------------------------------------------------------------------------
90+
// Tracking borrows in Debug mode
91+
92+
#[cfg(debug_assertions)]
93+
use borrow_info::DebugBorrowTracker;
94+
95+
#[cfg(not(debug_assertions))]
96+
use borrow_info_noop::DebugBorrowTracker;
97+
98+
#[cfg(debug_assertions)]
99+
mod borrow_info {
100+
use std::backtrace::Backtrace;
101+
use std::fmt;
102+
use std::sync::Mutex;
103+
104+
struct TrackedBorrow {
105+
backtrace: Backtrace,
106+
is_mut: bool,
107+
}
108+
109+
/// Informational-only info about ongoing borrows.
110+
pub(super) struct DebugBorrowTracker {
111+
// Currently just tracks the last borrow. Could technically track 1 mut or N ref borrows, but would need destructor integration.
112+
// Also never clears it when a guard drops, assuming that once a borrow fails, there must be at least one previous borrow conflicting.
113+
// Is also not yet integrated with "inaccessible" borrows (reborrow through base_mut).
114+
last_borrow: Mutex<Option<TrackedBorrow>>,
115+
}
116+
117+
impl DebugBorrowTracker {
118+
pub fn new() -> Self {
119+
Self {
120+
last_borrow: Mutex::new(None),
121+
}
122+
}
123+
124+
// Currently considers RUST_BACKTRACE due to performance reasons; force_capture() can be quite slow.
125+
// User is expected to set the env var during debug sessions.
126+
127+
#[track_caller]
128+
pub fn track_ref_borrow(&self) {
129+
let mut guard = self.last_borrow.lock().unwrap();
130+
*guard = Some(TrackedBorrow {
131+
backtrace: Backtrace::capture(),
132+
is_mut: false,
133+
});
134+
}
135+
136+
#[track_caller]
137+
pub fn track_mut_borrow(&self) {
138+
let mut guard = self.last_borrow.lock().unwrap();
139+
*guard = Some(TrackedBorrow {
140+
backtrace: Backtrace::capture(),
141+
is_mut: true,
142+
});
143+
}
144+
}
145+
146+
impl fmt::Display for DebugBorrowTracker {
147+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148+
let guard = self.last_borrow.lock().unwrap();
149+
if let Some(borrow) = &*guard {
150+
let mutability = if borrow.is_mut { "bind_mut" } else { "bind" };
151+
152+
let prefix = format!("backtrace of previous `{mutability}` borrow");
153+
let backtrace = crate::format_backtrace!(prefix, &borrow.backtrace);
154+
155+
writeln!(f, "{backtrace}")
156+
} else {
157+
writeln!(f, "no previous borrows tracked.")
158+
}
159+
}
160+
}
161+
}
162+
163+
#[cfg(not(debug_assertions))]
164+
mod borrow_info_noop {
165+
use std::fmt;
166+
167+
pub(super) struct DebugBorrowTracker;
168+
169+
impl DebugBorrowTracker {
170+
pub fn new() -> Self {
171+
Self
172+
}
173+
174+
pub fn track_ref_borrow(&self) {}
175+
176+
pub fn track_mut_borrow(&self) {}
177+
}
178+
179+
impl fmt::Display for DebugBorrowTracker {
180+
fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result {
181+
Ok(())
182+
}
183+
}
184+
}

godot-core/src/storage/multi_threaded.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use godot_cell::panicking::{GdCell, InaccessibleGuard, MutGuard, RefGuard};
1414
use godot_cell::blocking::{GdCell, InaccessibleGuard, MutGuard, RefGuard};
1515

1616
use crate::obj::{Base, GodotClass};
17-
use crate::storage::{AtomicLifecycle, Lifecycle, Storage, StorageRefCounted};
17+
use crate::storage::{AtomicLifecycle, DebugBorrowTracker, Lifecycle, Storage, StorageRefCounted};
1818

1919
pub struct InstanceStorage<T: GodotClass> {
2020
user_instance: GdCell<T>,
@@ -23,6 +23,9 @@ pub struct InstanceStorage<T: GodotClass> {
2323
// Declared after `user_instance`, is dropped last
2424
pub(super) lifecycle: AtomicLifecycle,
2525
godot_ref_count: AtomicU32,
26+
27+
// No-op in Release mode.
28+
borrow_tracker: DebugBorrowTracker,
2629
}
2730

2831
// SAFETY:
@@ -47,6 +50,7 @@ unsafe impl<T: GodotClass> Storage for InstanceStorage<T> {
4750
base,
4851
lifecycle: AtomicLifecycle::new(Lifecycle::Alive),
4952
godot_ref_count: AtomicU32::new(1),
53+
borrow_tracker: DebugBorrowTracker::new(),
5054
}
5155
}
5256

@@ -58,16 +62,27 @@ unsafe impl<T: GodotClass> Storage for InstanceStorage<T> {
5862
&self.base
5963
}
6064

65+
// Multi-threaded binds are currently blocking. However, if they still report an error, we follow the single-threaded behavior
66+
// of capturing the backtrace. This may be changed as the threading model (#18) evolves.
67+
6168
fn get(&self) -> RefGuard<'_, T> {
62-
self.user_instance
69+
let guard = self
70+
.user_instance
6371
.borrow()
64-
.unwrap_or_else(|e| super::bind_failed::<T>(e))
72+
.unwrap_or_else(|e| super::bind_failed::<T>(e, &self.borrow_tracker));
73+
74+
self.borrow_tracker.track_ref_borrow();
75+
guard
6576
}
6677

6778
fn get_mut(&self) -> MutGuard<'_, T> {
68-
self.user_instance
79+
let guard = self
80+
.user_instance
6981
.borrow_mut()
70-
.unwrap_or_else(|e| super::bind_mut_failed::<T>(e))
82+
.unwrap_or_else(|e| super::bind_mut_failed::<T>(e, &self.borrow_tracker));
83+
84+
self.borrow_tracker.track_mut_borrow();
85+
guard
7186
}
7287

7388
fn get_inaccessible<'a: 'b, 'b>(

godot-core/src/storage/single_threaded.rs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ use godot_cell::panicking::{GdCell, InaccessibleGuard, MutGuard, RefGuard};
1414
use godot_cell::blocking::{GdCell, InaccessibleGuard, MutGuard, RefGuard};
1515

1616
use crate::obj::{Base, GodotClass};
17-
use crate::storage::borrow_info::DebugBorrowTracker;
18-
use crate::storage::{Lifecycle, Storage, StorageRefCounted};
17+
use crate::storage::{DebugBorrowTracker, Lifecycle, Storage, StorageRefCounted};
1918

2019
pub struct InstanceStorage<T: GodotClass> {
2120
user_instance: GdCell<T>,
@@ -24,6 +23,9 @@ pub struct InstanceStorage<T: GodotClass> {
2423
// Declared after `user_instance`, is dropped last
2524
pub(super) lifecycle: cell::Cell<Lifecycle>,
2625
godot_ref_count: cell::Cell<u32>,
26+
27+
// No-op in Release mode.
28+
borrow_tracker: DebugBorrowTracker,
2729
}
2830

2931
// SAFETY:
@@ -48,6 +50,7 @@ unsafe impl<T: GodotClass> Storage for InstanceStorage<T> {
4850
base,
4951
lifecycle: cell::Cell::new(Lifecycle::Alive),
5052
godot_ref_count: cell::Cell::new(1),
53+
borrow_tracker: DebugBorrowTracker::new(),
5154
}
5255
}
5356

@@ -60,15 +63,23 @@ unsafe impl<T: GodotClass> Storage for InstanceStorage<T> {
6063
}
6164

6265
fn get(&self) -> RefGuard<'_, T> {
63-
self.user_instance
66+
let guard = self
67+
.user_instance
6468
.borrow()
65-
.unwrap_or_else(|e| super::bind_failed::<T>(e))
69+
.unwrap_or_else(|e| super::bind_failed::<T>(e, &self.borrow_tracker));
70+
71+
self.borrow_tracker.track_ref_borrow();
72+
guard
6673
}
6774

6875
fn get_mut(&self) -> MutGuard<'_, T> {
69-
self.user_instance
76+
let guard = self
77+
.user_instance
7078
.borrow_mut()
71-
.unwrap_or_else(|e| super::bind_mut_failed::<T>(e))
79+
.unwrap_or_else(|e| super::bind_mut_failed::<T>(e, &self.borrow_tracker));
80+
81+
self.borrow_tracker.track_mut_borrow();
82+
guard
7283
}
7384

7485
fn get_inaccessible<'stor: 'inst, 'inst>(

0 commit comments

Comments
 (0)