Skip to content

Commit 544d748

Browse files
Add rb-context command.
1 parent 3663aba commit 544d748

19 files changed

+532
-62
lines changed

context/getting-started.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,9 @@ Set a breakpoint and run:
112112
Once stopped, use Ruby debugging commands:
113113

114114
~~~
115-
(gdb) rb-object-print $ec->cfp->sp[-1] # Print top of VM stack
116-
(gdb) rb-fiber-scan-heap # Scan heap for fibers
117115
(gdb) rb-stack-trace # Show combined Ruby/C backtrace
116+
(gdb) rb-fiber-scan-heap # Scan heap for fibers
117+
(gdb) rb-heap-scan --type RUBY_T_STRING --limit 5 # Find strings
118118
~~~
119119

120120
### Debugging a Core Dump
@@ -130,8 +130,9 @@ Diagnose the issue (extensions load automatically if installed):
130130
~~~
131131
(gdb) rb-fiber-scan-heap # Scan heap for all fibers
132132
(gdb) rb-fiber-scan-stack-trace-all # Show backtraces for all fibers
133-
(gdb) rb-object-print $ec->errinfo # Print exception objects
134-
(gdb) rb-heap-scan --type RUBY_T_HASH # Find all hashes
133+
(gdb) rb-fiber-scan-switch 0 # Switch to main fiber
134+
(gdb) rb-object-print $errinfo --depth 2 # Print exception (now $errinfo is set)
135+
(gdb) rb-heap-scan --type RUBY_T_HASH --limit 10 # Find hashes
135136
~~~
136137

137138
## Common Workflows
@@ -143,10 +144,11 @@ When a Ruby exception occurs, you can inspect it in detail:
143144
~~~
144145
(gdb) break rb_exc_raise
145146
(gdb) run
146-
(gdb) rb-object-print $ec->errinfo --depth 2
147+
(gdb) rb-context
148+
(gdb) rb-object-print $errinfo --depth 2
147149
~~~
148150

149-
This shows the exception class, message, and any nested structures.
151+
This shows the exception class, message, and any nested structures. The `rb-context` command displays the current execution context and sets up `$ec`, `$cfp`, and `$errinfo` convenience variables.
150152

151153
### Debugging Fiber Issues
152154

data/toolbox/context.py

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
"""Ruby execution context utilities and commands."""
2+
3+
import debugger
4+
import format
5+
import value
6+
7+
8+
class RubyContext:
9+
"""Wrapper for Ruby execution context (rb_execution_context_t).
10+
11+
Provides a high-level interface for working with Ruby execution contexts,
12+
including inspection, convenience variable setup, and information display.
13+
14+
Example:
15+
ctx = RubyContext.current()
16+
if ctx:
17+
ctx.print_info(terminal)
18+
ctx.setup_convenience_variables()
19+
"""
20+
21+
def __init__(self, ec):
22+
"""Create a RubyContext wrapper.
23+
24+
Args:
25+
ec: Execution context pointer (rb_execution_context_t *)
26+
"""
27+
self.ec = ec
28+
self._cfp = None
29+
self._errinfo = None
30+
self._vm_stack = None
31+
self._vm_stack_size = None
32+
33+
@classmethod
34+
def current(cls):
35+
"""Get the current execution context from the running thread.
36+
37+
Tries multiple approaches in order of preference:
38+
1. ruby_current_ec - TLS variable (works in GDB, some LLDB)
39+
2. rb_current_ec_noinline() - function call (works in most cases)
40+
3. rb_current_ec() - macOS-specific function
41+
42+
Returns:
43+
RubyContext instance, or None if not available
44+
"""
45+
# Try ruby_current_ec variable first
46+
try:
47+
ec = debugger.parse_and_eval('ruby_current_ec')
48+
if ec is not None and int(ec) != 0:
49+
return cls(ec)
50+
except debugger.Error:
51+
pass
52+
53+
# Fallback to rb_current_ec_noinline() function
54+
try:
55+
ec = debugger.parse_and_eval('rb_current_ec_noinline()')
56+
if ec is not None and int(ec) != 0:
57+
return cls(ec)
58+
except debugger.Error:
59+
pass
60+
61+
# Last resort: rb_current_ec() (macOS-specific)
62+
try:
63+
ec = debugger.parse_and_eval('rb_current_ec()')
64+
if ec is not None and int(ec) != 0:
65+
return cls(ec)
66+
except debugger.Error:
67+
pass
68+
69+
return None
70+
71+
@property
72+
def cfp(self):
73+
"""Get control frame pointer (lazy load)."""
74+
if self._cfp is None:
75+
try:
76+
self._cfp = self.ec['cfp']
77+
except Exception:
78+
pass
79+
return self._cfp
80+
81+
@property
82+
def errinfo(self):
83+
"""Get exception VALUE (lazy load)."""
84+
if self._errinfo is None:
85+
try:
86+
self._errinfo = self.ec['errinfo']
87+
except Exception:
88+
pass
89+
return self._errinfo
90+
91+
@property
92+
def has_exception(self):
93+
"""Check if there's a real exception (not nil/special value)."""
94+
if self.errinfo is None:
95+
return False
96+
try:
97+
errinfo_int = int(self.errinfo)
98+
# Check if it's a real object (not nil or other special constant)
99+
is_special = (errinfo_int & 0x03) != 0 or errinfo_int == 0
100+
return not is_special
101+
except Exception:
102+
return False
103+
104+
@property
105+
def vm_stack(self):
106+
"""Get VM stack pointer (lazy load)."""
107+
if self._vm_stack is None:
108+
try:
109+
self._vm_stack = self.ec['vm_stack']
110+
except Exception:
111+
pass
112+
return self._vm_stack
113+
114+
@property
115+
def vm_stack_size(self):
116+
"""Get VM stack size (lazy load)."""
117+
if self._vm_stack_size is None:
118+
try:
119+
self._vm_stack_size = int(self.ec['vm_stack_size'])
120+
except Exception:
121+
pass
122+
return self._vm_stack_size
123+
124+
def setup_convenience_variables(self):
125+
"""Set up convenience variables for this execution context.
126+
127+
Sets:
128+
$ec - Execution context pointer
129+
$cfp - Control frame pointer
130+
$errinfo - Current exception (if any)
131+
132+
Returns:
133+
dict with keys: 'ec', 'cfp', 'errinfo' (values are the set variables)
134+
"""
135+
result = {}
136+
137+
# Set $ec
138+
debugger.set_convenience_variable('ec', self.ec)
139+
result['ec'] = self.ec
140+
141+
# Set $cfp (control frame pointer)
142+
if self.cfp is not None:
143+
debugger.set_convenience_variable('cfp', self.cfp)
144+
result['cfp'] = self.cfp
145+
else:
146+
result['cfp'] = None
147+
148+
# Set $errinfo if there's an exception
149+
if self.has_exception:
150+
debugger.set_convenience_variable('errinfo', self.errinfo)
151+
result['errinfo'] = self.errinfo
152+
else:
153+
result['errinfo'] = None
154+
155+
return result
156+
157+
def print_info(self, terminal):
158+
"""Print detailed information about this execution context.
159+
160+
Args:
161+
terminal: Terminal formatter for output
162+
"""
163+
print("Execution Context:")
164+
print(f" $ec = ", end='')
165+
print(terminal.print_type_tag('rb_execution_context_t', int(self.ec), None))
166+
167+
# VM Stack info
168+
if self.vm_stack is not None and self.vm_stack_size is not None:
169+
print(f" VM Stack: ", end='')
170+
print(terminal.print_type_tag('VALUE', int(self.vm_stack), f'size={self.vm_stack_size}'))
171+
else:
172+
print(f" VM Stack: <unavailable>")
173+
174+
# Control Frame info
175+
if self.cfp is not None:
176+
print(f" $cfp = ", end='')
177+
print(terminal.print_type_tag('rb_control_frame_t', int(self.cfp), None))
178+
else:
179+
print(f" $cfp = <unavailable>")
180+
181+
# Exception info
182+
if self.has_exception:
183+
print(f" $errinfo = ", end='')
184+
print(terminal.print_type_tag('VALUE', int(self.errinfo), None))
185+
print(" Exception present!")
186+
else:
187+
errinfo_int = int(self.errinfo) if self.errinfo else 0
188+
if errinfo_int == 4: # Qnil
189+
print(" Exception: None")
190+
elif errinfo_int == 0: # Qfalse
191+
print(" Exception: None (false)")
192+
else:
193+
print(f" Exception: None")
194+
195+
# Tag info (for ensure blocks)
196+
try:
197+
tag = self.ec['tag']
198+
tag_int = int(tag)
199+
if tag_int != 0:
200+
print(f" Tag: ", end='')
201+
print(terminal.print_type_tag('rb_vm_tag', tag_int, None))
202+
try:
203+
retval = tag['retval']
204+
retval_int = int(retval)
205+
is_retval_special = (retval_int & 0x03) != 0 or retval_int == 0
206+
if not is_retval_special:
207+
print(f" $retval available (in ensure block)")
208+
except Exception:
209+
pass
210+
except Exception:
211+
pass
212+
213+
214+
class RubyContextCommand(debugger.Command):
215+
"""Show current execution context and set convenience variables.
216+
217+
This command automatically discovers the current thread's execution context
218+
and displays detailed information about it, while also setting up convenience
219+
variables for easy inspection.
220+
221+
Usage:
222+
rb-context
223+
224+
Displays:
225+
- Execution context pointer and details
226+
- VM stack information
227+
- Control frame pointer
228+
- Exception information (if any)
229+
230+
Sets these convenience variables:
231+
$ec - Current execution context (rb_execution_context_t *)
232+
$cfp - Current control frame pointer
233+
$errinfo - Current exception (if any)
234+
235+
Example:
236+
(gdb) rb-context
237+
Execution Context:
238+
$ec = <rb_execution_context_t *@0x...>
239+
VM Stack: <VALUE *@0x...> size=1024
240+
$cfp = <rb_control_frame_t *@0x...>
241+
Exception: None
242+
243+
(gdb) rb-object-print $errinfo
244+
(gdb) rb-object-print $ec->cfp->sp[-1]
245+
"""
246+
247+
def __init__(self):
248+
super(RubyContextCommand, self).__init__("rb-context", debugger.COMMAND_USER)
249+
250+
def invoke(self, arg, from_tty):
251+
"""Execute the rb-context command."""
252+
try:
253+
terminal = format.create_terminal(from_tty)
254+
255+
# Get current execution context
256+
ctx = RubyContext.current()
257+
258+
if ctx is None:
259+
print("Error: Could not get current execution context")
260+
print()
261+
print("Possible reasons:")
262+
print(" • Ruby symbols not loaded (compile with debug symbols)")
263+
print(" • Process not stopped at a Ruby frame")
264+
print(" • Ruby not fully initialized yet")
265+
print()
266+
print("Try:")
267+
print(" • Break at a Ruby function: break rb_vm_exec")
268+
print(" • Use rb-fiber-scan-switch to switch to a fiber")
269+
print(" • Ensure Ruby debug symbols are available")
270+
return
271+
272+
# Print context information
273+
ctx.print_info(terminal)
274+
275+
# Set convenience variables
276+
vars = ctx.setup_convenience_variables()
277+
278+
print()
279+
print("Convenience variables set:")
280+
print(f" $ec - Execution context")
281+
if vars.get('cfp'):
282+
print(f" $cfp - Control frame pointer")
283+
if vars.get('errinfo'):
284+
print(f" $errinfo - Exception object")
285+
286+
print()
287+
print("Now you can use:")
288+
print(" rb-object-print $errinfo")
289+
print(" rb-object-print $ec->cfp->sp[-1]")
290+
print(" rb-stack-trace")
291+
292+
except Exception as e:
293+
print(f"Error: {e}")
294+
import traceback
295+
traceback.print_exc()
296+
297+
298+
# Register command
299+
RubyContextCommand()
300+

data/toolbox/init.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,18 @@
2525
# Try to load each extension individually
2626
extensions_to_load = [
2727
('object', 'rb-object-print'),
28+
('context', 'rb-context'),
2829
('fiber', 'rb-fiber-scan-heap, rb-fiber-switch'),
29-
('stack', 'rb-stack-print'),
30+
('stack', 'rb-stack-trace'),
3031
('heap', 'rb-heap-scan'),
3132
]
3233

3334
for module_name, commands in extensions_to_load:
3435
try:
3536
if module_name == 'object':
3637
import object
38+
elif module_name == 'context':
39+
import context
3740
elif module_name == 'fiber':
3841
import fiber
3942
elif module_name == 'stack':

0 commit comments

Comments
 (0)