Skip to content

Commit a530d99

Browse files
committed
First pass
1 parent 3ee9ed8 commit a530d99

File tree

2 files changed

+78
-88
lines changed

2 files changed

+78
-88
lines changed

datadog-crashtracker-ffi/src/runtime_callback.rs

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ use datadog_crashtracker::{
1515
pub use datadog_crashtracker::CallbackType as ddog_CallbackType;
1616
pub use datadog_crashtracker::RuntimeType as ddog_RuntimeType;
1717

18-
#[cfg(test)]
19-
use datadog_crashtracker::clear_runtime_callback;
20-
use std::ffi::c_char;
18+
pub use datadog_crashtracker::RuntimeStackFrame as ddog_RuntimeStackFrame;
2119

2220
/// Result type for runtime callback registration
2321
#[repr(C)]
@@ -60,7 +58,7 @@ impl From<CallbackError> for CallbackResult {
6058
/// static void my_runtime_callback(
6159
/// void (*emit_frame)(const ddog_RuntimeStackFrame*),
6260
/// void (*emit_stacktrace_string)(const char*),
63-
/// void* context
61+
/// void* writer_ctx
6462
/// ) {
6563
/// // Collect runtime frames and call emit_frame for each one
6664
/// ddog_RuntimeStackFrame frame = {
@@ -71,7 +69,7 @@ impl From<CallbackError> for CallbackResult {
7169
/// .class_name = "MyClass",
7270
/// .module_name = NULL
7371
/// };
74-
/// emit_frame(&frame);
72+
/// emit_frame(writer_ctx, &frame);
7573
/// }
7674
///
7775
///
@@ -131,7 +129,7 @@ pub extern "C" fn ddog_crasht_is_runtime_callback_registered() -> bool {
131129
/// - The caller should not free the returned pointer
132130
/// - The returned string should be copied if it needs to persist beyond callback lifetime
133131
#[no_mangle]
134-
pub unsafe extern "C" fn ddog_crasht_get_registered_runtime_type() -> *const c_char {
132+
pub unsafe extern "C" fn ddog_crasht_get_registered_runtime_type() -> *const std::ffi::c_char {
135133
get_registered_runtime_type_ptr()
136134
}
137135

@@ -145,18 +143,15 @@ pub unsafe extern "C" fn ddog_crasht_get_registered_runtime_type() -> *const c_c
145143
/// - The caller should not free the returned pointer
146144
/// - The returned string should be copied if it needs to persist beyond callback lifetime
147145
#[no_mangle]
148-
pub unsafe extern "C" fn ddog_crasht_get_registered_callback_type() -> *const c_char {
146+
pub unsafe extern "C" fn ddog_crasht_get_registered_callback_type() -> *const std::ffi::c_char {
149147
get_registered_callback_type_ptr()
150148
}
151149

152-
// Re-export the core RuntimeStackFrame for C interop; consumers need to create RuntimeStackFrame
153-
pub use datadog_crashtracker::RuntimeStackFrame as ddog_RuntimeStackFrame;
154-
155150
#[cfg(test)]
156151
mod tests {
157152
use super::*;
158-
use datadog_crashtracker::RuntimeStackFrame;
159-
use std::ffi::CString;
153+
use datadog_crashtracker::{clear_runtime_callback, RuntimeStackFrame};
154+
use std::ffi::{c_char, c_void, CString};
160155
use std::ptr;
161156
use std::sync::Mutex;
162157

@@ -165,8 +160,9 @@ mod tests {
165160
static TEST_MUTEX: Mutex<()> = Mutex::new(());
166161

167162
unsafe extern "C" fn test_runtime_callback(
168-
emit_frame: unsafe extern "C" fn(*const RuntimeStackFrame),
169-
_emit_stacktrace_string: unsafe extern "C" fn(*const c_char),
163+
emit_frame: unsafe extern "C" fn(*mut c_void, *const RuntimeStackFrame),
164+
_emit_stacktrace_string: unsafe extern "C" fn(*mut c_void, *const c_char),
165+
writer_ctx: *mut c_void,
170166
) {
171167
let function_name = CString::new("test_function").unwrap();
172168
let file_name = CString::new("test.rb").unwrap();
@@ -183,7 +179,7 @@ mod tests {
183179
module_name: ptr::null(),
184180
};
185181

186-
emit_frame(&frame);
182+
emit_frame(writer_ctx, &frame);
187183
}
188184

189185
#[test]

datadog-crashtracker/src/runtime_callback.rs

Lines changed: 67 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,6 @@ impl std::str::FromStr for CallbackType {
114114
static RUNTIME_CALLBACK: AtomicPtr<(RuntimeStackCallback, RuntimeType, CallbackType)> =
115115
AtomicPtr::new(ptr::null_mut());
116116

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-
122117
#[repr(C)]
123118
#[derive(Debug, Clone)]
124119
pub struct RuntimeStackFrame {
@@ -145,17 +140,21 @@ pub struct RuntimeStackFrame {
145140
/// - Only async-signal-safe functions
146141
///
147142
/// # 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
150147
///
151148
/// # Safety
152149
/// The callback function is marked unsafe because:
153150
/// - It receives function pointers that take raw pointers as parameters
154151
/// - The callback must ensure any pointers it passes to these functions are valid
155152
/// - 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
156154
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,
159158
);
160159

161160
/// Runtime stack representation for JSON serialization
@@ -367,78 +366,69 @@ pub(crate) unsafe fn invoke_runtime_callback_with_writer<W: std::io::Write>(
367366
// to a properly aligned, initialized tuple.
368367
let (callback_fn, _, _) = &*callback_ptr;
369368

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;
403397
}
404398

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() {
409405
return;
410406
}
411407

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+
412411
// Safety: stacktrace_string is guaranteed by the runtime callback contract to be
413412
// a valid, null-terminated C string that remains valid for the duration of this call.
414413
let cstr = std::ffi::CStr::from_ptr(stacktrace_string);
415414
let bytes = cstr.to_bytes();
416415

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();
426418
}
427419

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;
432423

433-
// Invoke the user callback
424+
// Invoke the user callback with the writer context
434425
// 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+
);
442432

443433
Ok(())
444434
}
@@ -555,8 +545,9 @@ mod tests {
555545
static TEST_MUTEX: Mutex<()> = Mutex::new(());
556546

557547
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,
560551
) {
561552
let function_name = CString::new("test_function").unwrap();
562553
let file_name = CString::new("test.rb").unwrap();
@@ -571,16 +562,19 @@ mod tests {
571562
module_name: ptr::null(),
572563
};
573564

574-
emit_frame(&frame);
565+
// Safety: frame is a valid RuntimeStackFrame with valid C string pointers
566+
emit_frame(writer_ctx, &frame);
575567
}
576568

577569
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,
580573
) {
581574
let stacktrace_string = CString::new("test_stacktrace_string").unwrap();
582575

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());
584578
}
585579

586580
fn ensure_callback_cleared() {

0 commit comments

Comments
 (0)