In this tutorial, Software Architect Mark Williamson follows on from Greg's tutorial on Debugging with pretty-printers in GDB by illustrating how to write pretty printers for more complex data structures.
Getting to the point_t
Our examples in this tutorial will revolve around data structures from an imaginary vector graphics program. As we work up to more complex geometry types we will extend to our pretty printers to help us understand the structures involved.
We’ll start with a very fundamental structure, representing a single point:
typedef struct {
int x;
int y;
} point_t;
Lets suppose our program has defined an instance of this type, representing a point in 2D space:
point_t p = {
.x = 1,
.y = 2,
};
Printing this immediately in GDB gives us a fairly familiar, though not particularly compact, representation:
(gdb) print p
$9 = {
x = 1,
y = 2
}
In our previous examples, we already learnt how to write a basic pretty printer. A similar pretty-printer script for our point_t
might look like this:
import gdb
class PointPrinter:
def __init__(self, val):
self.val = val
def to_string(self):
return '({x}, {y})'.format(
x=self.val['x'],
y=self.val['y']
)
def my_pp_func(val):
if str(val.type) == 'point_t': return PointPrinter(val)
return None
gdb.pretty_printers.append(my_pp_func)
This gives us a more compact output format that’s more suitable for the coordinate data we’re representing:
(gdb) print p
$8 = (1, 2)
Line up for nested structures!
Our point_t
structure is very likely to be used as an element of other data structures. For instance, in a drawing program, we might have a line_t
that contains two points:
typedef struct {
point_t start;
point_t end;
} line_t;
Let’s assume we’d like to pretty print this too. We could just copy-and-paste the code from the previous pretty-printer with some edits:
...
def to_string(self):
return '<({x1}, {y1}), ({x2}, {y2})>'.format(
x1=self.val['start']['x'],
y1=self.val['start']['y'],
x2=self.val['end']['x'],
y2=self.val['end']['y']
)
...
But this is already getting a bit repetitive. Fortunately, GDB already has this covered for us: when formatting strings for printing, it will recursively call existing pretty printers. So, actually, we can write:
class PointPrinter:
...
class LinePrinter:
def __init__(self, val):
self.val = val
def to_string(self):
return '<{p1}, {p2}>'.format(
p1=self.val['start'], p2=self.val['end']
)
def my_pp_func(val):
if str(val.type) == 'point_t': return PointPrinter(val)
elif str(val.type) == 'line_t': return LinePrinter(val)
return None
gdb.pretty_printers.append(my_pp_func)
Which produces the output:
(gdb) print l
$7 = <(3, 4), (5, 6)>
Deferring formatting decisions to existing pretty printers also means that formatting changes can be made in one place and reflected everywhere. For instance, if we now change our original PointPrinter
to say:
...
def to_string(self):
return 'Point({x}, {y})'.format(
x=self.val['x'],
y=self.val['y']
)
...
Then we’ll see also this reflected when we print a line_t
:
(gdb) print l
$8 = <Point(3, 4), Point(5, 6)>
UDB Time Travel Debugger
Find and fix test failures in minutes - including C/C++ race conditions, deadlocks and memory corruptions
Learn more »
More complex data structures - let GDB do the walk
We can also use pretty printers to handle more complicated data structures, for instance ones that use pointers to chain together multiple constituent structs.
Our vector drawing program will need a data structure to record all of the objects in the system. This structure allows us to track all the allocated points and lines:
struct drawing_element;
typedef struct drawing_element {
enum {
ELEMENT_POINT,
ELEMENT_LINE
} kind;
union element {
point_t *point;
line_t *line;
} el;
struct drawing_element *next; /* NULL-terminated */
} drawing_element_t;
Note that this can be an arbitrarily large data structure. For our example here, let's just chain together a couple of elements:
drawing_element_t last = {
.kind = ELEMENT_POINT,
.el = {
.point = &p,
},
.next = NULL,
};
drawing_element_t head = {
.kind = ELEMENT_LINE,
.el = {
.line = &l,
},
.next = &last,
};
Printing it won’t provide very helpful output by default, even with our existing pretty printers:
(gdb) print head
$1 = {
kind = ELEMENT_LINE,
el = {
point = 0x7fffffffd980,
line = 0x7fffffffd980
},
next = 0x7fffffffd960
}
We can see the enum value of kind
- which is helpful. Other than that, we just have a pair of pointers to the el
member and a next
pointer. We could walk along these pointers manually but it would be much nicer if GDB could do that work for us.
If we write a pretty printer for the drawing_element_t
then we can build a more complex string renderer that will walk the data structure and give us a summary. To do this, we’ll define a children()
method, which tells GDB that the type it’s pretty printing somehow contains other values of interest:
class DrawingElementPrinter:
def __init__(self, val):
self.val = val
def to_string(self):
# Describe the overall container structure.
return 'drawing_element_t @ {}'.format(self.val.address)
def children(self):
curr = self.val
enum_type = self.val['kind'].type
while curr:
# For each drawing_element_t in the list, check “kind” and
# choose the point_t or line_t pointer as appropriate.
if curr['kind'] == enum_type['ELEMENT_POINT'].enumval:
el_ptr = curr['el']['point']
elif curr['kind'] == enum_type['ELEMENT_LINE'].enumval:
el_ptr = curr['el']['line']
# Prepare for next loop.
curr = curr['next'].dereference() if curr['next'] != 0 else None
# Yield the child element - a string name and the value
# we want to show.
yield 'el', el_ptr.dereference()
def display_hint(self):
# Tell GDB how to display the output of children().
return 'array' # Format our sequence like an array’s contents.
def my_pp_func(val):
if str(val.type) == 'point_t': return PointPrinter(val)
elif str(val.type) == 'line_t': return LinePrinter(val)
elif str(val.type) == 'drawing_element_t':
return DrawingElementPrinter(val)
return None
gdb.pretty_printers.append(my_pp_func)
This pretty printer will:
- Display the location of our data structure
- Walk the linked list to retrieve all the elements
- For each element, use the kind field to determine its type
- Delegate display of that element to the existing pretty printer (by dereferencing the appropriate pointer)
By combining these elements, we have built a pretty printer for the whole chained data structure rather than for a single value. When we try our print
command again, we’ll see something much more interesting:
(gdb) p head
$2 = drawing_element_t @ 0x7fffffffd820 = {<Point(3, 4), Point(5, 6)>, Point(1, 2)}
Since we selected the “array” display hint, this will automatically reflect preferences for printing arrays (as set by set print array
).
The printer we’ve built will automatically walk a list of arbitrary length; with the full power of Python available, combined with GDB’s access to data values and types, it is possible to decode arbitrarily complex data structures.
UDB Time Travel Debugger
Find and fix bugs in minutes - save time on debugging
Learn more »
Let GDB help you
That’s it for this tutorial - we’ve seen how GDB can automatically invoke the right pretty printer to display your data and handle more advanced data structures. Use these techniques and you’ll get a better debug experience for less effort spent.
The full source of the examples shown above can be downloaded for reference:
Learn more about pretty printers
If you really want to get to an advanced level on pretty printers, then read the last installment in this series. Check out part 3.