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