Skip to content

Commit 3548d0d

Browse files
authored
Merge pull request #1094 from godot-rust/qol/refactor-storage
`bind/bind_mut` borrow errors now print previous stacktrace
2 parents c655b5a + 46a740a commit 3548d0d

File tree

8 files changed

+291
-115
lines changed

8 files changed

+291
-115
lines changed

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,11 @@ impl CallError {
301301

302302
#[doc(hidden)]
303303
pub fn failed_by_user_panic(call_ctx: &CallContext, reason: String) -> Self {
304-
Self::new(call_ctx, reason, None)
304+
// This can cause the panic message to be printed twice in some scenarios (e.g. bind_mut() borrow failure).
305+
// But in other cases (e.g. itest `dynamic_call_with_panic`), it is only printed once.
306+
// Would need some work to have a consistent experience.
307+
308+
Self::new(call_ctx, format!("function panicked: {reason}"), None)
305309
}
306310

307311
fn new(

godot-core/src/private.rs

Lines changed: 50 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,
@@ -259,11 +296,14 @@ where
259296

260297
let message = format_panic_message(panic_info);
261298
if godot_print() {
299+
// Also prints to stdout/stderr -- do not print twice.
262300
godot_error!("{message}");
301+
} else {
302+
eprintln!("{message}");
263303
}
264-
eprintln!("{message}");
265-
#[cfg(debug_assertions)]
266-
eprintln!("{}", std::backtrace::Backtrace::capture());
304+
305+
let backtrace = format_backtrace!("panic backtrace");
306+
eprintln!("{backtrace}");
267307
let _ignored_result = std::io::stderr().flush();
268308
}));
269309
}
@@ -322,6 +362,7 @@ thread_local! {
322362
pub fn get_gdext_panic_context() -> Option<String> {
323363
#[cfg(debug_assertions)]
324364
return ERROR_CONTEXT_STACK.with(|cell| cell.borrow().get_last());
365+
325366
#[cfg(not(debug_assertions))]
326367
None
327368
}
@@ -337,13 +378,18 @@ where
337378
E: Fn() -> String,
338379
F: FnOnce() -> R + std::panic::UnwindSafe,
339380
{
381+
#[cfg(not(debug_assertions))]
382+
let _ = error_context; // Unused in Release.
383+
340384
#[cfg(debug_assertions)]
341385
ERROR_CONTEXT_STACK.with(|cell| unsafe {
342386
// SAFETY: &error_context is valid for lifetime of function, and is removed from LAST_ERROR_CONTEXT before end of function.
343387
cell.borrow_mut().push_function(&error_context)
344388
});
389+
345390
let result =
346391
std::panic::catch_unwind(code).map_err(|payload| extract_panic_message(payload.as_ref()));
392+
347393
#[cfg(debug_assertions)]
348394
ERROR_CONTEXT_STACK.with(|cell| cell.borrow_mut().pop_function());
349395
result

godot-core/src/registry/class.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,9 +216,13 @@ pub fn auto_register_classes(init_level: InitLevel) {
216216
// but it is much slower and doesn't guarantee that all the dependent classes will be already loaded in most cases.
217217
register_classes_and_dyn_traits(&mut map, init_level);
218218

219-
// actually register all the classes
219+
// Actually register all the classes.
220220
for info in map.into_values() {
221+
#[cfg(feature = "debug-log")]
222+
let class_name = info.class_name;
223+
221224
register_class_raw(info);
225+
222226
out!("Class {class_name} loaded.");
223227
}
224228

godot-core/src/storage/instance_storage.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* License, v. 2.0. If a copy of the MPL was not distributed with this
55
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
66
*/
7+
78
use godot_ffi as sys;
89
use std::cell::Cell;
910
use std::ptr;

godot-core/src/storage/mod.rs

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

14+
use godot_ffi::out;
1415
pub use instance_storage::*;
16+
use std::any::type_name;
17+
18+
// ----------------------------------------------------------------------------------------------------------------------------------------------
19+
// Shared code for submodules
20+
21+
fn bind_failed<T>(err: Box<dyn std::error::Error>, tracker: &DebugBorrowTracker) -> ! {
22+
let ty = type_name::<T>();
23+
24+
eprint!("{tracker}");
25+
26+
panic!(
27+
"Gd<T>::bind() failed, already bound; T = {ty}.\n \
28+
Make sure to use `self.base_mut()` or `self.base()` instead of `self.to_gd()` when possible.\n \
29+
Details: {err}."
30+
)
31+
}
32+
33+
fn bind_mut_failed<T>(err: Box<dyn std::error::Error>, tracker: &DebugBorrowTracker) -> ! {
34+
let ty = type_name::<T>();
35+
36+
eprint!("{tracker}");
37+
38+
panic!(
39+
"Gd<T>::bind_mut() failed, already bound; T = {ty}.\n \
40+
Make sure to use `self.base_mut()` instead of `self.to_gd()` when possible.\n \
41+
Details: {err}."
42+
)
43+
}
44+
45+
fn bug_inaccessible<T>(err: Box<dyn std::error::Error>) -> ! {
46+
// We should never hit this, except maybe in extreme cases like having more than `usize::MAX` borrows.
47+
let ty = type_name::<T>();
48+
49+
panic!(
50+
"`base_mut()` failed for type T = {ty}.\n \
51+
This is most likely a bug, please report it.\n \
52+
Details: {err}."
53+
)
54+
}
55+
56+
fn log_construct<T>() {
57+
out!(
58+
" Storage::construct <{ty}>",
59+
ty = type_name::<T>()
60+
);
61+
}
62+
63+
fn log_inc_ref<T: StorageRefCounted>(storage: &T) {
64+
out!(
65+
" Storage::on_inc_ref (rc={rc}) <{ty}> -- {base:?}",
66+
rc = T::godot_ref_count(storage),
67+
base = storage.base(),
68+
ty = type_name::<T>(),
69+
);
70+
}
71+
72+
fn log_dec_ref<T: StorageRefCounted>(storage: &T) {
73+
out!(
74+
" | Storage::on_dec_ref (rc={rc}) <{ty}> -- {base:?}",
75+
rc = T::godot_ref_count(storage),
76+
base = storage.base(),
77+
ty = type_name::<T>(),
78+
);
79+
}
80+
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+
);
87+
}
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+
}

0 commit comments

Comments
 (0)