Skip to content

Commit ccd889e

Browse files
committed
docs: add listing-004 for Rust recorder internals
- outline recorder allocation and hook management in Rust extension - explain conversion of basic Ruby values for tracing
1 parent 414a5ea commit ccd889e

File tree

1 file changed

+213
-0
lines changed

1 file changed

+213
-0
lines changed

listing-004.md

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
# Listing 004
2+
3+
This section unpacks how the Rust extension manages the recorder’s lifecycle and begins translating Ruby objects into traceable Rust structures. We examine `gems/codetracer-ruby-recorder/ext/native_tracer/src/lib.rs` around the helper for struct serialization, the FFI hooks that allocate and enable the recorder, and the start of `to_value` which handles primitive Ruby types.
4+
5+
**Serialize a Ruby struct into a typed runtime-tracing record, computing field type IDs and assigning a versioned name.**
6+
```rust
7+
unsafe fn struct_value(
8+
recorder: &mut Recorder,
9+
class_name: &str,
10+
field_names: &[&str],
11+
field_values: &[VALUE],
12+
depth: usize,
13+
) -> ValueRecord {
14+
let mut vals = Vec::with_capacity(field_values.len());
15+
for &v in field_values {
16+
vals.push(to_value(recorder, v, depth - 1));
17+
}
18+
19+
let version_entry = recorder
20+
.struct_type_versions
21+
.entry(class_name.to_string())
22+
.or_insert(0);
23+
let name_version = format!("{} (#{})", class_name, *version_entry);
24+
*version_entry += 1;
25+
let mut field_types = Vec::with_capacity(field_names.len());
26+
for (n, v) in field_names.iter().zip(&vals) {
27+
field_types.push(FieldTypeRecord {
28+
name: (*n).to_string(),
29+
type_id: value_type_id(v),
30+
});
31+
}
32+
let typ = TypeRecord {
33+
kind: TypeKind::Struct,
34+
lang_type: name_version,
35+
specific_info: TypeSpecificInfo::Struct {
36+
fields: field_types,
37+
},
38+
};
39+
let type_id = TraceWriter::ensure_raw_type_id(&mut *recorder.tracer, typ);
40+
41+
ValueRecord::Struct {
42+
field_values: vals,
43+
type_id,
44+
}
45+
}
46+
```
47+
48+
**Custom destructor frees the Rust `Recorder` when Ruby’s GC releases the wrapper object.**
49+
```rust
50+
unsafe extern "C" fn recorder_free(ptr: *mut c_void) {
51+
if !ptr.is_null() {
52+
drop(Box::from_raw(ptr as *mut Recorder));
53+
}
54+
}
55+
```
56+
57+
**Declare Ruby’s view of the `Recorder` data type, wiring in the free callback for GC.**
58+
```rust
59+
static mut RECORDER_TYPE: rb_data_type_t = rb_data_type_t {
60+
wrap_struct_name: b"Recorder\0".as_ptr() as *const c_char,
61+
function: rb_data_type_struct__bindgen_ty_1 {
62+
dmark: None,
63+
dfree: Some(recorder_free),
64+
dsize: None,
65+
dcompact: None,
66+
reserved: [ptr::null_mut(); 1],
67+
},
68+
parent: ptr::null(),
69+
data: ptr::null_mut(),
70+
flags: 0 as VALUE,
71+
};
72+
```
73+
74+
**Fetch the internal `Recorder` pointer from a Ruby object, raising `IOError` if the type does not match.**
75+
```rust
76+
unsafe fn get_recorder(obj: VALUE) -> *mut Recorder {
77+
let ty = std::ptr::addr_of!(RECORDER_TYPE) as *const rb_data_type_t;
78+
let ptr = rb_check_typeddata(obj, ty);
79+
if ptr.is_null() {
80+
rb_raise(
81+
rb_eIOError,
82+
b"Invalid recorder object\0".as_ptr() as *const c_char,
83+
);
84+
}
85+
ptr as *mut Recorder
86+
}
87+
```
88+
89+
**Allocator for the Ruby class instantiates a boxed `Recorder` with default type IDs and inactive state.**
90+
```rust
91+
unsafe extern "C" fn ruby_recorder_alloc(klass: VALUE) -> VALUE {
92+
let recorder = Box::new(Recorder {
93+
tracer: create_trace_writer("ruby", &vec![], TraceEventsFileFormat::Binary),
94+
active: false,
95+
id: InternedSymbols::new(),
96+
set_class: Qnil.into(),
97+
open_struct_class: Qnil.into(),
98+
struct_type_versions: HashMap::new(),
99+
int_type_id: runtime_tracing::TypeId::default(),
100+
float_type_id: runtime_tracing::TypeId::default(),
101+
bool_type_id: runtime_tracing::TypeId::default(),
102+
string_type_id: runtime_tracing::TypeId::default(),
103+
symbol_type_id: runtime_tracing::TypeId::default(),
104+
error_type_id: runtime_tracing::TypeId::default(),
105+
});
106+
let ty = std::ptr::addr_of!(RECORDER_TYPE) as *const rb_data_type_t;
107+
rb_data_typed_object_wrap(klass, Box::into_raw(recorder) as *mut c_void, ty)
108+
}
109+
```
110+
111+
**Enable tracing by registering a low-level event hook; only one hook is active at a time.**
112+
```rust
113+
unsafe extern "C" fn enable_tracing(self_val: VALUE) -> VALUE {
114+
let recorder = &mut *get_recorder(self_val);
115+
if !recorder.active {
116+
let raw_cb: unsafe extern "C" fn(VALUE, *mut rb_trace_arg_t) = event_hook_raw;
117+
let func: rb_event_hook_func_t = Some(transmute(raw_cb));
118+
rb_add_event_hook2(
119+
func,
120+
RUBY_EVENT_LINE | RUBY_EVENT_CALL | RUBY_EVENT_RETURN | RUBY_EVENT_RAISE,
121+
self_val,
122+
rb_event_hook_flag_t::RUBY_EVENT_HOOK_FLAG_RAW_ARG,
123+
);
124+
recorder.active = true;
125+
}
126+
Qnil.into()
127+
}
128+
```
129+
130+
**Disable tracing by removing the previously installed event hook.**
131+
```rust
132+
unsafe extern "C" fn disable_tracing(self_val: VALUE) -> VALUE {
133+
let recorder = &mut *get_recorder(self_val);
134+
if recorder.active {
135+
let raw_cb: unsafe extern "C" fn(VALUE, *mut rb_trace_arg_t) = event_hook_raw;
136+
let func: rb_event_hook_func_t = Some(transmute(raw_cb));
137+
rb_remove_event_hook_with_data(func, self_val);
138+
recorder.active = false;
139+
}
140+
Qnil.into()
141+
}
142+
```
143+
144+
**Helper that converts a C string pointer to a Rust `String`, returning `None` if null.**
145+
```rust
146+
unsafe fn cstr_to_string(ptr: *const c_char) -> Option<String> {
147+
if ptr.is_null() {
148+
return None;
149+
}
150+
CStr::from_ptr(ptr).to_str().ok().map(|s| s.to_string())
151+
}
152+
```
153+
154+
**Extract a UTF‑8 string from a Ruby `VALUE`, replacing invalid bytes.**
155+
```rust
156+
unsafe fn rstring_lossy(val: VALUE) -> String {
157+
let ptr = RSTRING_PTR(val);
158+
let len = RSTRING_LEN(val) as usize;
159+
let slice = std::slice::from_raw_parts(ptr as *const u8, len);
160+
String::from_utf8_lossy(slice).to_string()
161+
}
162+
```
163+
164+
**Beginning of `to_value`: limit depth, map `nil` and booleans, and convert integers and floats with cached type IDs.**
165+
```rust
166+
unsafe fn to_value(recorder: &mut Recorder, val: VALUE, depth: usize) -> ValueRecord {
167+
if depth == 0 {
168+
return ValueRecord::None {
169+
type_id: recorder.error_type_id,
170+
};
171+
}
172+
if NIL_P(val) {
173+
return ValueRecord::None {
174+
type_id: recorder.error_type_id,
175+
};
176+
}
177+
if val == (Qtrue as VALUE) || val == (Qfalse as VALUE) {
178+
return ValueRecord::Bool {
179+
b: val == (Qtrue as VALUE),
180+
type_id: recorder.bool_type_id,
181+
};
182+
}
183+
if RB_INTEGER_TYPE_P(val) {
184+
let i = rb_num2long(val) as i64;
185+
return ValueRecord::Int {
186+
i,
187+
type_id: recorder.int_type_id,
188+
};
189+
}
190+
if RB_FLOAT_TYPE_P(val) {
191+
let f = rb_num2dbl(val);
192+
let type_id = if recorder.float_type_id == runtime_tracing::NONE_TYPE_ID {
193+
let id = TraceWriter::ensure_type_id(&mut *recorder.tracer, TypeKind::Float, "Float");
194+
recorder.float_type_id = id;
195+
id
196+
} else {
197+
recorder.float_type_id
198+
};
199+
return ValueRecord::Float { f, type_id };
200+
}
201+
if RB_SYMBOL_P(val) {
202+
return ValueRecord::String {
203+
text: cstr_to_string(rb_id2name(rb_sym2id(val))).unwrap_or_default(),
204+
type_id: recorder.symbol_type_id,
205+
};
206+
}
207+
if RB_TYPE_P(val, rb_sys::ruby_value_type::RUBY_T_STRING) {
208+
return ValueRecord::String {
209+
text: rstring_lossy(val),
210+
type_id: recorder.string_type_id,
211+
};
212+
}
213+
```

0 commit comments

Comments
 (0)