Custom debugger rendering for C++ objects using CLion and GDB

Debuggers and their GUIs often have built-in support for displaying a one-line summary of objects of well-known simple types, such as integers and strings. They may even have support for complex types, such as maps. However, objects of more complex or user-defined types tend to lack good built-in support. You might just get a type name and the address of your object, even though a string that usefully describes your object might be only a function call away. Users of Java and C# can address this by simply defining their own method that returns a string representing their object.1 Meanwhile, the options available to C++ users are, to put it simply, worse. Although inferior, options do exist. In this post, I show a method to override the default behavior of CLion’s GDB integration to display custom text for your objects.

First, note that this method will work only with GDB. Other debuggers such as LLDB and MSVC achieve this in their own different ways. I initially tried this with LLDB and Natvis, but it didn’t meet my needs, so here we are with a GDB-based solution.

Overview

In addition to whatever C++ code you use to generate the string to be printed, we will need to create or modify three files: a Python script my-printer.py (or whatever you want to call it), a project .gdbinit file, and a user .gdbinit file.

Python script

Create a Python script in your project. Throughout this tutorial, I will assume you placed it at ./my-printer.py where . is the root of your project. The overall idea in this script is that we declare a few classes, one per type we want to print. To reduce repetition, I introduce an abstract class, BasePrinter.

import gdb
from abc import ABC, abstractmethod

class BasePrinter(ABC):
    def __init__(self, val):
        self.val = val

    def to_string(self):
        try:
            addr = self.val.address
            if addr is None:
                addr = self.val.reference_value().address if hasattr(self.val, "reference_value") else None
            if addr is None:
                return "<No address>"
            expr = self.get_string_expression(addr)
            result = gdb.parse_and_eval(expr)
            return result.string()
        except Exception as e:
            return "<Unexpected error: {}>".format(e)

    # Returns a C++ expression that GDB will evaluate
    # to get a C string representing the object at address `addr`.
    @abstractmethod
    def get_string_expression(self, addr):
        pass

    # Allows debugger to expand structures as usual.
    def children(self):
        t = self.val.type.strip_typedefs()
        for field in t.fields():
            if field.is_base_class:
                try:
                    yield (field.name, self.val.cast(field.type))
                except Exception:
                    continue
            else:
                try:
                    yield (field.name, self.val[field.name])
                except Exception:
                    continue

Then you can define subclasses of BasePrinter:

class WidgetPrinter(BasePrinter):
    def __init__(self, val):
        super().__init__(val)

    def get_string_expression(self, addr):
        return "((company::products::Widget*){})->getModel()->getName().c_str()".format(int(addr))

class CustomerPrinter(BasePrinter):
    def __init__(self, val):
        super().__init__(val)

    def get_string_expression(self, addr):
        return "((company::Customer*){})->getFullName().c_str()".format(int(addr))

My examples use C++ code to get the strings I want the debugger to display, but you could also use pure Python to generate the strings. Indeed, if your debuggee is flaky, making the strings in Python may even be more reliable.

Next, we need to define a function that will return an instance of the appropriate printer class for the value given by GDB.

def get_printer(val):
    t = val.type.strip_typedefs()
    type_name = t.tag
    if type_name is None:
        return None

    # Use type name to select the right printer.
    if type_name == "company::products::Widget":
        return WidgetPrinter(val)
    if type_name == "company::Customer":
        return CustomerPrinter(val)

    # We can't handle this type.
    return None

Finally, hook up your printer lookup function to GDB’s machinery:

gdb.pretty_printers.append(get_printer)

In the following sections, we’ll make sure GDB runs this script when it starts up.

Project .gdbinit

In the root of your project, create a file named .gdbinit with contents source my-printer.py.

User .gdbinit

If you don’t already have one, create a file in your user’s home directory named .gdbinit. You can put GDB commands here (one per line) that will be executed whenever GDB starts up, such as set pagination off and set disassembly-flavor intel. At minimum, include the following:

set auto-load local-gdbinit on
set auto-load safe-path /home/geoff/projects/my-project

These two lines enable the loading of .gdbinit from your project’s directory. safe-path denotes a GDB’s whitelist for scripts. Replace /home/geoff/projects/my-project with the root of your project. Alternatively, you can use an ancestor of that directory (even just /), but doing so will correspondingly reduce security.

Troubleshooting

Once you’ve done all the above, no special settings should be necessary in CLion to make this work. Just start a new debugging session. Ensure CLion is using GDB and not LLDB or a different debugger.

  1. In Java, override Object.toString(). In C#, override Object.ToString(). This approach is not without downsides. It overloads the semantics of the primordial toString, a method that, in my opinion, ought not exist at all. But that’s a rant for another time. ↩︎


Posted

in

by

Tags: