Making life prettier with gdb PrettyPrinting API

Anyone who has peeked inside a gdb manual knows that gdb has some kind of Python API. And anyone who has skimmed through has seen something called “Pretty Printing” that supposedly tells gdb how to print complex data structures in a nice and readable way. Well, at least I have seen that, but I’ve never given it much thought. Still, one day, when I was typing:

(gdb) p/t table->read_set->bitmap[0] @ (table->read_set->n_bits+7)/8

for the umpteenth time I asked myself, “why the heck not?”, and so it begun…

Now, of course, a Pretty Printer in Python is a class. The value to print is passed to a constructor, and not just a simple scalar value, but a gdb.Value object. Later gdb invokes the to_string() method of this class to get the actual string that it’ll show to the user. For example, pretty printers for a couple of MariaDB internal classes might look like:

class BitmapPrinter:
    def __init__(self, val):
        self.val = val
    def to_string(self):
        s=''
        for i in range((self.val['n_bits']+7)//8):
            s = format(int(self.val['bitmap'][i]), '032b') + s
        return "b'" + s[-int(self.val['n_bits']):] + "'"

class StringPrinter:
    def __init__(self, val):
        self.val = val
    def to_string(self):
      return '_' + self.val['str_charset']['name'].string() + \
             ' "' + self.val['Ptr'].string('ascii', 'strict',
                      self.val['str_length']) + '"'

This is not all. There should also be a callable class that will tell gdb what pretty printer to use. But one doesn’t need to write it from scratch; gdb.printing provides a RegexpCollectionPrettyPrinter class that does just that, matching types with regular expressions. For pretty printers above, it could be used like:

import gdb.printing

def build_pretty_printer():
    pp = gdb.printing.RegexpCollectionPrettyPrinter(
        "my_library")
    pp.add_printer('String', '^String$', StringPrinter)
    pp.add_printer('bitmap', '^st_bitmap$', BitmapPrinter)
    return pp

gdb.printing.register_pretty_printer(
    gdb.current_objfile(),
    my_library.build_pretty_printer()

That works. But it’s too verbose for my taste. I hate boilerplate code. And here the pretty printer classes are redundant; the only useful part of them is the to_string() method. Registration requires too much typing too. Even the type to pretty print is repeated three times there!

And it doesn’t handle pointers. One needs to create a separate pretty printer for String *, although all it’ll add is printing the address, the rest of the code is the same. Too much copy pasting. And this is with two pretty printers, I expect to have lots of them.

It doesn’t handle typedefs either; one needs to use base types in pp.add_printer with all typedefs resolved. Doesn’t help much for cases like:

typedef unsigned long long sql_mode_t

I wanted pointers, I wanted typedefs, and I did not want boilerplate code. Ideally I wanted to write pretty printers like

@PrettyPrinter
def String(val):
    return '_' + val['str_charset']['name'].string() + \
           ' "' + val['Ptr'].string('ascii', 'strict',
                    val['str_length']) + '"'

@PrettyPrinter
def st_bitmap(val):
    s=''
    for i in range((val['n_bits']+7)//8):
        s = format(int(val['bitmap'][i]), '032b') + s
    return "b'" + s[-int(val['n_bits']):] + "'"

That’s it, only the printing function and the decorator. No nonsense. And that’s what I’ve done. This is how it works:

The decorator takes the type name to be the same as the function name. And registers a pretty printer for that type. The pretty printer class will take the function (and the value, of course) as an argument in the constructor and in its to_string() method it will simply invoke said function for the said value. Because this class simply wraps the actual pretty printing function, I’ve called it PrettyPrinterWrapper.

Now, I didn’t like gdb.printing.RegexpCollectionPrettyPrinter, so I created my own callable class too. It checks both the typedef-ed and the base type of the value, so sql_mode_t can be printed correctly. And it supports pointers — if the value is a pointer to something that can be pretty printed, it will print the address, gdb style, and then invoke the pretty printer on the dereferenced value. Because it wraps PrettyPrinterWrapper, I thought it’ll be funny to call this class PrettyPrinterWrapperWrapper.

What I didn’t think of at that moment was the simple fact that I cannot create a Python function with the name Alter_inplace_info::HA_ALTER_FLAGS. I had to implement a workaround:

@PrettyPrinter('Alter_inplace_info::HA_ALTER_FLAGS')
def Aii_HA_ALTER_FLAGS(val):
    s=''
    ...

and the way to achieve that in Python is to wrap the decorator in another function that will be invoked with the string argument and should return the actual decorator that will decorate the function. At that point I realized my mistake, but it was too late to stop. The function that wraps PrettyPrinterWrapperWrapper simply had to be called PrettyPrinterWrapperWrapperWrapper. Luckily, after that everything started to work smoothly and wrapper-crazyness didn’t develop any further.

Now I can write pretty printers easily with no copy-pasting or boilerplate code:

@PrettyPrinter
def sql_mode_t(val):
    s=''
    modes=['STRICT_TRANS_TABLES', 'STRICT_ALL_TABLES', 'NO_ZERO_IN_DATE',
           'TRADITIONAL', 'NO_AUTO_CREATE_USER', 'HIGH_NOT_PRECEDENCE',
           'NO_ENGINE_SUBSTITUTION', 'PAD_CHAR_TO_FULL_LENGTH']
    for i in range(0,len(modes)):
        if val & (1 << i): s += ',' + modes[i]
    return s[1:]

And enjoy the beauty of pretty printed structures, which is, of course, so important in the debugging of MariaDB:

(gdb) p table->alias
$1 = _binary "t3"
(gdb) p &table->alias
$2 = (String *) 0x7fffd409c250 _binary "t3"
(gdb) p table->read_set[0]
$3 = b'10011'
(gdb) p thd->variables.sql_mode
$4 = STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
(gdb) p ha_alter_info.handler_flags
$5 = ADD_INDEX,DROP_INDEX,ADD_PK_INDEX,ALTER_STORED_COLUMN_ORDER

The complete implementation is here. Disclaimer, if you’d like to use this decorator for your pretty printers, beware, it’s mostly Python 2. Might need some tweaking to work for Python 3. Patches are welcome.