Natvis-like experience with GDB pretty printers. Auto-summary

This article is part 4 of the Natvis-like experience with GDB pretty printers series:

  1. Introduction
  2. Implementing natvis features
  3. Natvis-like declarative printers
  4. Auto-summary
  5. Debugging and IDEs

Even with semi-declarative pretty printers, it is hardly possible to define a custom printer for every struct in the codebase. So it is great to have some automatic display string generation algorithm which works on everything and makes sense. MSVC debugger deep-prints all the types which are not customized. GDB does not, because we already know that its implementation is unacceptable.

Deep printing algorithm can be used safely if it accepts an extra parameter \(M\) such that:

  1. Only the first \(M\) characters are returned if the full output length exceeds \(M\) or is infinite.
  2. The whole request takes \(O(L)\) time, where \(L\) is the number of characters returned.

The parameter \(M\) can be deduced from the width of the GUI field or simply hardcoded to some sane limit.

An algorithm with such properties can be achieved by traversing the children tree recursively, appending all the strings on the fly, and stopping immediately when the length limit is achieved. It is critical that we print at least one character when we enter any node of the tree (e.g. opening brace), otherwise we can go into infinite recursion. If listing the first \(k\) children of a node takes \(O(k)\) time, then this algorithm works strictly in \(O(L)\) time. There is no need for such strictness in the real practice, of course.

Deep printing implementation

To begin with, we need some container to handle the limited concatenation:

# helper for simple concatenation of strings into a text of limited length
class StringBuilder:
    # return True on overflow
    def append(self, s):
        ...
    def finalize(self):
        ...

The recursive function which deep-prints a value accepts a builder and pushes all the results into it.

# appends deep-printed text for the given value
# return True iff build is overflown after the call
def append_deep_summary(builder, value):
    simple = print_simple_value(value)
    if simple is not None:
        return builder.append(simple)

    # avoid TYPE_CODE_TYPEDEF
    value = value.cast(gdb.types.get_basic_type(value.type))

    if value.type.code == gdb.TYPE_CODE_ARRAY:
        (n, elemtype) = get_array_length_and_element_type(value.type)
        if builder.append('{ '):
            return True
        for i in range(n):
            if i > 0 and builder.append(', '):
                return True
            if append_deep_summary(builder, value[i]):
                return True
        if builder.append(' }'):
            return True
        return False

    pp = default_visualizer(value)
    if pp and not getattr(type(pp), 'auto_summary', False):
        # never deep-print custom types with pretty-printer
        return builder.append(pp.to_string())

    if value.type.code == gdb.TYPE_CODE_PTR:
        if int(value) == 0:
            return builder.append('null')
        if builder.append('0x{:x} '.format(int(value))):
            return True
        try:
            target_value = value.dereference()
        except:
            return builder.append('bad') # includes void
        return append_deep_summary(builder, target_value)

    if value.type.code not in [gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION]:
        return builder.append('???')

    count = 0
    def process_base(tvalue):
        for f in tvalue.type.fields():
            if f.artificial:
                continue
            if f.is_base_class:
                base_type = gdb.lookup_type(f.name)
                base_value = tvalue.cast(base_type)
                if process_base(base_value):
                    return True
                continue
            nonlocal count
            if builder.append('%s%s: ' % (', ' if count > 0 else '', f.name)):
                return True
            count += 1
            if append_deep_summary(builder, get_field(tvalue, f)):
                return True
        return False

    if builder.append('{ '):
        return True
    if process_base(value):
        return True
    if builder.append(' }'):
        return True
    return False

You can notice that some code that duplicates previous sections:

  • Enumerating raw members of struct/union. But this time base classes are flattened, and we can early-out on output overflow.
  • Enumerating array elements. This time with early-out.
  • Resolving pointers by prepending address.

This is how this traversal can be used to get the final string output:

# returns auto summary as a final string with limit applied
# for performance reasons, it should not be concatenated with anything else...
def get_auto_summary_as_string(value, prefix = ''):
    builder = StringBuilder()
    builder.append(prefix)
    if value is not None:
        append_deep_summary(builder, value)
    return builder.finalize()

Default display string

Recall that we want this algorithm to be used for all non-customized types. Let's put it into to_string method of a pretty printer:

# pretty-printer to produce auto-summary for non-customized types
class AutoDisplayStringPrinter:
    def __init__(self, value, prefix = ''):
        self.value = value
        self.prefix = prefix
    def to_string(self):
        return get_auto_summary_as_string(self.value, self.prefix)
    def children(self):
        if self.value is None:
            return []
        return raw_children_inline(self.value)

Here are some examples of summaries generated by this algorithm. Includes structs, pointers, strings, customized types.

Unfortunately, we can't put this pretty printer into our TdmPrettyPrinterCollection for two reasons: 1) it needs to match all types, and 2) it has to have the least priority among everything. A custom collection class is needed. But it needs some of the same code, so it looks like a big chunk of copy/paste, which is not very interesting.

What is interesting is how this auto-summary pretty printer is registered in GDB:

def register_auto_display_string(event = None):
    # according to docs, gdb.pretty_printers has the least priority of 3 groups
    for i, x in enumerate(gdb.pretty_printers):
        if isinstance(x, gdb.printing.PrettyPrinter) and x.name == 'auto_display_string_printer':
            del gdb.pretty_printers[i]
            break
    gdb.pretty_printers.append(AutoDisplayStringDefaultPrinterCollection())
    # CLion has builtin printer 'lookup' which covers all arrays in a way we don't like
    # in order to see array contents in display string, we have to override it
    gdb.execute('disable pretty-printer global lookup')

if ENABLE_AUTO_SUMMARY:
    # delay registration until executable is loaded into GDB
    # this allows us to set this pretty printer last
    # also it is necessary because 'lookup' is loaded late
    gdb.events.executable_changed.connect(register_auto_display_string)
    # but also run it now: this helps for manual refresh of pretty printers without restarting debugging
    register_auto_display_string()

First, we register the collection differently, we register it to the global list, which has the least priority. Thus deleting the old version on hot-reload is something to be implemented manually. Second, CLion has built-in pretty printer collection called lookup, which is weaker than our auto-summary but has greater priority. This built-in collection is registered after .gdbinit is finished, so we have to subscribe to executable_changed GDB event in order to disable it properly.

Custom printers and auto-summary

As long as we are careful regarding performance, we can also use the auto-summary inside our custom printers. For example, this is how we make idListPrinter show its elements in the display string:

class idListPrinter:
    ...
    def to_string(self):
        n = int(self.value['num'])
        header = 'List[%d]' % n
        if ENABLE_AUTO_SUMMARY:
            builder = StringBuilder()
            builder.append(header + ' { ')
            for i in range(n):
                if i > 0:
                    builder.append(', ')
                if append_deep_summary(builder, self.value['list'][i]):
                    break
            builder.append(' }')
            return builder.finalize()
        else:
            return header

Here is an example with deep-printed array elements underlined:

Comments (0)
atom feed: comments

There are no comments yet.

Add a Comment



?

social