@@ -22,9 +22,9 @@ use crate::global::godot_error;
22
22
use crate :: meta:: error:: CallError ;
23
23
use crate :: meta:: CallContext ;
24
24
use crate :: sys;
25
+ use std:: cell:: RefCell ;
26
+ use std:: io:: Write ;
25
27
use std:: sync:: atomic;
26
- #[ cfg( debug_assertions) ]
27
- use std:: sync:: { Arc , Mutex } ;
28
28
use sys:: Global ;
29
29
// ----------------------------------------------------------------------------------------------------------------------------------------------
30
30
// Global variables
@@ -179,11 +179,6 @@ pub unsafe fn has_virtual_script_method(
179
179
sys:: interface_fn!( object_has_script_method) ( sys:: to_const_ptr ( object_ptr) , method_sname) != 0
180
180
}
181
181
182
- pub fn flush_stdout ( ) {
183
- use std:: io:: Write ;
184
- std:: io:: stdout ( ) . flush ( ) . expect ( "flush stdout" ) ;
185
- }
186
-
187
182
/// Ensure `T` is an editor plugin.
188
183
pub const fn is_editor_plugin < T : crate :: obj:: Inherits < crate :: classes:: EditorPlugin > > ( ) { }
189
184
@@ -220,15 +215,7 @@ pub fn is_class_runtime(is_tool: bool) -> bool {
220
215
// ----------------------------------------------------------------------------------------------------------------------------------------------
221
216
// Panic handling
222
217
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 {
232
219
if let Some ( s) = err. downcast_ref :: < & ' static str > ( ) {
233
220
s. to_string ( )
234
221
} 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 {
238
225
}
239
226
}
240
227
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}\n Context: {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
+
242
241
// If the message contains newlines, print all of the lines after a line break, and indent them.
243
242
let lbegin = "\n " ;
244
243
let indented = msg. replace ( '\n' , lbegin) ;
245
244
246
245
if indented. len ( ) != msg. len ( ) {
247
- format ! ( "[panic ]{lbegin}{indented}" )
246
+ format ! ( "[{prefix} ]{lbegin}{indented}" )
248
247
} else {
249
- format ! ( "[panic ] {msg}" )
248
+ format ! ( "[{prefix} ] {msg}" )
250
249
}
251
250
}
252
251
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
+
253
271
pub fn set_error_print_level ( level : u8 ) -> u8 {
254
272
assert ! ( level <= 2 ) ;
255
273
ERROR_PRINT_LEVEL . swap ( level, atomic:: Ordering :: Relaxed )
@@ -260,19 +278,75 @@ pub(crate) fn has_error_print_level(level: u8) -> bool {
260
278
ERROR_PRINT_LEVEL . load ( atomic:: Ordering :: Relaxed ) >= level
261
279
}
262
280
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
+
263
329
/// Executes `code`. If a panic is thrown, it is caught and an error message is printed to Godot.
264
330
///
265
331
/// Returns `Err(message)` if a panic occurred, and `Ok(result)` with the result of `code` otherwise.
266
332
///
267
333
/// In contrast to [`handle_varcall_panic`] and [`handle_ptrcall_panic`], this function is not intended for use in `try_` functions,
268
334
/// 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 >
270
336
where
271
- E : FnOnce ( ) -> S ,
337
+ E : Fn ( ) -> String ,
272
338
F : FnOnce ( ) -> R + std:: panic:: UnwindSafe ,
273
- S : std:: fmt:: Display ,
274
339
{
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
276
350
}
277
351
278
352
// TODO(bromeon): make call_ctx lazy-evaluated (like error_ctx) everywhere;
@@ -285,7 +359,7 @@ pub fn handle_varcall_panic<F, R>(
285
359
F : FnOnce ( ) -> Result < R , CallError > + std:: panic:: UnwindSafe ,
286
360
{
287
361
let outcome: Result < Result < R , CallError > , String > =
288
- handle_panic_with_print ( || call_ctx, code, false ) ;
362
+ handle_panic ( || format ! ( "{ call_ctx}" ) , code) ;
289
363
290
364
let call_error = match outcome {
291
365
// All good.
@@ -314,7 +388,7 @@ pub fn handle_ptrcall_panic<F, R>(call_ctx: &CallContext, code: F)
314
388
where
315
389
F : FnOnce ( ) -> R + std:: panic:: UnwindSafe ,
316
390
{
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) ;
318
392
319
393
let call_error = match outcome {
320
394
// All good.
@@ -343,91 +417,6 @@ fn report_call_error(call_error: CallError, track_globally: bool) -> i32 {
343
417
}
344
418
}
345
419
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
-
431
420
// ----------------------------------------------------------------------------------------------------------------------------------------------
432
421
433
422
#[ cfg( test) ]
0 commit comments