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 ; 
1010use  schemars:: JsonSchema ; 
1111use  serde:: { Deserialize ,  Serialize } ; 
1212use  std:: { 
@@ -19,7 +19,6 @@ use thiserror::Error;
1919static  FRAME_CSTR :  & std:: ffi:: CStr  = c"frame" ; 
2020static  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 ) ]  
2524pub  enum  CallbackType  { 
@@ -72,6 +71,50 @@ pub struct RuntimeStackFrame {
7271pub  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 ) ]  
132157pub  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
0 commit comments