Skip to content

Commit 36d5b91

Browse files
authored
Merge pull request #1037 from KevinThierauf/master
Panic handling: thread safety; set hook once and not repeatedly
2 parents f25e70f + d547d9f commit 36d5b91

File tree

9 files changed

+188
-145
lines changed

9 files changed

+188
-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

+99-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,49 @@ 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+
pub fn format_panic_message(panic_info: &std::panic::PanicHookInfo) -> String {
229+
let mut msg = extract_panic_message(panic_info.payload());
230+
231+
if let Some(context) = get_gdext_panic_context() {
232+
msg = format!("{msg}\nContext: {context}");
233+
}
234+
235+
let prefix = if let Some(location) = panic_info.location() {
236+
format!("panic {}:{}", location.file(), location.line())
237+
} else {
238+
"panic".to_string()
239+
};
240+
242241
// If the message contains newlines, print all of the lines after a line break, and indent them.
243242
let lbegin = "\n ";
244243
let indented = msg.replace('\n', lbegin);
245244

246245
if indented.len() != msg.len() {
247-
format!("[panic]{lbegin}{indented}")
246+
format!("[{prefix}]{lbegin}{indented}")
248247
} else {
249-
format!("[panic] {msg}")
248+
format!("[{prefix}] {msg}")
250249
}
251250
}
252251

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

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

278352
// TODO(bromeon): make call_ctx lazy-evaluated (like error_ctx) everywhere;
@@ -285,7 +359,7 @@ pub fn handle_varcall_panic<F, R>(
285359
F: FnOnce() -> Result<R, CallError> + std::panic::UnwindSafe,
286360
{
287361
let outcome: Result<Result<R, CallError>, String> =
288-
handle_panic_with_print(|| call_ctx, code, false);
362+
handle_panic(|| format!("{call_ctx}"), code);
289363

290364
let call_error = match outcome {
291365
// All good.
@@ -314,7 +388,7 @@ pub fn handle_ptrcall_panic<F, R>(call_ctx: &CallContext, code: F)
314388
where
315389
F: FnOnce() -> R + std::panic::UnwindSafe,
316390
{
317-
let outcome: Result<R, String> = handle_panic_with_print(|| call_ctx, code, false);
391+
let outcome: Result<R, String> = handle_panic(|| format!("{call_ctx}"), code);
318392

319393
let call_error = match outcome {
320394
// All good.
@@ -343,91 +417,6 @@ fn report_call_error(call_error: CallError, track_globally: bool) -> i32 {
343417
}
344418
}
345419

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-
431420
// ----------------------------------------------------------------------------------------------------------------------------------------------
432421

433422
#[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)