@@ -114,11 +114,6 @@ impl std::str::FromStr for CallbackType {
114
114
static RUNTIME_CALLBACK : AtomicPtr < ( RuntimeStackCallback , RuntimeType , CallbackType ) > =
115
115
AtomicPtr :: new ( ptr:: null_mut ( ) ) ;
116
116
117
- thread_local ! {
118
- static FRAME_WRITER : std:: cell:: RefCell <Option <* mut dyn std:: io:: Write >> = const { std:: cell:: RefCell :: new( None ) } ;
119
- static FRAME_COUNT : std:: cell:: Cell <usize > = const { std:: cell:: Cell :: new( 0 ) } ;
120
- }
121
-
122
117
#[ repr( C ) ]
123
118
#[ derive( Debug , Clone ) ]
124
119
pub struct RuntimeStackFrame {
@@ -145,17 +140,21 @@ pub struct RuntimeStackFrame {
145
140
/// - Only async-signal-safe functions
146
141
///
147
142
/// # Parameters
148
- /// - `emit_frame`: Function to call for each runtime frame (marked unsafe because it takes raw pointers)
149
- /// - `emit_stacktrace_string`: Function to call for complete stacktrace string (marked unsafe because it takes raw C string pointers)
143
+ /// - `emit_frame`: Function to call for each runtime frame (takes writer context and frame pointer)
144
+ /// - `emit_stacktrace_string`: Function to call for complete stacktrace string (takes writer
145
+ /// context and C string)
146
+ /// - `writer_ctx`: Opaque pointer to writer context that should be passed to emit functions
150
147
///
151
148
/// # Safety
152
149
/// The callback function is marked unsafe because:
153
150
/// - It receives function pointers that take raw pointers as parameters
154
151
/// - The callback must ensure any pointers it passes to these functions are valid
155
152
/// - All C strings passed must be null-terminated and remain valid for the call duration
153
+ /// - The writer_ctx must be passed unchanged to the emit functions
156
154
pub type RuntimeStackCallback = unsafe extern "C" fn (
157
- emit_frame : unsafe extern "C" fn ( * const RuntimeStackFrame ) ,
158
- emit_stacktrace_string : unsafe extern "C" fn ( * const c_char ) ,
155
+ emit_frame : unsafe extern "C" fn ( * mut std:: ffi:: c_void , * const RuntimeStackFrame ) ,
156
+ emit_stacktrace_string : unsafe extern "C" fn ( * mut std:: ffi:: c_void , * const c_char ) ,
157
+ writer_ctx : * mut std:: ffi:: c_void ,
159
158
) ;
160
159
161
160
/// Runtime stack representation for JSON serialization
@@ -367,78 +366,69 @@ pub(crate) unsafe fn invoke_runtime_callback_with_writer<W: std::io::Write>(
367
366
// to a properly aligned, initialized tuple.
368
367
let ( callback_fn, _, _) = & * callback_ptr;
369
368
370
- // Reset frame counter
371
- FRAME_COUNT . with ( |count| count. set ( 0 ) ) ;
372
-
373
- // Define the emit_frame function that writes directly to the pipe
374
- // Safety: This function is called by the user's runtime callback. It's marked unsafe
375
- // because it dereferences raw pointers, but the safety is guaranteed by the contract
376
- // with the runtime callback system.
377
- unsafe extern "C" fn emit_frame_collector ( frame : * const RuntimeStackFrame ) {
378
- // We need access to the writer, so we'll use thread-local storage for it
379
- FRAME_WRITER . with ( |writer_cell| {
380
- if let Some ( writer_ptr) = * writer_cell. borrow ( ) {
381
- // Safety: writer_ptr was stored in thread-local storage before
382
- // calling the user callback, so it's guaranteed to be valid and point
383
- // to the same writer object we passed in.
384
- let writer = & mut * writer_ptr;
385
-
386
- FRAME_COUNT . with ( |count| {
387
- let current_count = count. get ( ) ;
388
-
389
- // Add comma separator for frames after the first
390
- if current_count > 0 {
391
- let _ = write ! ( writer, ", " ) ;
392
- }
393
-
394
- // Write the frame as JSON
395
- // Safety: frame pointer is passed from the runtime callback, writer is valid
396
- let _ = emit_frame_as_json ( writer, frame) ;
397
- let _ = writer. flush ( ) ;
398
-
399
- count. set ( current_count + 1 ) ;
400
- } ) ;
401
- }
402
- } ) ;
369
+ let mut frame_count = 0usize ;
370
+
371
+ // Define the emit_frame function that writes directly to the writer
372
+ // Safety: This function receives writer context and frame pointer from the runtime callback.
373
+ // The writer context is guaranteed to be valid for the duration of the callback.
374
+ unsafe extern "C" fn emit_frame_collector (
375
+ writer_ctx : * mut std:: ffi:: c_void ,
376
+ frame : * const RuntimeStackFrame ,
377
+ ) {
378
+ if writer_ctx. is_null ( ) || frame. is_null ( ) {
379
+ return ;
380
+ }
381
+
382
+ // Safety: writer_ctx was created from a valid writer reference and frame_count pointer
383
+ let ( writer, frame_count) =
384
+ & mut * ( writer_ctx as * mut ( & mut dyn std:: io:: Write , & mut usize ) ) ;
385
+
386
+ // Add comma separator for frames after the first
387
+ if * * frame_count > 0 {
388
+ let _ = write ! ( writer, ", " ) ;
389
+ }
390
+
391
+ // Write the frame as JSON
392
+ // Safety: frame pointer is passed from the runtime callback
393
+ let _ = emit_frame_as_json ( * writer, frame) ;
394
+ let _ = writer. flush ( ) ;
395
+
396
+ * * frame_count += 1 ;
403
397
}
404
398
405
- // Safety: This function is called by the user's runtime callback with a C string pointer.
406
- // The safety requirements are documented in the RuntimeStackCallback type.
407
- unsafe extern "C" fn emit_stacktrace_string_collector ( stacktrace_string : * const c_char ) {
408
- if stacktrace_string. is_null ( ) {
399
+ // Safety: This function receives writer context and C string pointer from the runtime callback.
400
+ unsafe extern "C" fn emit_stacktrace_string_collector (
401
+ writer_ctx : * mut std:: ffi:: c_void ,
402
+ stacktrace_string : * const c_char ,
403
+ ) {
404
+ if writer_ctx. is_null ( ) || stacktrace_string. is_null ( ) {
409
405
return ;
410
406
}
411
407
408
+ // Safety: writer_ctx was created from a valid writer reference
409
+ let ( writer, _) = & mut * ( writer_ctx as * mut ( & mut dyn std:: io:: Write , & mut usize ) ) ;
410
+
412
411
// Safety: stacktrace_string is guaranteed by the runtime callback contract to be
413
412
// a valid, null-terminated C string that remains valid for the duration of this call.
414
413
let cstr = std:: ffi:: CStr :: from_ptr ( stacktrace_string) ;
415
414
let bytes = cstr. to_bytes ( ) ;
416
415
417
- FRAME_WRITER . with ( |writer_cell| {
418
- if let Some ( writer_ptr) = * writer_cell. borrow ( ) {
419
- // Safety: writer_ptr was stored in thread-local storage just before
420
- // calling the user callback, so it's guaranteed to be valid.
421
- let writer = & mut * writer_ptr;
422
- let _ = writer. write_all ( bytes) ;
423
- let _ = writer. flush ( ) ;
424
- }
425
- } ) ;
416
+ let _ = writer. write_all ( bytes) ;
417
+ let _ = writer. flush ( ) ;
426
418
}
427
419
428
- // Store the writer in thread-local storage so the emit functions can access it
429
- FRAME_WRITER . with ( |writer_cell| {
430
- * writer_cell. borrow_mut ( ) = Some ( writer as * mut W as * mut dyn std:: io:: Write ) ;
431
- } ) ;
420
+ // Create writer context that bundles writer and frame counter
421
+ let mut writer_context = ( writer as & mut dyn std:: io:: Write , & mut frame_count) ;
422
+ let writer_ctx = & mut writer_context as * mut _ as * mut std:: ffi:: c_void ;
432
423
433
- // Invoke the user callback
424
+ // Invoke the user callback with the writer context
434
425
// Safety: callback_fn was verified to be non-null during registration, and the
435
- // emit functions we're passing are compatible with the expected function signatures.
436
- callback_fn ( emit_frame_collector, emit_stacktrace_string_collector) ;
437
-
438
- // Clear the writer reference
439
- FRAME_WRITER . with ( |writer_cell| {
440
- * writer_cell. borrow_mut ( ) = None ;
441
- } ) ;
426
+ // emit functions and writer context are valid for the duration of this call.
427
+ callback_fn (
428
+ emit_frame_collector,
429
+ emit_stacktrace_string_collector,
430
+ writer_ctx,
431
+ ) ;
442
432
443
433
Ok ( ( ) )
444
434
}
@@ -555,8 +545,9 @@ mod tests {
555
545
static TEST_MUTEX : Mutex < ( ) > = Mutex :: new ( ( ) ) ;
556
546
557
547
unsafe extern "C" fn test_emit_frame_callback (
558
- emit_frame : unsafe extern "C" fn ( * const RuntimeStackFrame ) ,
559
- _emit_stacktrace_string : unsafe extern "C" fn ( * const c_char ) ,
548
+ emit_frame : unsafe extern "C" fn ( * mut std:: ffi:: c_void , * const RuntimeStackFrame ) ,
549
+ _emit_stacktrace_string : unsafe extern "C" fn ( * mut std:: ffi:: c_void , * const c_char ) ,
550
+ writer_ctx : * mut std:: ffi:: c_void ,
560
551
) {
561
552
let function_name = CString :: new ( "test_function" ) . unwrap ( ) ;
562
553
let file_name = CString :: new ( "test.rb" ) . unwrap ( ) ;
@@ -571,16 +562,19 @@ mod tests {
571
562
module_name : ptr:: null ( ) ,
572
563
} ;
573
564
574
- emit_frame ( & frame) ;
565
+ // Safety: frame is a valid RuntimeStackFrame with valid C string pointers
566
+ emit_frame ( writer_ctx, & frame) ;
575
567
}
576
568
577
569
unsafe extern "C" fn test_emit_stacktrace_string_callback (
578
- _emit_frame : unsafe extern "C" fn ( * const RuntimeStackFrame ) ,
579
- emit_stacktrace_string : unsafe extern "C" fn ( * const c_char ) ,
570
+ _emit_frame : unsafe extern "C" fn ( * mut std:: ffi:: c_void , * const RuntimeStackFrame ) ,
571
+ emit_stacktrace_string : unsafe extern "C" fn ( * mut std:: ffi:: c_void , * const c_char ) ,
572
+ writer_ctx : * mut std:: ffi:: c_void ,
580
573
) {
581
574
let stacktrace_string = CString :: new ( "test_stacktrace_string" ) . unwrap ( ) ;
582
575
583
- emit_stacktrace_string ( stacktrace_string. as_ptr ( ) ) ;
576
+ // Safety: stacktrace_string.as_ptr() returns a valid null-terminated C string
577
+ emit_stacktrace_string ( writer_ctx, stacktrace_string. as_ptr ( ) ) ;
584
578
}
585
579
586
580
fn ensure_callback_cleared ( ) {
0 commit comments