use crate::crash_info::StackFrame;
use core::ffi::c_char;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[cfg(unix)]
use core::{
ptr,
sync::atomic::{AtomicPtr, Ordering},
};
use thiserror::Error;
#[cfg(unix)]
static FRAME_CSTR: &core::ffi::CStr = c"frame";
#[cfg(unix)]
static STACKTRACE_STRING_CSTR: &core::ffi::CStr = c"stacktrace_string";
#[cfg(unix)]
#[derive(Debug)]
pub enum CallbackData {
Frame(RuntimeFrameCallback),
StacktraceString(RuntimeStacktraceStringCallback),
}
#[cfg(unix)]
static RUNTIME_CALLBACK: AtomicPtr<CallbackData> = AtomicPtr::new(ptr::null_mut());
#[derive(Debug, Clone)]
pub struct RuntimeStackFrame<'a> {
pub line: u32,
pub column: u32,
pub function: &'a [u8],
pub file: &'a [u8],
pub type_name: &'a [u8],
}
pub type RuntimeFrameCallback =
unsafe extern "C" fn(emit_frame: unsafe extern "C" fn(&RuntimeStackFrame));
pub type RuntimeStacktraceStringCallback =
unsafe extern "C" fn(emit_stacktrace_string: unsafe extern "C" fn(*const c_char));
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct RuntimeStack {
pub format: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub frames: Vec<StackFrame>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stacktrace_string: Option<String>,
}
#[derive(Debug, Error)]
pub enum CallbackError {
#[error("Null callback function provided")]
NullCallback,
}
#[cfg(unix)]
pub fn register_runtime_frame_callback(
callback: RuntimeFrameCallback,
) -> Result<(), CallbackError> {
if callback as usize == 0 {
return Err(CallbackError::NullCallback);
}
let callback_data = Box::into_raw(Box::new(CallbackData::Frame(callback)));
let previous = RUNTIME_CALLBACK.swap(callback_data, Ordering::SeqCst);
if !previous.is_null() {
let _ = unsafe { Box::from_raw(previous) };
}
Ok(())
}
#[cfg(unix)]
pub fn register_runtime_stacktrace_string_callback(
callback: RuntimeStacktraceStringCallback,
) -> Result<(), CallbackError> {
if callback as usize == 0 {
return Err(CallbackError::NullCallback);
}
let callback_data = Box::into_raw(Box::new(CallbackData::StacktraceString(callback)));
let previous = RUNTIME_CALLBACK.swap(callback_data, Ordering::SeqCst);
if !previous.is_null() {
let _ = unsafe { Box::from_raw(previous) };
}
Ok(())
}
#[cfg(unix)]
pub fn is_runtime_callback_registered() -> bool {
!RUNTIME_CALLBACK.load(Ordering::SeqCst).is_null()
}
#[cfg(all(unix, feature = "collector"))]
pub(crate) unsafe fn get_registered_callback() -> Option<CallbackData> {
let callback_ptr = RUNTIME_CALLBACK.load(Ordering::SeqCst);
if callback_ptr.is_null() {
return None;
}
Some(callback_ptr.read())
}
#[cfg(unix)]
pub unsafe fn get_registered_callback_type_ptr() -> *const core::ffi::c_char {
let callback_ptr = RUNTIME_CALLBACK.load(Ordering::SeqCst);
if callback_ptr.is_null() {
return core::ptr::null();
}
let callback_data = &*callback_ptr;
match callback_data {
CallbackData::Frame(_) => FRAME_CSTR.as_ptr(),
CallbackData::StacktraceString(_) => STACKTRACE_STRING_CSTR.as_ptr(),
}
}
#[cfg(unix)]
pub unsafe fn clear_runtime_callback() {
let old_ptr = RUNTIME_CALLBACK.swap(core::ptr::null_mut(), Ordering::SeqCst);
if !old_ptr.is_null() {
let _ = Box::from_raw(old_ptr);
}
}
#[cfg(all(unix, feature = "collector"))]
pub(crate) unsafe fn invoke_runtime_callback_with_writer<W: std::io::Write>(
writer: &mut W,
) -> Result<(), std::io::Error> {
static mut CURRENT_WRITER: Option<&'static mut dyn std::io::Write> = None;
let callback_ptr = RUNTIME_CALLBACK.load(Ordering::SeqCst);
if callback_ptr.is_null() {
return Err(std::io::Error::other("No runtime callback registered"));
}
let callback_data = &*callback_ptr;
CURRENT_WRITER = Some(core::mem::transmute::<
&mut dyn std::io::Write,
&'static mut dyn std::io::Write,
>(writer));
unsafe extern "C" fn emit_frame_collector(frame: &RuntimeStackFrame) {
if let Some(ref mut writer) = CURRENT_WRITER {
let _ = emit_frame_as_json(writer, frame);
let _ = writer.flush();
}
}
unsafe extern "C" fn emit_stacktrace_string_collector(stacktrace_string: *const c_char) {
if stacktrace_string.is_null() {
return;
}
if let Some(ref mut writer) = CURRENT_WRITER {
let cstr = core::ffi::CStr::from_ptr(stacktrace_string);
let bytes = cstr.to_bytes();
let _ = writer.write_all(bytes);
let _ = writeln!(writer);
let _ = writer.flush();
}
}
match callback_data {
CallbackData::Frame(cb) => cb(emit_frame_collector),
CallbackData::StacktraceString(cb) => cb(emit_stacktrace_string_collector),
}
CURRENT_WRITER = None;
Ok(())
}
#[cfg(all(unix, feature = "collector"))]
unsafe fn emit_frame_as_json(
writer: &mut dyn std::io::Write,
frame: &RuntimeStackFrame,
) -> std::io::Result<()> {
write!(writer, "{{")?;
let mut first_field = true;
if !frame.function.is_empty() {
if !first_field {
write!(writer, ", ")?;
}
write!(writer, "\"function\": {:?}", frame.function)?;
first_field = false;
}
if !frame.type_name.is_empty() {
if !first_field {
write!(writer, ", ")?;
}
write!(writer, "\"type_name\": {:?}", frame.type_name)?;
first_field = false;
}
if !frame.file.is_empty() {
if !first_field {
write!(writer, ", ")?;
}
write!(writer, "\"file\": {:?}", frame.file)?;
first_field = false;
}
if frame.line != 0 {
if !first_field {
write!(writer, ", ")?;
}
write!(writer, "\"line\": {}", frame.line)?;
first_field = false;
}
if frame.column != 0 {
if !first_field {
write!(writer, ", ")?;
}
write!(writer, "\"column\": {}", frame.column)?;
}
writeln!(writer, "}}")?;
Ok(())
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::sync::Mutex;
static TEST_MUTEX: Mutex<()> = Mutex::new(());
unsafe extern "C" fn test_emit_frame_callback(
emit_frame: unsafe extern "C" fn(&RuntimeStackFrame),
) {
let type_name = "TestModule.TestClass";
let function_name = "test_function";
let file_name = "test.rb";
let frame = RuntimeStackFrame {
type_name: type_name.as_bytes(),
function: function_name.as_bytes(),
file: file_name.as_bytes(),
line: 42,
column: 10,
};
emit_frame(&frame);
}
#[cfg(feature = "collector")]
unsafe extern "C" fn test_emit_stacktrace_string_callback(
emit_stacktrace_string: unsafe extern "C" fn(*const c_char),
) {
let stacktrace_string = alloc::ffi::CString::new("test_stacktrace_string").unwrap();
emit_stacktrace_string(stacktrace_string.as_ptr());
}
fn ensure_callback_cleared() {
let old_ptr = RUNTIME_CALLBACK.swap(ptr::null_mut(), Ordering::SeqCst);
if !old_ptr.is_null() {
let _ = unsafe { Box::from_raw(old_ptr) };
}
}
#[test]
fn test_callback_registration() {
let _guard = TEST_MUTEX.lock().unwrap();
ensure_callback_cleared();
let result = register_runtime_frame_callback(test_emit_frame_callback);
assert!(result.is_ok(), "Failed to register callback: {:?}", result);
let result = register_runtime_frame_callback(test_emit_frame_callback);
assert!(
result.is_ok(),
"Failed to re-register callback: {:?}",
result
);
}
#[test]
#[cfg_attr(miri, ignore)]
#[cfg(feature = "collector")]
fn test_frame_collection() {
let _guard = TEST_MUTEX.lock().unwrap();
ensure_callback_cleared();
let result = register_runtime_frame_callback(test_emit_frame_callback);
assert!(result.is_ok(), "Failed to register callback: {:?}", result);
let mut buffer = Vec::new();
let invocation_result = unsafe { invoke_runtime_callback_with_writer(&mut buffer) };
assert!(
invocation_result.is_ok(),
"Failed to invoke callback with writer"
);
let json_output = String::from_utf8(buffer).expect("Invalid UTF-8 in output");
assert!(
json_output.contains("\"function\""),
"Missing function field"
);
let function_bytes = format!("{:?}", "test_function".as_bytes());
assert!(
json_output.contains(&function_bytes),
"Missing function name as byte array"
);
assert!(
json_output.contains("\"type_name\""),
"Missing type_name field"
);
let type_name_bytes = format!("{:?}", "TestModule.TestClass".as_bytes());
assert!(
json_output.contains(&type_name_bytes),
"Missing type_name as byte array"
);
assert!(json_output.contains("\"file\""), "Missing file field");
let file_bytes = format!("{:?}", "test.rb".as_bytes());
assert!(
json_output.contains(&file_bytes),
"Missing file name as byte array"
);
assert!(json_output.contains("\"line\": 42"), "Missing line number");
assert!(
json_output.contains("\"column\": 10"),
"Missing column number"
);
}
#[test]
#[cfg_attr(miri, ignore)]
#[cfg(feature = "collector")]
fn test_stacktrace_string_collection() {
let _guard = TEST_MUTEX.lock().unwrap();
ensure_callback_cleared();
let result =
register_runtime_stacktrace_string_callback(test_emit_stacktrace_string_callback);
assert!(result.is_ok(), "Failed to register callback: {:?}", result);
let mut buffer = Vec::new();
let invocation_result = unsafe { invoke_runtime_callback_with_writer(&mut buffer) };
assert!(
invocation_result.is_ok(),
"Failed to invoke callback with writer"
);
let json_output = String::from_utf8(buffer).expect("Invalid UTF-8 in output");
assert!(
json_output.contains("test_stacktrace_string"),
"Missing stacktrace string"
);
}
#[test]
#[cfg(feature = "collector")]
fn test_no_callback_registered() {
let _guard = TEST_MUTEX.lock().unwrap();
ensure_callback_cleared();
let mut buffer = Vec::new();
let invocation_result = unsafe { invoke_runtime_callback_with_writer(&mut buffer) };
#[allow(clippy::std_instead_of_core)]
{
assert_eq!(
invocation_result.unwrap_err().kind(),
std::io::ErrorKind::Other,
"Expected Other error when no callback registered"
);
}
assert!(
buffer.is_empty(),
"Expected empty buffer when no callback registered"
);
}
#[test]
#[cfg_attr(miri, ignore)]
#[cfg(feature = "collector")]
fn test_direct_pipe_writing() {
let _guard = TEST_MUTEX.lock().unwrap();
ensure_callback_cleared();
let result = register_runtime_frame_callback(test_emit_frame_callback);
assert!(result.is_ok(), "Failed to register callback: {:?}", result);
let mut buffer = Vec::new();
let invocation_result = unsafe { invoke_runtime_callback_with_writer(&mut buffer) };
assert!(
invocation_result.is_ok(),
"Failed to invoke callback with writer"
);
let json_output = String::from_utf8(buffer).expect("Invalid UTF-8 in output");
assert!(
json_output.contains("\"function\""),
"Missing function field"
);
let function_bytes = format!("{:?}", "test_function".as_bytes());
assert!(
json_output.contains(&function_bytes),
"Missing function name as byte array"
);
assert!(
json_output.contains("\"type_name\""),
"Missing type_name field"
);
let type_name_bytes = format!("{:?}", "TestModule.TestClass".as_bytes());
assert!(
json_output.contains(&type_name_bytes),
"Missing type name as byte array"
);
assert!(json_output.contains("\"file\""), "Missing file field");
let file_bytes = format!("{:?}", "test.rb".as_bytes());
assert!(
json_output.contains(&file_bytes),
"Missing file name as byte array"
);
assert!(json_output.contains("\"line\": 42"), "Missing line number");
assert!(
json_output.contains("\"column\": 10"),
"Missing column number"
);
}
}