Skip to content

Commit e5b0772

Browse files
BromeonKevinThierauf
authored andcommitted
Panic handling: thread safety; set hook once and not repeatedly
2 parents 9530704 + c59ece1 commit e5b0772

File tree

10 files changed

+187
-144
lines changed

10 files changed

+187
-144
lines changed

godot-codegen/src/models/domain_mapping.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,8 @@ impl UtilityFunction {
559559
impl Enum {
560560
pub fn from_json(json_enum: &JsonEnum, surrounding_class: Option<&TyName>) -> Self {
561561
let godot_name = &json_enum.name;
562-
let is_bitfield = json_enum.is_bitfield;
562+
let is_bitfield = special_cases::is_enum_bitfield(surrounding_class, godot_name)
563+
.unwrap_or(json_enum.is_bitfield);
563564
let is_private = special_cases::is_enum_private(surrounding_class, godot_name);
564565
let is_exhaustive = special_cases::is_enum_exhaustive(surrounding_class, godot_name);
565566

godot-codegen/src/special_cases/special_cases.rs

+15
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,21 @@ pub fn is_enum_exhaustive(class_name: Option<&TyName>, enum_name: &str) -> bool
855855
}
856856
}
857857

858+
/// Overrides Godot's enum/bitfield status.
859+
/// * `Some(true)` -> bitfield
860+
/// * `Some(false)` -> enum
861+
/// * `None` -> keep default
862+
#[rustfmt::skip]
863+
pub fn is_enum_bitfield(class_name: Option<&TyName>, enum_name: &str) -> Option<bool> {
864+
let class_name = class_name.map(|c| c.godot_ty.as_str());
865+
match (class_name, enum_name) {
866+
| (Some("Object"), "ConnectFlags")
867+
868+
=> Some(true),
869+
_ => None
870+
}
871+
}
872+
858873
/// Whether an enum can be combined with another enum (return value) for bitmasking purposes.
859874
///
860875
/// If multiple masks are ever necessary, this can be extended to return a slice instead of Option.

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
@@ -30,6 +30,7 @@ pub mod registry;
3030
pub mod tools;
3131

3232
mod storage;
33+
pub use crate::private::{get_gdext_panic_context, set_gdext_hook};
3334
pub use godot_ffi as sys;
3435

3536
// ----------------------------------------------------------------------------------------------------------------------------------------------

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
// ----------------------------------------------------------------------------------------------------------------------------------------------
@@ -180,11 +180,6 @@ pub unsafe fn has_virtual_script_method(
180180
sys::interface_fn!(object_has_script_method)(sys::to_const_ptr(object_ptr), method_sname) != 0
181181
}
182182

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

@@ -221,15 +216,7 @@ pub fn is_class_runtime(is_tool: bool) -> bool {
221216
// ----------------------------------------------------------------------------------------------------------------------------------------------
222217
// Panic handling
223218

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

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

247247
if indented.len() != msg.len() {
248-
format!("[panic]{lbegin}{indented}")
248+
format!("[{prefix}]{lbegin}{indented}")
249249
} else {
250-
format!("[panic] {msg}")
250+
format!("[{prefix}] {msg}")
251251
}
252252
}
253253

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

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

279354
// TODO(bromeon): make call_ctx lazy-evaluated (like error_ctx) everywhere;
@@ -286,7 +361,7 @@ pub fn handle_varcall_panic<F, R>(
286361
F: FnOnce() -> Result<R, CallError> + std::panic::UnwindSafe,
287362
{
288363
let outcome: Result<Result<R, CallError>, String> =
289-
handle_panic_with_print(|| call_ctx, code, false);
364+
handle_panic(|| format!("{call_ctx}"), code);
290365

291366
let call_error = match outcome {
292367
// All good.
@@ -315,7 +390,7 @@ pub fn handle_ptrcall_panic<F, R>(call_ctx: &CallContext, code: F)
315390
where
316391
F: FnOnce() -> R + std::panic::UnwindSafe,
317392
{
318-
let outcome: Result<R, String> = handle_panic_with_print(|| call_ctx, code, false);
393+
let outcome: Result<R, String> = handle_panic(|| format!("{call_ctx}"), code);
319394

320395
let call_error = match outcome {
321396
// All good.
@@ -344,91 +419,6 @@ fn report_call_error(call_error: CallError, track_globally: bool) -> i32 {
344419
}
345420
}
346421

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

434424
#[cfg(test)]

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ fn make_ptrcall_fn(call_ctx: &TokenStream, wrapped_method: &TokenStream) -> Toke
473473
) {
474474
let call_ctx = #call_ctx;
475475
let _success = ::godot::private::handle_panic(
476-
|| &call_ctx,
476+
|| format!("{call_ctx}"),
477477
|| #invocation
478478
);
479479

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ impl CallableRefcountTest {
330330
#[cfg(since_api = "4.2")]
331331
pub mod custom_callable {
332332
use super::*;
333-
use crate::framework::{assert_eq_self, quick_thread, ThreadCrosser};
333+
use crate::framework::{assert_eq_self, quick_thread, suppress_panic_log, ThreadCrosser};
334334
use godot::builtin::{Dictionary, RustCallable};
335335
use godot::sys;
336336
use godot::sys::GdextBuild;
@@ -543,7 +543,9 @@ pub mod custom_callable {
543543
let received = Arc::new(AtomicU32::new(0));
544544
let received_callable = received.clone();
545545
let callable = Callable::from_local_fn("test", move |_args| {
546-
panic!("TEST: {}", received_callable.fetch_add(1, Ordering::SeqCst))
546+
suppress_panic_log(|| {
547+
panic!("TEST: {}", received_callable.fetch_add(1, Ordering::SeqCst))
548+
})
547549
});
548550

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

0 commit comments

Comments
 (0)