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.
- In Java, override
Object.toString(). In C#, overrideObject.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. ↩︎
