Tips for writing LLDB pretty printers

LLDB supports custom scripts (“variable formatters”) to pretty print C++ data structures. For example, std::vector is typically implemented as a struct with three pointers: begin, end, and capacity. But if you wanted to print out a std::vector variable during a debugging session, printing out these three pointers isn’t likely to be helpful. What you actually want is to print the contents of the vector. Pretty printer scripts allow for doing this for your own data structures.1

Here are a few tips to supplement the official documentation. Sudara also has a great post on the topic.

Forwarding children of a member variable

Let’s say you have a data structure that internally wraps a std::vector.

struct MyVector
{
  std::vector<int> mData;
};

Printing this in lldb shows:

(MyVector) $0 = {
  mData = size=5 {
    [0] = 1
    [1] = 2
    [2] = 3
    [3] = 4
    [4] = 5
  }
}

However, you might want the data within mData to be directly inlined into MyVector. This reduces noise, but is also useful in GUI debugging environments like VS Code + vscode-lldb, where you need to click to unfold variables.

We can implement this custom inlined view with a “synthetic children” variable formatter. Synthetic children are the name for the “[0]”, etc members shown above. They are virtual members, computed by the formatter script.

The script will look like this:

class InliningSyntheticProvider:
    def __init__(self, valobj, internal_dict):
        self.valobj = valobj
    def update(self):
        self.wrapped_member = self.valobj.GetChildMemberWithName('mData')
        # Without this, the children of the wrapped member will not be synthetic
        # members. To properly forward certain types like std::vector, we need the
        # synthetic members -- otherwise, we will get the real private member
        # variables, which are not useful.
        self.wrapped_member.SetPreferSyntheticValue(True)
    def num_children(self):
        return self.wrapped_member.num_children
    def get_child_index(self,name):
        # tbh, I'm not sure about this.
        return 0
    def get_child_at_index(self,index):
        return self.wrapped_member.GetChildAtIndex(index)

It uses the .GetChildMemberWithName API to access lldb’s representation of the vector member, then delegates to that member for the num_children and get_child_at_index callbacks. See the documentation for more on this.

To register the printer, you’ll use an lldb command like this. Again, see the docs for more.

type synthetic add -w live -l InliningSyntheticProvider MyVector

Debugging formatting scripts

It can be very difficult to develop these scripts, as lldb by default swallows all Python exceptions raised and disables console output by default.

You’ll want to use:

lldb.formatters.Logger._lldb_formatters_debug_level = 2

And then in your script, you can do

logger = lldb.formatters.Logger.Logger()
logger.write(valobj)

See this stackoverflow post.

Writing printers for STL types

Let’s say you want to write a printer for a specific instantiation of std::pair<K, V> used in a std::map. When writing the regex, you need to be careful to include the __1 inline namespace used by libc++ — so it will look like std::__1::pair. See this comment in the lldb example scripts.

  1. LLDB implements build-in printers for the most common data structures in libc++ (but not GNU libstdc++). Interestingly, these are implemented in C++, rather than as Python scripts. These are quite interesting to read — they are essentially partial reimplementations of the C++ standard library since when writing printers, you are limited to accessing member variables (including private ones) and reading memory. Calling member functions is not easily done, as far as I understand. See the printer for std::map. In order to know how many synthetic children there are of a particular std::map instance, you can’t just call map.size(). If std::map had a private member containing the size you could simply read it, but now you’re at the point where you need to understand private implementation details of std::map.

Any thoughts?