Skip to content

Commit 9f85daf

Browse files
Add rb-context command.
1 parent 3663aba commit 9f85daf

19 files changed

Lines changed: 537 additions & 62 deletions

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

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)