Skip to content

Commit 3ee9ed8

Browse files
committed
First pass
1 parent f61c42a commit 3ee9ed8

File tree

11 files changed

+1865
-1
lines changed

11 files changed

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

0 commit comments

Comments
 (0)