1use crate::crash_info::StackFrame;
10use core::ffi::c_char;
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13
14#[cfg(unix)]
15use core::{
16 ptr,
17 sync::atomic::{AtomicPtr, Ordering},
18};
19use thiserror::Error;
20
21#[cfg(unix)]
22static FRAME_CSTR: &core::ffi::CStr = c"frame";
23#[cfg(unix)]
24static STACKTRACE_STRING_CSTR: &core::ffi::CStr = c"stacktrace_string";
25
26#[cfg(unix)]
27#[derive(Debug)]
28pub enum CallbackData {
29 Frame(RuntimeFrameCallback),
30 StacktraceString(RuntimeStacktraceStringCallback),
31}
32
33#[cfg(unix)]
35static RUNTIME_CALLBACK: AtomicPtr<CallbackData> = AtomicPtr::new(ptr::null_mut());
36
37#[derive(Debug, Clone)]
38pub struct RuntimeStackFrame<'a> {
39 pub line: u32,
41 pub column: u32,
43 pub function: &'a [u8],
45 pub file: &'a [u8],
47 pub type_name: &'a [u8],
49}
50
51pub type RuntimeFrameCallback =
63 unsafe extern "C" fn(emit_frame: unsafe extern "C" fn(&RuntimeStackFrame));
64
65pub type RuntimeStacktraceStringCallback =
77 unsafe extern "C" fn(emit_stacktrace_string: unsafe extern "C" fn(*const c_char));
78
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
81pub struct RuntimeStack {
82 pub format: String,
83 #[serde(default, skip_serializing_if = "Vec::is_empty")]
86 pub frames: Vec<StackFrame>,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub stacktrace_string: Option<String>,
90}
91
92#[derive(Debug, Error)]
93pub enum CallbackError {
94 #[error("Null callback function provided")]
95 NullCallback,
96}
97
98#[cfg(unix)]
99pub fn register_runtime_frame_callback(
100 callback: RuntimeFrameCallback,
101) -> Result<(), CallbackError> {
102 if callback as usize == 0 {
103 return Err(CallbackError::NullCallback);
104 }
105
106 let callback_data = Box::into_raw(Box::new(CallbackData::Frame(callback)));
107 let previous = RUNTIME_CALLBACK.swap(callback_data, Ordering::SeqCst);
108
109 if !previous.is_null() {
110 let _ = unsafe { Box::from_raw(previous) };
113 }
114
115 Ok(())
116}
117
118#[cfg(unix)]
119pub fn register_runtime_stacktrace_string_callback(
120 callback: RuntimeStacktraceStringCallback,
121) -> Result<(), CallbackError> {
122 if callback as usize == 0 {
123 return Err(CallbackError::NullCallback);
124 }
125
126 let callback_data = Box::into_raw(Box::new(CallbackData::StacktraceString(callback)));
127 let previous = RUNTIME_CALLBACK.swap(callback_data, Ordering::SeqCst);
128
129 if !previous.is_null() {
130 let _ = unsafe { Box::from_raw(previous) };
133 }
134
135 Ok(())
136}
137
138#[cfg(unix)]
140pub fn is_runtime_callback_registered() -> bool {
141 !RUNTIME_CALLBACK.load(Ordering::SeqCst).is_null()
142}
143
144#[cfg(all(unix, feature = "collector"))]
152pub(crate) unsafe fn get_registered_callback() -> Option<CallbackData> {
153 let callback_ptr = RUNTIME_CALLBACK.load(Ordering::SeqCst);
154 if callback_ptr.is_null() {
155 return None;
156 }
157
158 Some(callback_ptr.read())
162}
163
164#[cfg(unix)]
172pub unsafe fn get_registered_callback_type_ptr() -> *const core::ffi::c_char {
173 let callback_ptr = RUNTIME_CALLBACK.load(Ordering::SeqCst);
174 if callback_ptr.is_null() {
175 return core::ptr::null();
176 }
177
178 let callback_data = &*callback_ptr;
183 match callback_data {
184 CallbackData::Frame(_) => FRAME_CSTR.as_ptr(),
185 CallbackData::StacktraceString(_) => STACKTRACE_STRING_CSTR.as_ptr(),
186 }
187}
188
189#[cfg(unix)]
198pub unsafe fn clear_runtime_callback() {
199 let old_ptr = RUNTIME_CALLBACK.swap(core::ptr::null_mut(), Ordering::SeqCst);
200 if !old_ptr.is_null() {
201 let _ = Box::from_raw(old_ptr);
204 }
205}
206
207#[cfg(all(unix, feature = "collector"))]
216pub(crate) unsafe fn invoke_runtime_callback_with_writer<W: std::io::Write>(
217 writer: &mut W,
218) -> Result<(), std::io::Error> {
219 static mut CURRENT_WRITER: Option<&'static mut dyn std::io::Write> = None;
220
221 let callback_ptr = RUNTIME_CALLBACK.load(Ordering::SeqCst);
222 if callback_ptr.is_null() {
223 return Err(std::io::Error::other("No runtime callback registered"));
224 }
225 let callback_data = &*callback_ptr;
226
227 CURRENT_WRITER = Some(core::mem::transmute::<
228 &mut dyn std::io::Write,
229 &'static mut dyn std::io::Write,
230 >(writer));
231
232 unsafe extern "C" fn emit_frame_collector(frame: &RuntimeStackFrame) {
233 if let Some(ref mut writer) = CURRENT_WRITER {
234 let _ = emit_frame_as_json(writer, frame);
235 let _ = writer.flush();
236 }
237 }
238
239 unsafe extern "C" fn emit_stacktrace_string_collector(stacktrace_string: *const c_char) {
240 if stacktrace_string.is_null() {
241 return;
242 }
243
244 if let Some(ref mut writer) = CURRENT_WRITER {
245 let cstr = core::ffi::CStr::from_ptr(stacktrace_string);
247 let bytes = cstr.to_bytes();
248 let _ = writer.write_all(bytes);
249 let _ = writeln!(writer);
250 let _ = writer.flush();
251 }
252 }
253
254 match callback_data {
255 CallbackData::Frame(cb) => cb(emit_frame_collector),
256 CallbackData::StacktraceString(cb) => cb(emit_stacktrace_string_collector),
257 }
258
259 CURRENT_WRITER = None;
260
261 Ok(())
262}
263
264#[cfg(all(unix, feature = "collector"))]
271unsafe fn emit_frame_as_json(
272 writer: &mut dyn std::io::Write,
273 frame: &RuntimeStackFrame,
274) -> std::io::Result<()> {
275 write!(writer, "{{")?;
280
281 let mut first_field = true;
282
283 if !frame.function.is_empty() {
284 if !first_field {
285 write!(writer, ", ")?;
286 }
287 write!(writer, "\"function\": {:?}", frame.function)?;
288 first_field = false;
289 }
290
291 if !frame.type_name.is_empty() {
292 if !first_field {
293 write!(writer, ", ")?;
294 }
295 write!(writer, "\"type_name\": {:?}", frame.type_name)?;
296 first_field = false;
297 }
298
299 if !frame.file.is_empty() {
300 if !first_field {
301 write!(writer, ", ")?;
302 }
303 write!(writer, "\"file\": {:?}", frame.file)?;
304 first_field = false;
305 }
306
307 if frame.line != 0 {
308 if !first_field {
309 write!(writer, ", ")?;
310 }
311 write!(writer, "\"line\": {}", frame.line)?;
312 first_field = false;
313 }
314
315 if frame.column != 0 {
316 if !first_field {
317 write!(writer, ", ")?;
318 }
319 write!(writer, "\"column\": {}", frame.column)?;
320 }
321
322 writeln!(writer, "}}")?;
323 Ok(())
324}
325
326#[cfg(all(test, unix))]
327mod tests {
328 use super::*;
329 use std::sync::Mutex;
330
331 static TEST_MUTEX: Mutex<()> = Mutex::new(());
333
334 unsafe extern "C" fn test_emit_frame_callback(
335 emit_frame: unsafe extern "C" fn(&RuntimeStackFrame),
336 ) {
337 let type_name = "TestModule.TestClass";
338 let function_name = "test_function";
339 let file_name = "test.rb";
340
341 let frame = RuntimeStackFrame {
342 type_name: type_name.as_bytes(),
343 function: function_name.as_bytes(),
344 file: file_name.as_bytes(),
345 line: 42,
346 column: 10,
347 };
348
349 emit_frame(&frame);
350 }
351
352 #[cfg(feature = "collector")]
353 unsafe extern "C" fn test_emit_stacktrace_string_callback(
354 emit_stacktrace_string: unsafe extern "C" fn(*const c_char),
355 ) {
356 let stacktrace_string = alloc::ffi::CString::new("test_stacktrace_string").unwrap();
357
358 emit_stacktrace_string(stacktrace_string.as_ptr());
359 }
360
361 fn ensure_callback_cleared() {
362 let old_ptr = RUNTIME_CALLBACK.swap(ptr::null_mut(), Ordering::SeqCst);
363 if !old_ptr.is_null() {
364 let _ = unsafe { Box::from_raw(old_ptr) };
365 }
366 }
367
368 #[test]
369 fn test_callback_registration() {
370 let _guard = TEST_MUTEX.lock().unwrap();
371 ensure_callback_cleared();
372
373 let result = register_runtime_frame_callback(test_emit_frame_callback);
374 assert!(result.is_ok(), "Failed to register callback: {:?}", result);
375
376 let result = register_runtime_frame_callback(test_emit_frame_callback);
377 assert!(
378 result.is_ok(),
379 "Failed to re-register callback: {:?}",
380 result
381 );
382 }
383
384 #[test]
385 #[cfg_attr(miri, ignore)]
386 #[cfg(feature = "collector")]
387 fn test_frame_collection() {
388 let _guard = TEST_MUTEX.lock().unwrap();
389 ensure_callback_cleared();
390
391 let result = register_runtime_frame_callback(test_emit_frame_callback);
392 assert!(result.is_ok(), "Failed to register callback: {:?}", result);
393
394 let mut buffer = Vec::new();
395 let invocation_result = unsafe { invoke_runtime_callback_with_writer(&mut buffer) };
396 assert!(
397 invocation_result.is_ok(),
398 "Failed to invoke callback with writer"
399 );
400
401 let json_output = String::from_utf8(buffer).expect("Invalid UTF-8 in output");
402
403 assert!(
405 json_output.contains("\"function\""),
406 "Missing function field"
407 );
408
409 let function_bytes = format!("{:?}", "test_function".as_bytes());
410 assert!(
411 json_output.contains(&function_bytes),
412 "Missing function name as byte array"
413 );
414
415 assert!(
416 json_output.contains("\"type_name\""),
417 "Missing type_name field"
418 );
419
420 let type_name_bytes = format!("{:?}", "TestModule.TestClass".as_bytes());
421 assert!(
422 json_output.contains(&type_name_bytes),
423 "Missing type_name as byte array"
424 );
425
426 assert!(json_output.contains("\"file\""), "Missing file field");
427
428 let file_bytes = format!("{:?}", "test.rb".as_bytes());
429 assert!(
430 json_output.contains(&file_bytes),
431 "Missing file name as byte array"
432 );
433 assert!(json_output.contains("\"line\": 42"), "Missing line number");
434 assert!(
435 json_output.contains("\"column\": 10"),
436 "Missing column number"
437 );
438 }
439
440 #[test]
441 #[cfg_attr(miri, ignore)]
442 #[cfg(feature = "collector")]
443 fn test_stacktrace_string_collection() {
444 let _guard = TEST_MUTEX.lock().unwrap();
445 ensure_callback_cleared();
446
447 let result =
448 register_runtime_stacktrace_string_callback(test_emit_stacktrace_string_callback);
449 assert!(result.is_ok(), "Failed to register callback: {:?}", result);
450
451 let mut buffer = Vec::new();
452 let invocation_result = unsafe { invoke_runtime_callback_with_writer(&mut buffer) };
453 assert!(
454 invocation_result.is_ok(),
455 "Failed to invoke callback with writer"
456 );
457
458 let json_output = String::from_utf8(buffer).expect("Invalid UTF-8 in output");
459 assert!(
461 json_output.contains("test_stacktrace_string"),
462 "Missing stacktrace string"
463 );
464 }
465
466 #[test]
467 #[cfg(feature = "collector")]
468 fn test_no_callback_registered() {
469 let _guard = TEST_MUTEX.lock().unwrap();
470 ensure_callback_cleared();
471
472 let mut buffer = Vec::new();
474 let invocation_result = unsafe { invoke_runtime_callback_with_writer(&mut buffer) };
475
476 #[allow(clippy::std_instead_of_core)]
477 {
479 assert_eq!(
480 invocation_result.unwrap_err().kind(),
481 std::io::ErrorKind::Other,
482 "Expected Other error when no callback registered"
483 );
484 }
485
486 assert!(
487 buffer.is_empty(),
488 "Expected empty buffer when no callback registered"
489 );
490 }
491
492 #[test]
493 #[cfg_attr(miri, ignore)]
494 #[cfg(feature = "collector")]
495 fn test_direct_pipe_writing() {
496 let _guard = TEST_MUTEX.lock().unwrap();
497 ensure_callback_cleared();
498
499 let result = register_runtime_frame_callback(test_emit_frame_callback);
500 assert!(result.is_ok(), "Failed to register callback: {:?}", result);
501
502 let mut buffer = Vec::new();
504 let invocation_result = unsafe { invoke_runtime_callback_with_writer(&mut buffer) };
505 assert!(
506 invocation_result.is_ok(),
507 "Failed to invoke callback with writer"
508 );
509
510 let json_output = String::from_utf8(buffer).expect("Invalid UTF-8 in output");
512
513 assert!(
514 json_output.contains("\"function\""),
515 "Missing function field"
516 );
517
518 let function_bytes = format!("{:?}", "test_function".as_bytes());
519 assert!(
520 json_output.contains(&function_bytes),
521 "Missing function name as byte array"
522 );
523
524 assert!(
525 json_output.contains("\"type_name\""),
526 "Missing type_name field"
527 );
528
529 let type_name_bytes = format!("{:?}", "TestModule.TestClass".as_bytes());
530 assert!(
531 json_output.contains(&type_name_bytes),
532 "Missing type name as byte array"
533 );
534
535 assert!(json_output.contains("\"file\""), "Missing file field");
536
537 let file_bytes = format!("{:?}", "test.rb".as_bytes());
538 assert!(
539 json_output.contains(&file_bytes),
540 "Missing file name as byte array"
541 );
542 assert!(json_output.contains("\"line\": 42"), "Missing line number");
543 assert!(
544 json_output.contains("\"column\": 10"),
545 "Missing column number"
546 );
547 }
548}