Skip to content

Commit 1ca8039

Browse files
samuel-williams-shopifyioquatix
authored andcommitted
Better printing of robject.
1 parent 283019e commit 1ca8039

File tree

12 files changed

+298
-4
lines changed

12 files changed

+298
-4
lines changed

data/toolbox/heap.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -531,8 +531,18 @@ def _parse_type(self, type_arg):
531531
"""
532532
import constants
533533

534-
# Try as a constant name first
535-
type_value = constants.get(type_arg)
534+
# Try as a Ruby type constant (RUBY_T_*) first, using defaults table
535+
if type_arg.startswith('RUBY_T_'):
536+
try:
537+
return constants.type(type_arg)
538+
except Exception:
539+
pass
540+
541+
# Try as a general constant name
542+
try:
543+
type_value = constants.get(type_arg)
544+
except Exception:
545+
type_value = None
536546

537547
if type_value is None:
538548
# Try parsing as a number (hex or decimal)

data/toolbox/print.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import rfloat
1616
import rbignum
1717
import rbasic
18+
import robject
1819
import format
1920

2021

data/toolbox/robject.py

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import debugger
2+
import constants
3+
import format
4+
import rclass
5+
6+
7+
class RObjectBase:
8+
"""Base class for Ruby T_OBJECT instances (regular class instances like User.new)."""
9+
10+
def __init__(self, value):
11+
"""value is a VALUE pointing to a T_OBJECT."""
12+
self.value = value
13+
self.basic = value.cast(constants.type_struct('struct RBasic').pointer())
14+
self.flags = int(self.basic.dereference()['flags'])
15+
self._class_name = None
16+
17+
@property
18+
def class_name(self):
19+
if self._class_name is None:
20+
try:
21+
klass = self.basic['klass']
22+
self._class_name = rclass.get_class_name(klass)
23+
except Exception:
24+
self._class_name = f"#<Class:0x{int(self.value):x}>"
25+
return self._class_name
26+
27+
def numiv(self):
28+
"""Get the number of instance variables. Subclasses must implement."""
29+
raise NotImplementedError
30+
31+
def ivptr(self):
32+
"""Get pointer to instance variable values. Subclasses must implement."""
33+
raise NotImplementedError
34+
35+
def __str__(self):
36+
addr = int(self.value)
37+
n = self.numiv()
38+
if n > 0:
39+
return f"<{self.class_name}@0x{addr:x} ivars={n}>"
40+
return f"<{self.class_name}@0x{addr:x}>"
41+
42+
def print_to(self, terminal):
43+
addr = int(self.value)
44+
n = self.numiv()
45+
details = f"ivars={n}" if n > 0 else None
46+
terminal.print_type_tag(self.class_name, addr, details)
47+
48+
def print_recursive(self, printer, depth):
49+
printer.print(self)
50+
51+
n = self.numiv()
52+
if depth <= 0 or n <= 0:
53+
return
54+
55+
ptr = self.ivptr()
56+
if ptr is None:
57+
return
58+
59+
for i in range(n):
60+
try:
61+
printer.print_item_label(printer.max_depth - depth, i)
62+
printer.print_value(ptr[i], depth - 1)
63+
except Exception:
64+
break
65+
66+
67+
class RObjectEmbedded(RObjectBase):
68+
"""T_OBJECT with instance variables stored inline (small objects)."""
69+
70+
def __init__(self, value, robject):
71+
super().__init__(value)
72+
self.robject = robject
73+
self._numiv = None
74+
75+
def numiv(self):
76+
if self._numiv is not None:
77+
return self._numiv
78+
79+
try:
80+
# Try shape-based numiv (Ruby 3.4+)
81+
self._numiv = self._numiv_from_shape()
82+
if self._numiv is not None:
83+
return self._numiv
84+
except Exception:
85+
pass
86+
87+
# Fallback: count non-zero slots in the embedded array
88+
self._numiv = self._count_embedded_slots()
89+
return self._numiv
90+
91+
def _numiv_from_shape(self):
92+
"""Try to get ivar count from the object's shape (Ruby 3.4+)."""
93+
try:
94+
shape_id_mask = 0xFFFF # shape_id is in bits 16..31 of flags typically
95+
# In Ruby 3.4+, shape_id_t is stored in flags
96+
# SHAPE_FLAG_SHIFT is typically 16
97+
shape_id = (self.flags >> 16) & 0xFFFF
98+
if shape_id == 0:
99+
return 0
100+
101+
# Try to read from shape table
102+
shape_list = debugger.parse_and_eval('rb_shape_tree.shape_list')
103+
shape = shape_list[shape_id]
104+
next_iv = int(shape['next_iv'])
105+
return next_iv
106+
except Exception:
107+
return None
108+
109+
def _count_embedded_slots(self):
110+
"""Count non-Qundef/non-zero slots in the embedded array."""
111+
try:
112+
ary = self.robject.dereference()['as']['ary']
113+
count = 0
114+
# ROBJECT_EMBED_LEN_MAX is typically 3 (Ruby 3.2) or varies by slot size
115+
for i in range(12):
116+
try:
117+
val = int(ary[i])
118+
if val == 0 or val == 0x34: # Qundef
119+
break
120+
count += 1
121+
except Exception:
122+
break
123+
return count
124+
except Exception:
125+
return 0
126+
127+
def ivptr(self):
128+
try:
129+
return self.robject.dereference()['as']['ary']
130+
except Exception:
131+
return None
132+
133+
134+
class RObjectHeap(RObjectBase):
135+
"""T_OBJECT with instance variables stored on the heap (many ivars)."""
136+
137+
def __init__(self, value, robject):
138+
super().__init__(value)
139+
self.robject = robject
140+
self._numiv = None
141+
142+
def numiv(self):
143+
if self._numiv is not None:
144+
return self._numiv
145+
146+
try:
147+
self._numiv = int(self.robject.dereference()['as']['heap']['numiv'])
148+
except Exception:
149+
self._numiv = 0
150+
return self._numiv
151+
152+
def ivptr(self):
153+
try:
154+
return self.robject.dereference()['as']['heap']['ivptr']
155+
except Exception:
156+
return None
157+
158+
159+
class RObjectGeneric(RObjectBase):
160+
"""Fallback T_OBJECT when struct RObject is unavailable or has unknown layout."""
161+
162+
def numiv(self):
163+
return 0
164+
165+
def ivptr(self):
166+
return None
167+
168+
169+
def RObject(value):
170+
"""Factory function that detects the RObject variant and returns the appropriate instance.
171+
172+
Caller should ensure value is a RUBY_T_OBJECT before calling.
173+
"""
174+
try:
175+
robject = value.cast(constants.type_struct('struct RObject').pointer())
176+
except Exception:
177+
return RObjectGeneric(value)
178+
179+
# Detect embedded vs heap storage
180+
try:
181+
FL_USER1 = constants.flag('RUBY_FL_USER1')
182+
basic = value.cast(constants.type_struct('struct RBasic').pointer())
183+
flags = int(basic.dereference()['flags'])
184+
185+
if flags & FL_USER1:
186+
# ROBJECT_EMBED flag set — ivars are inline
187+
return RObjectEmbedded(value, robject)
188+
else:
189+
# Heap-allocated ivars
190+
return RObjectHeap(value, robject)
191+
except Exception:
192+
pass
193+
194+
# Feature detection: check what fields exist
195+
try:
196+
as_union = robject.dereference()['as']
197+
if as_union is not None:
198+
heap = as_union['heap']
199+
if heap is not None:
200+
numiv_field = heap['numiv']
201+
if numiv_field is not None:
202+
return RObjectHeap(value, robject)
203+
204+
ary = as_union['ary']
205+
if ary is not None:
206+
return RObjectEmbedded(value, robject)
207+
except Exception:
208+
pass
209+
210+
return RObjectGeneric(value)

data/toolbox/rvalue.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import rhash
1212
import rstruct
1313
import rbignum
14+
import robject
1415

1516
class RImmediate:
1617
"""Wrapper for Ruby immediate values (fixnum, nil, true, false)."""
@@ -174,6 +175,8 @@ def interpret(value):
174175
return rfloat.RFloat(value)
175176
elif type_flag == constants.type("RUBY_T_BIGNUM"):
176177
return rbignum.RBignum(value)
178+
elif type_flag == constants.type("RUBY_T_OBJECT"):
179+
return robject.RObject(value)
177180
else:
178181
# Unknown type - return generic RBasic
179182
return rbasic.RBasic(value)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Test printing T_OBJECT (regular class instance)
2+
3+
# Reduce GDB verbosity
4+
set verbose off
5+
set confirm off
6+
set pagination off
7+
set print thread-events off
8+
9+
source data/toolbox/init.py
10+
11+
# Enable pending breakpoints (for when symbols load from shared libraries)
12+
set breakpoint pending on
13+
14+
# Break at the point where puts is called
15+
break rb_f_puts
16+
run
17+
18+
echo ===TOOLBOX-OUTPUT-START===\n
19+
rb-heap-scan --type RUBY_T_OBJECT --limit 1
20+
rb-print $heap0
21+
echo ===TOOLBOX-OUTPUT-END===\n
22+
23+
quit
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
object = Object.new
7+
puts object
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Scanning heap for type 0x..., limit=1...
2+
3+
Found 1 object(s):
4+
5+
[0] $heap0 = <Object@...>
6+
7+
Objects saved in $heap0 through $heap0
8+
Next scan address saved to $heap: 0x...
9+
Run "..." for next page
10+
<Object@...>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Test printing T_OBJECT (regular class instance)
2+
3+
command script import data/toolbox/init.py
4+
5+
b rb_f_puts
6+
run
7+
8+
script print("===TOOLBOX-OUTPUT-START===")
9+
rb-heap-scan --type RUBY_T_OBJECT --limit 1
10+
rb-print $heap0
11+
script print("===TOOLBOX-OUTPUT-END===")
12+
13+
quit
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
object = Object.new
7+
puts object
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Scanning heap for type 0x..., limit=1...
2+
3+
Found 1 object(s):
4+
5+
[0] $heap0 = <Object@...>
6+
7+
Objects saved in $heap0 through $heap0
8+
Next scan address saved to $heap: 0x...
9+
Run "..." for next page
10+
<Object@...>

0 commit comments

Comments
 (0)