@@ -10,15 +10,20 @@ use std::{
1010
1111use rb_sys:: {
1212 rb_add_event_hook2, rb_remove_event_hook_with_data, rb_define_class,
13- rb_define_alloc_func, rb_define_method,
13+ rb_define_alloc_func, rb_define_method, rb_funcall , rb_intern ,
1414 rb_event_hook_flag_t:: RUBY_EVENT_HOOK_FLAG_RAW_ARG ,
1515 rb_event_flag_t, rb_trace_arg_t,
16- rb_tracearg_event_flag, rb_tracearg_lineno, rb_tracearg_path,
17- rb_cObject, VALUE , ID , RUBY_EVENT_LINE ,
18- RSTRING_PTR , RSTRING_LEN ,
16+ rb_tracearg_event_flag, rb_tracearg_lineno, rb_tracearg_path, rb_tracearg_self,
17+ rb_tracearg_binding, rb_tracearg_callee_id, rb_tracearg_return_value,
18+ rb_tracearg_raised_exception,
19+ rb_cObject, VALUE , ID , RUBY_EVENT_LINE , RUBY_EVENT_CALL , RUBY_EVENT_RETURN ,
20+ RUBY_EVENT_RAISE ,
1921 rb_raise, rb_eIOError,
22+ rb_sym2id, rb_id2name, rb_obj_classname, rb_num2long
2023} ;
21- use runtime_tracing:: { Tracer , Line } ;
24+ use rb_sys:: { RARRAY_LEN , RARRAY_CONST_PTR , RSTRING_LEN , RSTRING_PTR , RB_INTEGER_TYPE_P , RB_TYPE_P , RB_SYMBOL_P , NIL_P } ;
25+ use rb_sys:: { Qtrue , Qfalse , Qnil } ;
26+ use runtime_tracing:: { Tracer , Line , ValueRecord , TypeKind , EventLogKind , TraceLowLevelEvent , CallRecord , FullValueRecord , ReturnRecord , RecordEvent } ;
2227
2328#[ repr( C ) ]
2429struct RTypedData {
@@ -86,7 +91,17 @@ unsafe fn get_recorder(obj: VALUE) -> *mut Recorder {
8691}
8792
8893unsafe extern "C" fn ruby_recorder_alloc ( klass : VALUE ) -> VALUE {
89- let recorder = Box :: new ( Recorder { tracer : Tracer :: new ( "ruby" , & vec ! [ ] ) , active : false } ) ;
94+ let mut tracer = Tracer :: new ( "ruby" , & vec ! [ ] ) ;
95+ // pre-register common types to match the pure Ruby tracer
96+ tracer. ensure_type_id ( TypeKind :: Int , "Integer" ) ;
97+ tracer. ensure_type_id ( TypeKind :: String , "String" ) ;
98+ tracer. ensure_type_id ( TypeKind :: Bool , "Bool" ) ;
99+ tracer. ensure_type_id ( TypeKind :: String , "Symbol" ) ;
100+ tracer. ensure_type_id ( TypeKind :: Error , "No type" ) ;
101+ let path = Path :: new ( "" ) ;
102+ let func_id = tracer. ensure_function_id ( "<top-level>" , path, Line ( 1 ) ) ;
103+ tracer. events . push ( TraceLowLevelEvent :: Call ( CallRecord { function_id : func_id, args : vec ! [ ] } ) ) ;
104+ let recorder = Box :: new ( Recorder { tracer, active : false } ) ;
90105 let ty = std:: ptr:: addr_of!( RECORDER_TYPE ) as * const rb_data_type_t ;
91106 rb_data_typed_object_wrap ( klass, Box :: into_raw ( recorder) as * mut c_void , ty)
92107}
@@ -96,7 +111,12 @@ unsafe extern "C" fn enable_tracing(self_val: VALUE) -> VALUE {
96111 if !recorder. active {
97112 let raw_cb: unsafe extern "C" fn ( VALUE , * mut rb_trace_arg_t ) = event_hook_raw;
98113 let cb: unsafe extern "C" fn ( rb_event_flag_t , VALUE , VALUE , ID , VALUE ) = transmute ( raw_cb) ;
99- rb_add_event_hook2 ( Some ( cb) , RUBY_EVENT_LINE , self_val, RUBY_EVENT_HOOK_FLAG_RAW_ARG ) ;
114+ rb_add_event_hook2 (
115+ Some ( cb) ,
116+ RUBY_EVENT_LINE | RUBY_EVENT_CALL | RUBY_EVENT_RETURN | RUBY_EVENT_RAISE ,
117+ self_val,
118+ RUBY_EVENT_HOOK_FLAG_RAW_ARG ,
119+ ) ;
100120 recorder. active = true ;
101121 }
102122 rb_sys:: Qnil . into ( )
@@ -124,6 +144,105 @@ fn flush_to_dir(tracer: &Tracer, dir: &Path) -> Result<(), Box<dyn std::error::E
124144 Ok ( ( ) )
125145}
126146
147+ unsafe fn cstr_to_string ( ptr : * const c_char ) -> Option < String > {
148+ if ptr. is_null ( ) {
149+ return None ;
150+ }
151+ CStr :: from_ptr ( ptr) . to_str ( ) . ok ( ) . map ( |s| s. to_string ( ) )
152+ }
153+
154+ unsafe fn value_to_string ( val : VALUE ) -> Option < String > {
155+ if RB_TYPE_P ( val, rb_sys:: ruby_value_type:: RUBY_T_STRING ) {
156+ let ptr = RSTRING_PTR ( val) ;
157+ let len = RSTRING_LEN ( val) as usize ;
158+ let slice = std:: slice:: from_raw_parts ( ptr as * const u8 , len) ;
159+ return Some ( String :: from_utf8_lossy ( slice) . to_string ( ) ) ;
160+ }
161+ let to_s_id = rb_intern ( b"to_s\0 " . as_ptr ( ) as * const c_char ) ;
162+ let str_val = rb_funcall ( val, to_s_id, 0 ) ;
163+ let ptr = RSTRING_PTR ( str_val) ;
164+ let len = RSTRING_LEN ( str_val) as usize ;
165+ let slice = std:: slice:: from_raw_parts ( ptr as * const u8 , len) ;
166+ Some ( String :: from_utf8_lossy ( slice) . to_string ( ) )
167+ }
168+
169+ unsafe fn to_value ( tracer : & mut Tracer , val : VALUE , depth : usize ) -> ValueRecord {
170+ if depth == 0 {
171+ let type_id = tracer. ensure_type_id ( TypeKind :: Error , "No type" ) ;
172+ return ValueRecord :: None { type_id } ;
173+ }
174+ if NIL_P ( val) {
175+ let type_id = tracer. ensure_type_id ( TypeKind :: Error , "No type" ) ;
176+ return ValueRecord :: None { type_id } ;
177+ }
178+ if val == ( Qtrue as VALUE ) || val == ( Qfalse as VALUE ) {
179+ let type_id = tracer. ensure_type_id ( TypeKind :: Bool , "Bool" ) ;
180+ return ValueRecord :: Bool { b : val == ( Qtrue as VALUE ) , type_id } ;
181+ }
182+ if RB_INTEGER_TYPE_P ( val) {
183+ let i = rb_num2long ( val) as i64 ;
184+ let type_id = tracer. ensure_type_id ( TypeKind :: Int , "Integer" ) ;
185+ return ValueRecord :: Int { i, type_id } ;
186+ }
187+ if RB_SYMBOL_P ( val) {
188+ let id = rb_sym2id ( val) ;
189+ let name = CStr :: from_ptr ( rb_id2name ( id) ) . to_str ( ) . unwrap_or ( "" ) ;
190+ let type_id = tracer. ensure_type_id ( TypeKind :: String , "Symbol" ) ;
191+ return ValueRecord :: String { text : name. to_string ( ) , type_id } ;
192+ }
193+ if RB_TYPE_P ( val, rb_sys:: ruby_value_type:: RUBY_T_STRING ) {
194+ let ptr = RSTRING_PTR ( val) ;
195+ let len = RSTRING_LEN ( val) as usize ;
196+ let slice = std:: slice:: from_raw_parts ( ptr as * const u8 , len) ;
197+ let type_id = tracer. ensure_type_id ( TypeKind :: String , "String" ) ;
198+ return ValueRecord :: String { text : String :: from_utf8_lossy ( slice) . to_string ( ) , type_id } ;
199+ }
200+ if RB_TYPE_P ( val, rb_sys:: ruby_value_type:: RUBY_T_ARRAY ) {
201+ let len = RARRAY_LEN ( val) as usize ;
202+ let mut elements = Vec :: new ( ) ;
203+ let ptr = RARRAY_CONST_PTR ( val) ;
204+ for i in 0 ..len {
205+ let elem = * ptr. add ( i) ;
206+ elements. push ( to_value ( tracer, elem, depth - 1 ) ) ;
207+ }
208+ let type_id = tracer. ensure_type_id ( TypeKind :: Seq , "Array" ) ;
209+ return ValueRecord :: Sequence { elements, is_slice : false , type_id } ;
210+ }
211+ let class_name = cstr_to_string ( rb_obj_classname ( val) ) . unwrap_or_else ( || "Object" . to_string ( ) ) ;
212+ let text = value_to_string ( val) . unwrap_or_default ( ) ;
213+ let type_id = tracer. ensure_type_id ( TypeKind :: Raw , & class_name) ;
214+ ValueRecord :: Raw { r : text, type_id }
215+ }
216+
217+ unsafe fn record_variables ( tracer : & mut Tracer , binding : VALUE ) -> Vec < FullValueRecord > {
218+ let mut result = Vec :: new ( ) ;
219+ let locals_id = rb_intern ( b"local_variables\0 " . as_ptr ( ) as * const c_char ) ;
220+ let get_id = rb_intern ( b"local_variable_get\0 " . as_ptr ( ) as * const c_char ) ;
221+ let vars = rb_funcall ( binding, locals_id, 0 ) ;
222+ let len = RARRAY_LEN ( vars) as usize ;
223+ let ptr = RARRAY_CONST_PTR ( vars) ;
224+ for i in 0 ..len {
225+ let sym = * ptr. add ( i) ;
226+ let id = rb_sym2id ( sym) ;
227+ let name = CStr :: from_ptr ( rb_id2name ( id) ) . to_str ( ) . unwrap_or ( "" ) ;
228+ let value = rb_funcall ( binding, get_id, 1 , sym) ;
229+ let val_rec = to_value ( tracer, value, 10 ) ;
230+ tracer. register_variable_with_full_value ( name, val_rec. clone ( ) ) ;
231+ let var_id = tracer. ensure_variable_id ( name) ;
232+ result. push ( FullValueRecord { variable_id : var_id, value : val_rec } ) ;
233+ }
234+ result
235+ }
236+
237+ unsafe fn record_event ( tracer : & mut Tracer , path : & str , line : i64 , content : & str ) {
238+ tracer. register_step ( Path :: new ( path) , Line ( line) ) ;
239+ tracer. events . push ( TraceLowLevelEvent :: Event ( RecordEvent {
240+ kind : EventLogKind :: Write ,
241+ metadata : String :: new ( ) ,
242+ content : content. to_string ( ) ,
243+ } ) ) ;
244+ }
245+
127246unsafe extern "C" fn flush_trace ( self_val : VALUE , out_dir : VALUE ) -> VALUE {
128247 let recorder_ptr = get_recorder ( self_val) ;
129248 let recorder = & mut * recorder_ptr;
@@ -143,6 +262,21 @@ unsafe extern "C" fn flush_trace(self_val: VALUE, out_dir: VALUE) -> VALUE {
143262 rb_sys:: Qnil . into ( )
144263}
145264
265+ unsafe extern "C" fn record_event_api ( self_val : VALUE , path : VALUE , line : VALUE , content : VALUE ) -> VALUE {
266+ let recorder = & mut * get_recorder ( self_val) ;
267+ let path_str = if NIL_P ( path) {
268+ "" . to_string ( )
269+ } else {
270+ let ptr = RSTRING_PTR ( path) ;
271+ let len = RSTRING_LEN ( path) as usize ;
272+ String :: from_utf8_lossy ( std:: slice:: from_raw_parts ( ptr as * const u8 , len) ) . to_string ( )
273+ } ;
274+ let line_num = rb_num2long ( line) as i64 ;
275+ let content_str = value_to_string ( content) . unwrap_or_default ( ) ;
276+ record_event ( & mut recorder. tracer , & path_str, line_num, & content_str) ;
277+ rb_sys:: Qnil . into ( )
278+ }
279+
146280/// Raw-argument callback (Ruby will call it when we set
147281/// `RUBY_EVENT_HOOK_FLAG_RAW_ARG`).
148282///
@@ -161,16 +295,66 @@ unsafe extern "C" fn event_hook_raw(data: VALUE, arg: *mut rb_trace_arg_t) {
161295 }
162296
163297 let ev: rb_event_flag_t = rb_tracearg_event_flag ( arg) ;
164- if ( ev & RUBY_EVENT_LINE ) == 0 {
298+ let path_val = rb_tracearg_path ( arg) ;
299+ let line_val = rb_tracearg_lineno ( arg) ;
300+ let path = if NIL_P ( path_val) {
301+ "" . to_string ( )
302+ } else {
303+ let ptr = RSTRING_PTR ( path_val) ;
304+ let len = RSTRING_LEN ( path_val) as usize ;
305+ String :: from_utf8_lossy ( std:: slice:: from_raw_parts ( ptr as * const u8 , len) ) . to_string ( )
306+ } ;
307+ let line = rb_num2long ( line_val) as i64 ;
308+ if path. contains ( "native_trace.rb" ) {
165309 return ;
166310 }
167311
168- let path_ptr = rb_tracearg_path ( arg) as * const c_char ;
169- let line = rb_tracearg_lineno ( arg) as i64 ;
170-
171- if !path_ptr. is_null ( ) {
172- if let Ok ( path) = CStr :: from_ptr ( path_ptr) . to_str ( ) {
173- recorder. tracer . register_step ( Path :: new ( path) , Line ( line) ) ;
312+ if ( ev & RUBY_EVENT_LINE ) != 0 {
313+ let binding = rb_tracearg_binding ( arg) ;
314+ recorder. tracer . register_step ( Path :: new ( & path) , Line ( line) ) ;
315+ if !NIL_P ( binding) {
316+ record_variables ( & mut recorder. tracer , binding) ;
317+ }
318+ } else if ( ev & RUBY_EVENT_CALL ) != 0 {
319+ recorder. tracer . register_step ( Path :: new ( & path) , Line ( line) ) ;
320+ let binding = rb_tracearg_binding ( arg) ;
321+ let mut args = Vec :: new ( ) ;
322+ let self_val = rb_tracearg_self ( arg) ;
323+ let self_rec = to_value ( & mut recorder. tracer , self_val, 10 ) ;
324+ recorder. tracer . register_variable_with_full_value ( "self" , self_rec. clone ( ) ) ;
325+ args. push ( recorder. tracer . arg ( "self" , self_rec) ) ;
326+ if !NIL_P ( binding) {
327+ let mut other = record_variables ( & mut recorder. tracer , binding) ;
328+ args. append ( & mut other) ;
329+ }
330+ let mid_sym = rb_tracearg_callee_id ( arg) ;
331+ let mid = rb_sym2id ( mid_sym) ;
332+ let name_c = rb_id2name ( mid) ;
333+ let mut name = if !name_c. is_null ( ) {
334+ CStr :: from_ptr ( name_c) . to_str ( ) . unwrap_or ( "" ) . to_string ( )
335+ } else {
336+ String :: new ( )
337+ } ;
338+ let class_name = cstr_to_string ( rb_obj_classname ( self_val) ) . unwrap_or_else ( || "Object" . to_string ( ) ) ;
339+ if class_name != "Object" {
340+ name = format ! ( "{}#{}" , class_name, name) ;
341+ }
342+ let fid = recorder. tracer . ensure_function_id ( & name, Path :: new ( & path) , Line ( line) ) ;
343+ recorder. tracer . events . push ( TraceLowLevelEvent :: Call ( CallRecord { function_id : fid, args } ) ) ;
344+ } else if ( ev & RUBY_EVENT_RETURN ) != 0 {
345+ recorder. tracer . register_step ( Path :: new ( & path) , Line ( line) ) ;
346+ let ret = rb_tracearg_return_value ( arg) ;
347+ let val_rec = to_value ( & mut recorder. tracer , ret, 10 ) ;
348+ recorder. tracer . register_variable_with_full_value ( "<return_value>" , val_rec. clone ( ) ) ;
349+ recorder. tracer . events . push ( TraceLowLevelEvent :: Return ( ReturnRecord { return_value : val_rec } ) ) ;
350+ } else if ( ev & RUBY_EVENT_RAISE ) != 0 {
351+ let exc = rb_tracearg_raised_exception ( arg) ;
352+ if let Some ( msg) = value_to_string ( exc) {
353+ recorder. tracer . events . push ( TraceLowLevelEvent :: Event ( RecordEvent {
354+ kind : EventLogKind :: Error ,
355+ metadata : String :: new ( ) ,
356+ content : msg,
357+ } ) ) ;
174358 }
175359 }
176360}
@@ -183,8 +367,10 @@ pub extern "C" fn Init_codetracer_ruby_recorder() {
183367 let enable_cb: unsafe extern "C" fn ( VALUE ) -> VALUE = enable_tracing;
184368 let disable_cb: unsafe extern "C" fn ( VALUE ) -> VALUE = disable_tracing;
185369 let flush_cb: unsafe extern "C" fn ( VALUE , VALUE ) -> VALUE = flush_trace;
370+ let event_cb: unsafe extern "C" fn ( VALUE , VALUE , VALUE , VALUE ) -> VALUE = record_event_api;
186371 rb_define_method ( class, b"enable_tracing\0 " . as_ptr ( ) as * const c_char , Some ( transmute ( enable_cb) ) , 0 ) ;
187372 rb_define_method ( class, b"disable_tracing\0 " . as_ptr ( ) as * const c_char , Some ( transmute ( disable_cb) ) , 0 ) ;
188373 rb_define_method ( class, b"flush_trace\0 " . as_ptr ( ) as * const c_char , Some ( transmute ( flush_cb) ) , 1 ) ;
374+ rb_define_method ( class, b"record_event\0 " . as_ptr ( ) as * const c_char , Some ( transmute ( event_cb) ) , 3 ) ;
189375 }
190376}
0 commit comments