Skip to content

Commit d86a9c3

Browse files
committed
Use StackFrame in internal code; convert FFI obj to StackFrame for runtime stack
1 parent 39f7063 commit d86a9c3

File tree

4 files changed

+66
-140
lines changed

4 files changed

+66
-140
lines changed

datadog-crashtracker-ffi/src/runtime_callback.rs

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -152,33 +152,7 @@ mod tests {
152152
}
153153

154154
#[test]
155-
fn test_ffi_callback_registration() {
156-
let _guard = TEST_MUTEX.lock().unwrap();
157-
unsafe {
158-
clear_runtime_callback();
159-
assert!(!ddog_crasht_is_runtime_callback_registered());
160-
161-
let result = ddog_crasht_register_runtime_stack_callback(
162-
test_runtime_callback,
163-
CallbackType::Frame,
164-
);
165-
166-
assert_eq!(result, CallbackResult::Ok);
167-
assert!(ddog_crasht_is_runtime_callback_registered());
168-
169-
let result = ddog_crasht_register_runtime_stack_callback(
170-
test_runtime_callback,
171-
CallbackType::Frame,
172-
);
173-
assert_eq!(result, CallbackResult::Ok);
174-
assert!(ddog_crasht_is_runtime_callback_registered());
175-
176-
clear_runtime_callback();
177-
}
178-
}
179-
180-
#[test]
181-
fn test_enum_based_registration() {
155+
fn test_callback_registration() {
182156
let _guard = TEST_MUTEX.lock().unwrap();
183157
unsafe {
184158
clear_runtime_callback();

datadog-crashtracker/src/receiver/receive_report.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
use crate::{
5-
crash_info::{CrashInfo, CrashInfoBuilder, ErrorKind, Metadata, SigInfo, Span},
6-
runtime_callback::{RuntimeFrame, RuntimeStack},
5+
crash_info::{CrashInfo, CrashInfoBuilder, ErrorKind, Metadata, SigInfo, Span, StackFrame},
6+
runtime_callback::RuntimeStack,
77
shared::constants::*,
88
CrashtrackerConfiguration,
99
};
@@ -62,7 +62,7 @@ pub(crate) enum StdinState {
6262
Waiting,
6363
// StackFrame is always emitted as one stream of all the frames but StackString
6464
// may have lines that we need to accumulate depending on runtime (e.g. Python)
65-
RuntimeStackFrame(Vec<RuntimeFrame>),
65+
RuntimeStackFrame(Vec<StackFrame>),
6666
RuntimeStackString(Vec<String>),
6767
}
6868

datadog-crashtracker/src/runtime_callback.rs

Lines changed: 60 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
//! This module provides APIs for runtime languages (Ruby, Python, PHP, etc.) to register
77
//! callbacks that can provide runtime-specific stack traces during crash handling.
88
9-
/// Runtime-specific stack frame representation suitable for signal-safe context
9+
use crate::crash_info::StackFrame;
1010
use schemars::JsonSchema;
1111
use serde::{Deserialize, Serialize};
1212
use std::{
@@ -19,7 +19,6 @@ use thiserror::Error;
1919
static FRAME_CSTR: &std::ffi::CStr = c"frame";
2020
static STACKTRACE_STRING_CSTR: &std::ffi::CStr = c"stacktrace_string";
2121

22-
/// Callback type identifier for different collection strategies
2322
#[repr(C)]
2423
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2524
pub enum CallbackType {
@@ -72,6 +71,50 @@ pub struct RuntimeStackFrame {
7271
pub column_number: u32,
7372
}
7473

74+
impl From<&RuntimeStackFrame> for StackFrame {
75+
/// Convert RuntimeStackFrame (C FFI) to StackFrame (internal representation)
76+
///
77+
/// # Safety
78+
/// The caller must ensure that all C string pointers are either null or point to
79+
/// valid, null-terminated C strings that remain valid for the duration of this call.
80+
fn from(rsf: &RuntimeStackFrame) -> Self {
81+
let mut stack_frame = StackFrame::new();
82+
83+
// Convert C strings to Rust strings and populate StackFrame
84+
if !rsf.function_name.is_null() {
85+
// Safety: function_name was checked to be non-null. The caller
86+
// guarantees it points to a valid, null-terminated C string.
87+
let c_str = unsafe { std::ffi::CStr::from_ptr(rsf.function_name) };
88+
if let Ok(s) = c_str.to_str() {
89+
if !s.is_empty() {
90+
stack_frame.function = Some(s.to_string());
91+
}
92+
}
93+
}
94+
95+
if !rsf.file_name.is_null() {
96+
// Safety: file_name was checked to be non-null. The caller
97+
// guarantees it points to a valid, null-terminated C string.
98+
let c_str = unsafe { std::ffi::CStr::from_ptr(rsf.file_name) };
99+
if let Ok(s) = c_str.to_str() {
100+
if !s.is_empty() {
101+
stack_frame.file = Some(s.to_string());
102+
}
103+
}
104+
}
105+
106+
if rsf.line_number != 0 {
107+
stack_frame.line = Some(rsf.line_number);
108+
}
109+
110+
if rsf.column_number != 0 {
111+
stack_frame.column = Some(rsf.column_number);
112+
}
113+
114+
stack_frame
115+
}
116+
}
117+
75118
/// Function signature for runtime stack collection callbacks
76119
///
77120
/// This callback is invoked during crash handling in a signal context, so it must be best effort
@@ -103,30 +146,12 @@ pub struct RuntimeStack {
103146
/// Array of runtime-specific stack frames (optional, mutually exclusive with
104147
/// stacktrace_string)
105148
#[serde(default, skip_serializing_if = "Vec::is_empty")]
106-
pub frames: Vec<RuntimeFrame>,
149+
pub frames: Vec<StackFrame>,
107150
/// Raw stacktrace string (optional, mutually exclusive with frames)
108151
#[serde(default, skip_serializing_if = "Option::is_none")]
109152
pub stacktrace_string: Option<String>,
110153
}
111154

112-
/// JSON-serializable runtime stack frame
113-
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
114-
pub struct RuntimeFrame {
115-
/// Fully qualified function/method name
116-
/// Examples: "my_package.submodule.TestClass.method", "MyClass::method", "namespace::function"
117-
#[serde(default, skip_serializing_if = "Option::is_none")]
118-
pub function: Option<String>,
119-
/// Source file name
120-
#[serde(default, skip_serializing_if = "Option::is_none")]
121-
pub file: Option<String>,
122-
/// Line number in source file
123-
#[serde(default, skip_serializing_if = "Option::is_none")]
124-
pub line: Option<u32>,
125-
/// Column number in source file
126-
#[serde(default, skip_serializing_if = "Option::is_none")]
127-
pub column: Option<u32>,
128-
}
129-
130155
/// Errors that can occur during callback registration
131156
#[derive(Debug, Error)]
132157
pub enum CallbackError {
@@ -346,53 +371,12 @@ unsafe fn emit_frame_as_json(
346371
// Safety: frame was checked to be non-null above. The caller guarantees it points
347372
// to a valid RuntimeStackFrame.
348373
let frame_ref = &*frame;
349-
write!(writer, "{{")?;
350-
let mut first = true;
351-
352-
// Convert C strings to Rust strings and write JSON fields
353-
if !frame_ref.function_name.is_null() {
354-
// Safety: frame_ref.function_name was checked to be non-null. The caller
355-
// guarantees it points to a valid, null-terminated C string.
356-
let c_str = std::ffi::CStr::from_ptr(frame_ref.function_name);
357-
if let Ok(s) = c_str.to_str() {
358-
if !s.is_empty() {
359-
write!(writer, "\"function\": \"{}\"", s)?;
360-
first = false;
361-
}
362-
}
363-
}
364-
365-
if !frame_ref.file_name.is_null() {
366-
// Safety: frame_ref.file_name was checked to be non-null. The caller
367-
// guarantees it points to a valid, null-terminated C string.
368-
let c_str = std::ffi::CStr::from_ptr(frame_ref.file_name);
369-
if let Ok(s) = c_str.to_str() {
370-
if !s.is_empty() {
371-
if !first {
372-
write!(writer, ", ")?;
373-
}
374-
write!(writer, "\"file\": \"{}\"", s)?;
375-
first = false;
376-
}
377-
}
378-
}
379374

380-
if frame_ref.line_number != 0 {
381-
if !first {
382-
write!(writer, ", ")?;
383-
}
384-
write!(writer, "\"line\": {}", frame_ref.line_number)?;
385-
first = false;
386-
}
387-
388-
if frame_ref.column_number != 0 {
389-
if !first {
390-
write!(writer, ", ")?;
391-
}
392-
write!(writer, "\"column\": {}", frame_ref.column_number)?;
393-
}
375+
let stack_frame: StackFrame = frame_ref.into();
394376

395-
writeln!(writer, "}}")?;
377+
// Serialize to JSON and write
378+
let json = serde_json::to_string(&stack_frame).map_err(std::io::Error::other)?;
379+
writeln!(writer, "{}", json)?;
396380
Ok(())
397381
}
398382

@@ -494,9 +478,12 @@ mod tests {
494478
);
495479
assert!(json_output.contains("\"file\""), "Missing file field");
496480
assert!(json_output.contains("test.rb"), "Missing file name");
497-
assert!(json_output.contains("\"line\": 42"), "Missing line number");
498481
assert!(
499-
json_output.contains("\"column\": 10"),
482+
json_output.contains("\"line\":42") || json_output.contains("\"line\": 42"),
483+
"Missing line number"
484+
);
485+
assert!(
486+
json_output.contains("\"column\":10") || json_output.contains("\"column\": 10"),
500487
"Missing column number"
501488
);
502489

@@ -586,9 +573,12 @@ mod tests {
586573
);
587574
assert!(json_output.contains("\"file\""), "Missing file field");
588575
assert!(json_output.contains("test.rb"), "Missing file name");
589-
assert!(json_output.contains("\"line\": 42"), "Missing line number");
590576
assert!(
591-
json_output.contains("\"column\": 10"),
577+
json_output.contains("\"line\":42") || json_output.contains("\"line\": 42"),
578+
"Missing line number"
579+
);
580+
assert!(
581+
json_output.contains("\"column\":10") || json_output.contains("\"column\": 10"),
592582
"Missing column number"
593583
);
594584

docs/RFCs/artifacts/0011-crashtracker-unified-runtime-stack-schema.json

Lines changed: 2 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -254,44 +254,6 @@
254254
}
255255
}
256256
},
257-
"RuntimeFrame": {
258-
"description": "JSON-serializable runtime stack frame",
259-
"type": "object",
260-
"properties": {
261-
"column": {
262-
"description": "Column number in source file",
263-
"type": [
264-
"integer",
265-
"null"
266-
],
267-
"format": "uint32",
268-
"minimum": 0.0
269-
},
270-
"file": {
271-
"description": "Source file name",
272-
"type": [
273-
"string",
274-
"null"
275-
]
276-
},
277-
"function": {
278-
"description": "Fully qualified function/method name Examples: \"my_package.submodule.TestClass.method\", \"MyClass::method\", \"namespace::function\"",
279-
"type": [
280-
"string",
281-
"null"
282-
]
283-
},
284-
"line": {
285-
"description": "Line number in source file",
286-
"type": [
287-
"integer",
288-
"null"
289-
],
290-
"format": "uint32",
291-
"minimum": 0.0
292-
}
293-
}
294-
},
295257
"RuntimeStack": {
296258
"description": "Runtime stack representation for JSON serialization",
297259
"type": "object",
@@ -307,7 +269,7 @@
307269
"description": "Array of runtime-specific stack frames (optional, mutually exclusive with stacktrace_string)",
308270
"type": "array",
309271
"items": {
310-
"$ref": "#/definitions/RuntimeFrame"
272+
"$ref": "#/definitions/StackFrame"
311273
}
312274
},
313275
"stacktrace_string": {
@@ -599,4 +561,4 @@
599561
}
600562
}
601563
}
602-
}
564+
}

0 commit comments

Comments
 (0)