Skip to content

Commit 7b66803

Browse files
committed
First pass
1 parent f61c42a commit 7b66803

File tree

11 files changed

+1855
-1
lines changed

11 files changed

+1855
-1
lines changed

datadog-crashtracker-ffi/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ mod crash_info;
1616
mod demangler;
1717
#[cfg(all(unix, feature = "receiver"))]
1818
mod receiver;
19+
mod runtime_callback;
1920
#[cfg(all(unix, feature = "collector"))]
2021
pub use collector::*;
2122
#[cfg(all(windows, feature = "collector_windows"))]
@@ -25,3 +26,4 @@ pub use crash_info::*;
2526
pub use demangler::*;
2627
#[cfg(all(unix, feature = "receiver"))]
2728
pub use receiver::*;
29+
pub use runtime_callback::*;
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! FFI bindings for runtime callback registration
5+
//!
6+
//! This module provides C-compatible FFI bindings for registering runtime-specific
7+
//! crash callbacks that can provide stack traces for dynamic languages.
8+
use datadog_crashtracker::{
9+
get_registered_callback_type_ptr, get_registered_runtime_type_ptr,
10+
is_runtime_callback_registered, register_runtime_stack_callback, CallbackError, CallbackType,
11+
RuntimeStackCallback, RuntimeType,
12+
};
13+
14+
// Re-export the enums for C/C++ consumers
15+
pub use datadog_crashtracker::CallbackType as ddog_CallbackType;
16+
pub use datadog_crashtracker::RuntimeType as ddog_RuntimeType;
17+
18+
pub use datadog_crashtracker::RuntimeStackFrame as ddog_RuntimeStackFrame;
19+
20+
/// Result type for runtime callback registration
21+
#[repr(C)]
22+
#[derive(Debug, PartialEq, Eq)]
23+
pub enum CallbackResult {
24+
Ok,
25+
NullCallback,
26+
UnknownError,
27+
}
28+
29+
impl From<CallbackError> for CallbackResult {
30+
fn from(error: CallbackError) -> Self {
31+
match error {
32+
CallbackError::NullCallback => CallbackResult::NullCallback,
33+
}
34+
}
35+
}
36+
37+
/// Register a runtime stack collection callback
38+
///
39+
/// This function allows language runtimes to register a callback that will be invoked
40+
/// during crash handling to collect runtime-specific stack traces.
41+
///
42+
/// # Arguments
43+
/// - `callback`: The callback function to invoke during crashes
44+
/// - `context`: User-provided context pointer passed to the callback
45+
///
46+
/// # Returns
47+
/// - `CallbackResult::Ok` if registration succeeds
48+
/// - `CallbackResult::NullCallback` if the callback function is null
49+
///
50+
/// # Safety
51+
/// - The callback must be signal-safe
52+
/// - The context pointer must remain valid for the lifetime of the process
53+
/// - Only one callback can be registered at a time
54+
/// - The callback must be registered once on CrashTracker initialization, before any crash occurs
55+
///
56+
/// # Example Usage from C
57+
/// ```c
58+
/// static void my_runtime_callback(
59+
/// void (*emit_frame)(const ddog_RuntimeStackFrame*),
60+
/// void (*emit_stacktrace_string)(const char*),
61+
/// void* writer_ctx
62+
/// ) {
63+
/// // Collect runtime frames and call emit_frame for each one
64+
/// ddog_RuntimeStackFrame frame = {
65+
/// .function_name = "my_function",
66+
/// .file_name = "script.rb",
67+
/// .line_number = 42,
68+
/// .column_number = 10,
69+
/// .class_name = "MyClass",
70+
/// .module_name = NULL
71+
/// };
72+
/// emit_frame(writer_ctx, &frame);
73+
/// }
74+
///
75+
///
76+
/// ddog_CallbackResult result = ddog_crasht_register_runtime_stack_callback(
77+
/// my_runtime_callback,
78+
/// RuntimeType::Ruby,
79+
/// CallbackType::Frame,
80+
/// );
81+
/// ```
82+
/// Register a runtime stack collection callback using type-safe enums
83+
///
84+
/// This function provides compile-time safety by using enums instead of strings
85+
/// for runtime and callback types.
86+
///
87+
/// # Arguments
88+
/// - `callback`: The callback function to invoke during crashes
89+
/// - `runtime_type`: Runtime type enum (Python, Ruby, Php, Nodejs, Unknown)
90+
/// - `callback_type`: Callback type enum (Frame, StacktraceString)
91+
///
92+
/// # Returns
93+
/// - `CallbackResult::Ok` if registration succeeds (replaces any existing callback)
94+
/// - `CallbackResult::NullCallback` if the callback function is null
95+
///
96+
/// # Safety
97+
/// - The callback must be signal-safe
98+
/// - Only one callback can be registered at a time (this replaces any existing one)
99+
#[no_mangle]
100+
pub unsafe extern "C" fn ddog_crasht_register_runtime_stack_callback(
101+
callback: RuntimeStackCallback,
102+
runtime_type: RuntimeType,
103+
callback_type: CallbackType,
104+
) -> CallbackResult {
105+
match register_runtime_stack_callback(callback, runtime_type, callback_type) {
106+
Ok(()) => CallbackResult::Ok,
107+
Err(e) => e.into(),
108+
}
109+
}
110+
111+
/// Check if a runtime callback is currently registered
112+
///
113+
/// Returns true if a callback is registered, false otherwise
114+
///
115+
/// # Safety
116+
/// This function is safe to call at any time
117+
#[no_mangle]
118+
pub extern "C" fn ddog_crasht_is_runtime_callback_registered() -> bool {
119+
is_runtime_callback_registered()
120+
}
121+
122+
/// Get the runtime type from the currently registered callback context
123+
///
124+
/// Returns the runtime type C string pointer if a callback with valid context is registered,
125+
/// null pointer otherwise
126+
///
127+
/// # Safety
128+
/// - The returned pointer is valid only while the callback remains registered
129+
/// - The caller should not free the returned pointer
130+
/// - The returned string should be copied if it needs to persist beyond callback lifetime
131+
#[no_mangle]
132+
pub unsafe extern "C" fn ddog_crasht_get_registered_runtime_type() -> *const std::ffi::c_char {
133+
get_registered_runtime_type_ptr()
134+
}
135+
136+
/// Get the callback type from the currently registered callback context
137+
///
138+
/// Returns the callback type C string pointer if a callback with valid context is registered,
139+
/// null pointer otherwise
140+
///
141+
/// # Safety
142+
/// - The returned pointer is valid only while the callback remains registered
143+
/// - The caller should not free the returned pointer
144+
/// - The returned string should be copied if it needs to persist beyond callback lifetime
145+
#[no_mangle]
146+
pub unsafe extern "C" fn ddog_crasht_get_registered_callback_type() -> *const std::ffi::c_char {
147+
get_registered_callback_type_ptr()
148+
}
149+
150+
#[cfg(test)]
151+
mod tests {
152+
use super::*;
153+
use datadog_crashtracker::{clear_runtime_callback, RuntimeStackFrame};
154+
use std::ffi::{c_char, c_void, CString};
155+
use std::ptr;
156+
use std::sync::Mutex;
157+
158+
// Use a mutex to ensure tests run sequentially to avoid race conditions
159+
// with the global static variable
160+
static TEST_MUTEX: Mutex<()> = Mutex::new(());
161+
162+
unsafe extern "C" fn test_runtime_callback(
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,
166+
) {
167+
let function_name = CString::new("test_function").unwrap();
168+
let file_name = CString::new("test.rb").unwrap();
169+
let class_name = CString::new("TestClass").unwrap();
170+
171+
// Create the internal RuntimeStackFrame directly; no conversion needed
172+
// since both RuntimeStackFrame and ddog_RuntimeStackFrame have identical layouts
173+
let frame = RuntimeStackFrame {
174+
function_name: function_name.as_ptr(),
175+
file_name: file_name.as_ptr(),
176+
line_number: 42,
177+
column_number: 10,
178+
class_name: class_name.as_ptr(),
179+
module_name: ptr::null(),
180+
};
181+
182+
emit_frame(writer_ctx, &frame);
183+
}
184+
185+
#[test]
186+
fn test_ffi_callback_registration() {
187+
let _guard = TEST_MUTEX.lock().unwrap();
188+
unsafe {
189+
// Ensure clean state at start
190+
clear_runtime_callback();
191+
192+
// Test that no callback is initially registered
193+
assert!(!ddog_crasht_is_runtime_callback_registered());
194+
assert_eq!(ddog_crasht_get_registered_runtime_type(), ptr::null());
195+
196+
// Test successful registration using type-safe enums
197+
let result = ddog_crasht_register_runtime_stack_callback(
198+
test_runtime_callback,
199+
RuntimeType::Ruby,
200+
CallbackType::Frame,
201+
);
202+
203+
assert_eq!(result, CallbackResult::Ok);
204+
205+
// Verify callback is now registered
206+
assert!(ddog_crasht_is_runtime_callback_registered());
207+
208+
// Verify we can retrieve the runtime type
209+
let runtime_type_ptr = ddog_crasht_get_registered_runtime_type();
210+
assert!(!runtime_type_ptr.is_null());
211+
let runtime_type_str = std::ffi::CStr::from_ptr(runtime_type_ptr).to_str().unwrap();
212+
assert_eq!(runtime_type_str, "ruby");
213+
214+
// Test duplicate registration fails
215+
let result = ddog_crasht_register_runtime_stack_callback(
216+
test_runtime_callback,
217+
RuntimeType::Ruby,
218+
CallbackType::Frame,
219+
);
220+
assert_eq!(result, CallbackResult::Ok);
221+
222+
// Callback should still be registered after successful re-registration
223+
assert!(ddog_crasht_is_runtime_callback_registered());
224+
225+
// Clean up - clear the registered callback for subsequent tests
226+
clear_runtime_callback();
227+
}
228+
}
229+
230+
#[test]
231+
fn test_enum_based_registration() {
232+
let _guard = TEST_MUTEX.lock().unwrap();
233+
unsafe {
234+
clear_runtime_callback();
235+
236+
// Test that no callback is initially registered
237+
assert!(!ddog_crasht_is_runtime_callback_registered());
238+
239+
// Test registration with enum values - Python + StacktraceString
240+
let result = ddog_crasht_register_runtime_stack_callback(
241+
test_runtime_callback,
242+
RuntimeType::Python,
243+
CallbackType::StacktraceString,
244+
);
245+
246+
assert_eq!(result, CallbackResult::Ok);
247+
assert!(ddog_crasht_is_runtime_callback_registered());
248+
249+
// Verify runtime type
250+
let runtime_type_ptr = ddog_crasht_get_registered_runtime_type();
251+
assert!(!runtime_type_ptr.is_null());
252+
let runtime_type_str = std::ffi::CStr::from_ptr(runtime_type_ptr).to_str().unwrap();
253+
assert_eq!(runtime_type_str, "python");
254+
255+
// Verify callback type
256+
let callback_type_ptr = ddog_crasht_get_registered_callback_type();
257+
assert!(!callback_type_ptr.is_null());
258+
let callback_type_str = std::ffi::CStr::from_ptr(callback_type_ptr)
259+
.to_str()
260+
.unwrap();
261+
assert_eq!(callback_type_str, "stacktrace_string");
262+
263+
// Test re-registration with different values - Ruby + Frame
264+
let result = ddog_crasht_register_runtime_stack_callback(
265+
test_runtime_callback,
266+
RuntimeType::Ruby,
267+
CallbackType::Frame,
268+
);
269+
270+
assert_eq!(result, CallbackResult::Ok);
271+
272+
// Verify new values
273+
let runtime_type_ptr = ddog_crasht_get_registered_runtime_type();
274+
let runtime_type_str = std::ffi::CStr::from_ptr(runtime_type_ptr).to_str().unwrap();
275+
assert_eq!(runtime_type_str, "ruby");
276+
277+
let callback_type_ptr = ddog_crasht_get_registered_callback_type();
278+
let callback_type_str = std::ffi::CStr::from_ptr(callback_type_ptr)
279+
.to_str()
280+
.unwrap();
281+
assert_eq!(callback_type_str, "frame");
282+
283+
clear_runtime_callback();
284+
}
285+
}
286+
}

0 commit comments

Comments
 (0)