Skip to content

Commit 723f157

Browse files
KevinThieraufBromeon
authored andcommitted
Panic handling: thread safety; set hook once and not repeatedly
1 parent b568129 commit 723f157

File tree

9 files changed

+169
-145
lines changed

9 files changed

+169
-145
lines changed

godot-core/src/init/mod.rs

+16-2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ pub unsafe fn __gdext_load_library<E: ExtensionLibrary>(
4949
sys::initialize(get_proc_address, library, config);
5050
}
5151

52+
// With experimental-features enabled, we can always print panics to godot_print!
53+
#[cfg(feature = "experimental-threads")]
54+
crate::private::set_gdext_hook(|| true);
55+
56+
// Without experimental-features enabled, we can only print panics with godot_print! if the panic occurs on the main (Godot) thread.
57+
#[cfg(not(feature = "experimental-threads"))]
58+
{
59+
let main_thread = std::thread::current().id();
60+
crate::private::set_gdext_hook(move || std::thread::current().id() == main_thread);
61+
}
62+
5263
// Currently no way to express failure; could be exposed to E if necessary.
5364
// No early exit, unclear if Godot still requires output parameters to be set.
5465
let success = true;
@@ -68,8 +79,11 @@ pub unsafe fn __gdext_load_library<E: ExtensionLibrary>(
6879
success as u8
6980
};
7081

71-
let ctx = || "error when loading GDExtension library";
72-
let is_success = crate::private::handle_panic(ctx, init_code);
82+
// Use std::panic::catch_unwind instead of handle_panic: handle_panic uses TLS, which
83+
// calls `thread_atexit` on linux, which sets the hot reloading flag on linux.
84+
// Using std::panic::catch_unwind avoids this, although we lose out on context information
85+
// for debugging.
86+
let is_success = std::panic::catch_unwind(init_code);
7387

7488
is_success.unwrap_or(0)
7589
}

godot-core/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ pub mod task {}
3434
pub mod tools;
3535

3636
mod storage;
37+
pub use crate::private::{get_gdext_panic_context, set_gdext_hook};
3738
pub use godot_ffi as sys;
3839

3940
// ----------------------------------------------------------------------------------------------------------------------------------------------

godot-core/src/private.rs

+100-110
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ use crate::global::godot_error;
2222
use crate::meta::error::CallError;
2323
use crate::meta::CallContext;
2424
use crate::sys;
25+
use std::cell::RefCell;
26+
use std::io::Write;
2527
use std::sync::atomic;
26-
#[cfg(debug_assertions)]
27-
use std::sync::{Arc, Mutex};
2828
use sys::Global;
2929
// ----------------------------------------------------------------------------------------------------------------------------------------------
3030
// Global variables
@@ -179,11 +179,6 @@ pub unsafe fn has_virtual_script_method(
179179
sys::interface_fn!(object_has_script_method)(sys::to_const_ptr(object_ptr), method_sname) != 0
180180
}
181181

182-
pub fn flush_stdout() {
183-
use std::io::Write;
184-
std::io::stdout().flush().expect("flush stdout");
185-
}
186-
187182
/// Ensure `T` is an editor plugin.
188183
pub const fn is_editor_plugin<T: crate::obj::Inherits<crate::classes::EditorPlugin>>() {}
189184

@@ -220,15 +215,7 @@ pub fn is_class_runtime(is_tool: bool) -> bool {
220215
// ----------------------------------------------------------------------------------------------------------------------------------------------
221216
// Panic handling
222217

223-
#[cfg(debug_assertions)]
224-
#[derive(Debug)]
225-
struct GodotPanicInfo {
226-
line: u32,
227-
file: String,
228-
//backtrace: Backtrace, // for future use
229-
}
230-
231-
pub fn extract_panic_message(err: Box<dyn std::any::Any + Send>) -> String {
218+
pub fn extract_panic_message(err: &(dyn Send + std::any::Any)) -> String {
232219
if let Some(s) = err.downcast_ref::<&'static str>() {
233220
s.to_string()
234221
} else if let Some(s) = err.downcast_ref::<String>() {
@@ -238,18 +225,50 @@ pub fn extract_panic_message(err: Box<dyn std::any::Any + Send>) -> String {
238225
}
239226
}
240227

241-
fn format_panic_message(msg: String) -> String {
228+
#[doc(hidden)]
229+
pub fn format_panic_message(panic_info: &std::panic::PanicHookInfo) -> String {
230+
let mut msg = extract_panic_message(panic_info.payload());
231+
232+
if let Some(context) = get_gdext_panic_context() {
233+
msg = format!("{msg}\nContext: {context}");
234+
}
235+
236+
let prefix = if let Some(location) = panic_info.location() {
237+
format!("panic {}:{}", location.file(), location.line())
238+
} else {
239+
"panic".to_string()
240+
};
241+
242242
// If the message contains newlines, print all of the lines after a line break, and indent them.
243243
let lbegin = "\n ";
244244
let indented = msg.replace('\n', lbegin);
245245

246246
if indented.len() != msg.len() {
247-
format!("[panic]{lbegin}{indented}")
247+
format!("[{prefix}]{lbegin}{indented}")
248248
} else {
249-
format!("[panic] {msg}")
249+
format!("[{prefix}] {msg}")
250250
}
251251
}
252252

253+
pub fn set_gdext_hook<F>(godot_print: F)
254+
where
255+
F: Fn() -> bool + Send + Sync + 'static,
256+
{
257+
std::panic::set_hook(Box::new(move |panic_info| {
258+
// Flush, to make sure previous Rust output (e.g. test announcement, or debug prints during app) have been printed.
259+
let _ignored_result = std::io::stdout().flush();
260+
261+
let message = format_panic_message(panic_info);
262+
if godot_print() {
263+
godot_error!("{message}");
264+
}
265+
eprintln!("{message}");
266+
#[cfg(debug_assertions)]
267+
eprintln!("{}", std::backtrace::Backtrace::capture());
268+
let _ignored_result = std::io::stderr().flush();
269+
}));
270+
}
271+
253272
pub fn set_error_print_level(level: u8) -> u8 {
254273
assert!(level <= 2);
255274
ERROR_PRINT_LEVEL.swap(level, atomic::Ordering::Relaxed)
@@ -260,19 +279,75 @@ pub(crate) fn has_error_print_level(level: u8) -> bool {
260279
ERROR_PRINT_LEVEL.load(atomic::Ordering::Relaxed) >= level
261280
}
262281

282+
/// Internal type used to store context information for debug purposes. Debug context is stored on the thread-local
283+
/// ERROR_CONTEXT_STACK, which can later be used to retrieve the current context in the event of a panic. This value
284+
/// probably shouldn't be used directly; use ['get_gdext_panic_context()'](get_gdext_panic_context) instead.
285+
#[cfg(debug_assertions)]
286+
struct ScopedFunctionStack {
287+
functions: Vec<*const dyn Fn() -> String>,
288+
}
289+
290+
#[cfg(debug_assertions)]
291+
impl ScopedFunctionStack {
292+
/// # Safety
293+
/// Function must be removed (using [`pop_function()`](Self::pop_function)) before lifetime is invalidated.
294+
unsafe fn push_function(&mut self, function: &dyn Fn() -> String) {
295+
let function = std::ptr::from_ref(function);
296+
#[allow(clippy::unnecessary_cast)]
297+
let function = function as *const (dyn Fn() -> String + 'static);
298+
self.functions.push(function);
299+
}
300+
301+
fn pop_function(&mut self) {
302+
self.functions.pop().expect("function stack is empty!");
303+
}
304+
305+
fn get_last(&self) -> Option<String> {
306+
self.functions.last().cloned().map(|pointer| {
307+
// SAFETY:
308+
// Invariants provided by push_function assert that any and all functions held by ScopedFunctionStack
309+
// are removed before they are invalidated; functions must always be valid.
310+
unsafe { (*pointer)() }
311+
})
312+
}
313+
}
314+
315+
#[cfg(debug_assertions)]
316+
thread_local! {
317+
static ERROR_CONTEXT_STACK: RefCell<ScopedFunctionStack> = const {
318+
RefCell::new(ScopedFunctionStack { functions: Vec::new() })
319+
}
320+
}
321+
322+
// Value may return `None`, even from panic hook, if called from a non-Godot thread.
323+
pub fn get_gdext_panic_context() -> Option<String> {
324+
#[cfg(debug_assertions)]
325+
return ERROR_CONTEXT_STACK.with(|cell| cell.borrow().get_last());
326+
#[cfg(not(debug_assertions))]
327+
None
328+
}
329+
263330
/// Executes `code`. If a panic is thrown, it is caught and an error message is printed to Godot.
264331
///
265332
/// Returns `Err(message)` if a panic occurred, and `Ok(result)` with the result of `code` otherwise.
266333
///
267334
/// In contrast to [`handle_varcall_panic`] and [`handle_ptrcall_panic`], this function is not intended for use in `try_` functions,
268335
/// where the error is propagated as a `CallError` in a global variable.
269-
pub fn handle_panic<E, F, R, S>(error_context: E, code: F) -> Result<R, String>
336+
pub fn handle_panic<E, F, R>(error_context: E, code: F) -> Result<R, String>
270337
where
271-
E: FnOnce() -> S,
338+
E: Fn() -> String,
272339
F: FnOnce() -> R + std::panic::UnwindSafe,
273-
S: std::fmt::Display,
274340
{
275-
handle_panic_with_print(error_context, code, has_error_print_level(1))
341+
#[cfg(debug_assertions)]
342+
ERROR_CONTEXT_STACK.with(|cell| unsafe {
343+
// SAFETY: &error_context is valid for lifetime of function, and is removed from LAST_ERROR_CONTEXT before end of function.
344+
cell.borrow_mut().push_function(&error_context)
345+
});
346+
let result =
347+
std::panic::catch_unwind(code).map_err(|payload| extract_panic_message(payload.as_ref()));
348+
#[cfg(debug_assertions)]
349+
ERROR_CONTEXT_STACK.with(|cell| cell.borrow_mut().pop_function());
350+
result
276351
}
277352

278353
// TODO(bromeon): make call_ctx lazy-evaluated (like error_ctx) everywhere;
@@ -285,7 +360,7 @@ pub fn handle_varcall_panic<F, R>(
285360
F: FnOnce() -> Result<R, CallError> + std::panic::UnwindSafe,
286361
{
287362
let outcome: Result<Result<R, CallError>, String> =
288-
handle_panic_with_print(|| call_ctx, code, false);
363+
handle_panic(|| format!("{call_ctx}"), code);
289364

290365
let call_error = match outcome {
291366
// All good.
@@ -314,7 +389,7 @@ pub fn handle_ptrcall_panic<F, R>(call_ctx: &CallContext, code: F)
314389
where
315390
F: FnOnce() -> R + std::panic::UnwindSafe,
316391
{
317-
let outcome: Result<R, String> = handle_panic_with_print(|| call_ctx, code, false);
392+
let outcome: Result<R, String> = handle_panic(|| format!("{call_ctx}"), code);
318393

319394
let call_error = match outcome {
320395
// All good.
@@ -343,91 +418,6 @@ fn report_call_error(call_error: CallError, track_globally: bool) -> i32 {
343418
}
344419
}
345420

346-
fn handle_panic_with_print<E, F, R, S>(error_context: E, code: F, print: bool) -> Result<R, String>
347-
where
348-
E: FnOnce() -> S,
349-
F: FnOnce() -> R + std::panic::UnwindSafe,
350-
S: std::fmt::Display,
351-
{
352-
#[cfg(debug_assertions)]
353-
let info: Arc<Mutex<Option<GodotPanicInfo>>> = Arc::new(Mutex::new(None));
354-
355-
// Back up previous hook, set new one.
356-
#[cfg(debug_assertions)]
357-
let prev_hook = {
358-
let info = info.clone();
359-
let prev_hook = std::panic::take_hook();
360-
361-
std::panic::set_hook(Box::new(move |panic_info| {
362-
if let Some(location) = panic_info.location() {
363-
*info.lock().unwrap() = Some(GodotPanicInfo {
364-
file: location.file().to_string(),
365-
line: location.line(),
366-
//backtrace: Backtrace::capture(),
367-
});
368-
} else {
369-
eprintln!("panic occurred, but can't get location information");
370-
}
371-
}));
372-
373-
prev_hook
374-
};
375-
376-
// Run code that should panic, restore hook.
377-
let panic = std::panic::catch_unwind(code);
378-
379-
// Restore the previous panic hook if in Debug mode.
380-
#[cfg(debug_assertions)]
381-
std::panic::set_hook(prev_hook);
382-
383-
match panic {
384-
Ok(result) => Ok(result),
385-
Err(err) => {
386-
// Flush, to make sure previous Rust output (e.g. test announcement, or debug prints during app) have been printed
387-
// TODO write custom panic handler and move this there, before panic backtrace printing.
388-
flush_stdout();
389-
390-
// Handle panic info only in Debug mode.
391-
#[cfg(debug_assertions)]
392-
{
393-
let msg = extract_panic_message(err);
394-
let mut msg = format_panic_message(msg);
395-
396-
// Try to add location information.
397-
if let Ok(guard) = info.lock() {
398-
if let Some(info) = guard.as_ref() {
399-
msg = format!("{}\n at {}:{}", msg, info.file, info.line);
400-
}
401-
}
402-
403-
if print {
404-
godot_error!(
405-
"Rust function panicked: {}\n Context: {}",
406-
msg,
407-
error_context()
408-
);
409-
//eprintln!("Backtrace:\n{}", info.backtrace);
410-
}
411-
412-
Err(msg)
413-
}
414-
415-
#[cfg(not(debug_assertions))]
416-
{
417-
let _ = error_context; // Unused warning.
418-
let msg = extract_panic_message(err);
419-
let msg = format_panic_message(msg);
420-
421-
if print {
422-
godot_error!("{msg}");
423-
}
424-
425-
Err(msg)
426-
}
427-
}
428-
}
429-
}
430-
431421
// ----------------------------------------------------------------------------------------------------------------------------------------------
432422

433423
#[cfg(test)]

godot-core/src/task/async_runtime.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ fn poll_future(godot_waker: Arc<GodotWaker>) {
403403
return;
404404
};
405405

406-
let error_context = || "Godot async task failed";
406+
let error_context = || "Godot async task failed".to_string();
407407

408408
// If Future::poll() panics, the future is immediately dropped and cannot be accessed again,
409409
// thus any state that may not have been unwind-safe cannot be observed later.

godot-macros/src/class/data_models/func.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,7 @@ fn make_ptrcall_fn(call_ctx: &TokenStream, wrapped_method: &TokenStream) -> Toke
485485
) {
486486
let call_ctx = #call_ctx;
487487
let _success = ::godot::private::handle_panic(
488-
|| &call_ctx,
488+
|| format!("{call_ctx}"),
489489
|| #invocation
490490
);
491491

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ impl CallableRefcountTest {
382382
#[cfg(since_api = "4.2")]
383383
pub mod custom_callable {
384384
use super::*;
385-
use crate::framework::{assert_eq_self, quick_thread, ThreadCrosser};
385+
use crate::framework::{assert_eq_self, quick_thread, suppress_panic_log, ThreadCrosser};
386386
use godot::builtin::{Dictionary, RustCallable};
387387
use godot::prelude::Signal;
388388
use godot::sys;
@@ -596,7 +596,9 @@ pub mod custom_callable {
596596
let received = Arc::new(AtomicU32::new(0));
597597
let received_callable = received.clone();
598598
let callable = Callable::from_local_fn("test", move |_args| {
599-
panic!("TEST: {}", received_callable.fetch_add(1, Ordering::SeqCst))
599+
suppress_panic_log(|| {
600+
panic!("TEST: {}", received_callable.fetch_add(1, Ordering::SeqCst))
601+
})
600602
});
601603

602604
assert_eq!(Variant::nil(), callable.callv(&varray![]));

0 commit comments

Comments
 (0)