@@ -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,50 @@ 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
+ #[ 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}\n Context: {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
+
242
242
// If the message contains newlines, print all of the lines after a line break, and indent them.
243
243
let lbegin = "\n " ;
244
244
let indented = msg. replace ( '\n' , lbegin) ;
245
245
246
246
if indented. len ( ) != msg. len ( ) {
247
- format ! ( "[panic ]{lbegin}{indented}" )
247
+ format ! ( "[{prefix} ]{lbegin}{indented}" )
248
248
} else {
249
- format ! ( "[panic ] {msg}" )
249
+ format ! ( "[{prefix} ] {msg}" )
250
250
}
251
251
}
252
252
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
+
253
272
pub fn set_error_print_level ( level : u8 ) -> u8 {
254
273
assert ! ( level <= 2 ) ;
255
274
ERROR_PRINT_LEVEL . swap ( level, atomic:: Ordering :: Relaxed )
@@ -260,19 +279,75 @@ pub(crate) fn has_error_print_level(level: u8) -> bool {
260
279
ERROR_PRINT_LEVEL . load ( atomic:: Ordering :: Relaxed ) >= level
261
280
}
262
281
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
+
263
330
/// Executes `code`. If a panic is thrown, it is caught and an error message is printed to Godot.
264
331
///
265
332
/// Returns `Err(message)` if a panic occurred, and `Ok(result)` with the result of `code` otherwise.
266
333
///
267
334
/// In contrast to [`handle_varcall_panic`] and [`handle_ptrcall_panic`], this function is not intended for use in `try_` functions,
268
335
/// 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 >
270
337
where
271
- E : FnOnce ( ) -> S ,
338
+ E : Fn ( ) -> String ,
272
339
F : FnOnce ( ) -> R + std:: panic:: UnwindSafe ,
273
- S : std:: fmt:: Display ,
274
340
{
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
276
351
}
277
352
278
353
// TODO(bromeon): make call_ctx lazy-evaluated (like error_ctx) everywhere;
@@ -285,7 +360,7 @@ pub fn handle_varcall_panic<F, R>(
285
360
F : FnOnce ( ) -> Result < R , CallError > + std:: panic:: UnwindSafe ,
286
361
{
287
362
let outcome: Result < Result < R , CallError > , String > =
288
- handle_panic_with_print ( || call_ctx, code, false ) ;
363
+ handle_panic ( || format ! ( "{ call_ctx}" ) , code) ;
289
364
290
365
let call_error = match outcome {
291
366
// All good.
@@ -314,7 +389,7 @@ pub fn handle_ptrcall_panic<F, R>(call_ctx: &CallContext, code: F)
314
389
where
315
390
F : FnOnce ( ) -> R + std:: panic:: UnwindSafe ,
316
391
{
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) ;
318
393
319
394
let call_error = match outcome {
320
395
// All good.
@@ -343,91 +418,6 @@ fn report_call_error(call_error: CallError, track_globally: bool) -> i32 {
343
418
}
344
419
}
345
420
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
421
// ----------------------------------------------------------------------------------------------------------------------------------------------
432
422
433
423
#[ cfg( test) ]
0 commit comments