@@ -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
// ----------------------------------------------------------------------------------------------------------------------------------------------
@@ -180,11 +180,6 @@ pub unsafe fn has_virtual_script_method(
180
180
sys:: interface_fn!( object_has_script_method) ( sys:: to_const_ptr ( object_ptr) , method_sname) != 0
181
181
}
182
182
183
- pub fn flush_stdout ( ) {
184
- use std:: io:: Write ;
185
- std:: io:: stdout ( ) . flush ( ) . expect ( "flush stdout" ) ;
186
- }
187
-
188
183
/// Ensure `T` is an editor plugin.
189
184
pub const fn is_editor_plugin < T : crate :: obj:: Inherits < crate :: classes:: EditorPlugin > > ( ) { }
190
185
@@ -221,15 +216,7 @@ pub fn is_class_runtime(is_tool: bool) -> bool {
221
216
// ----------------------------------------------------------------------------------------------------------------------------------------------
222
217
// Panic handling
223
218
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 {
233
220
if let Some ( s) = err. downcast_ref :: < & ' static str > ( ) {
234
221
s. to_string ( )
235
222
} 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 {
239
226
}
240
227
}
241
228
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}\n Context: {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
+
243
243
// If the message contains newlines, print all of the lines after a line break, and indent them.
244
244
let lbegin = "\n " ;
245
245
let indented = msg. replace ( '\n' , lbegin) ;
246
246
247
247
if indented. len ( ) != msg. len ( ) {
248
- format ! ( "[panic ]{lbegin}{indented}" )
248
+ format ! ( "[{prefix} ]{lbegin}{indented}" )
249
249
} else {
250
- format ! ( "[panic ] {msg}" )
250
+ format ! ( "[{prefix} ] {msg}" )
251
251
}
252
252
}
253
253
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
+
254
273
pub fn set_error_print_level ( level : u8 ) -> u8 {
255
274
assert ! ( level <= 2 ) ;
256
275
ERROR_PRINT_LEVEL . swap ( level, atomic:: Ordering :: Relaxed )
@@ -261,19 +280,75 @@ pub(crate) fn has_error_print_level(level: u8) -> bool {
261
280
ERROR_PRINT_LEVEL . load ( atomic:: Ordering :: Relaxed ) >= level
262
281
}
263
282
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
+
264
331
/// Executes `code`. If a panic is thrown, it is caught and an error message is printed to Godot.
265
332
///
266
333
/// Returns `Err(message)` if a panic occurred, and `Ok(result)` with the result of `code` otherwise.
267
334
///
268
335
/// In contrast to [`handle_varcall_panic`] and [`handle_ptrcall_panic`], this function is not intended for use in `try_` functions,
269
336
/// 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 >
271
338
where
272
- E : FnOnce ( ) -> S ,
339
+ E : Fn ( ) -> String ,
273
340
F : FnOnce ( ) -> R + std:: panic:: UnwindSafe ,
274
- S : std:: fmt:: Display ,
275
341
{
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
277
352
}
278
353
279
354
// TODO(bromeon): make call_ctx lazy-evaluated (like error_ctx) everywhere;
@@ -286,7 +361,7 @@ pub fn handle_varcall_panic<F, R>(
286
361
F : FnOnce ( ) -> Result < R , CallError > + std:: panic:: UnwindSafe ,
287
362
{
288
363
let outcome: Result < Result < R , CallError > , String > =
289
- handle_panic_with_print ( || call_ctx, code, false ) ;
364
+ handle_panic ( || format ! ( "{ call_ctx}" ) , code) ;
290
365
291
366
let call_error = match outcome {
292
367
// All good.
@@ -315,7 +390,7 @@ pub fn handle_ptrcall_panic<F, R>(call_ctx: &CallContext, code: F)
315
390
where
316
391
F : FnOnce ( ) -> R + std:: panic:: UnwindSafe ,
317
392
{
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) ;
319
394
320
395
let call_error = match outcome {
321
396
// All good.
@@ -344,91 +419,6 @@ fn report_call_error(call_error: CallError, track_globally: bool) -> i32 {
344
419
}
345
420
}
346
421
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
-
432
422
// ----------------------------------------------------------------------------------------------------------------------------------------------
433
423
434
424
#[ cfg( test) ]
0 commit comments