If you use a LLDB based toolchain (for example, working with C++ in Xcode or CLion), you can easily customize how your IDE displays variables while debugging.
The variable formatting docs are a bit dense, so here’s a friendly getting started guide.
What are Type Summaries?
Summaries show when you hover over a variable in your IDE while debugging:
It’s also what displays when you po
(print object) on the LLDB console:
You can teach LLDB to provide any arbitrary string as the summary:
For your custom types (which won’t have defined summaries) Xcode won’t just show anything at all on hover (which feels like bad UX). CLion will just show the type name (which is nicer):
What Are Type Children?
When inspecting a type, you can click the +
(CLion) or disclosure triangle (Xcode) to see “children” of the type
Children are members of the the current type (or “items” of an array type) and their summaries. This lets you drill down while debugging.
Like type summaries, type children are customizable. You can teach LLDB how to create “synthetic” children for your custom type.
This lets you display and organize the information you care about displaying. Especially with convoluted modern C++, a type might have a lot of nesting or internal complexity that gets in the way when debugging.
Customizing Summaries and Children with Python
On startup, your IDE will look for a file named ~/.lldbinit
and parse and run it.
Here’s the current contents of my ~/.lldbinit
:
command script import /Users/sudara/projects/librariesAndExamples/juce-toys/juce_lldb_xcode.py
command script import /Users/sudara/projects/melatonin_audio_sparklines/sparklines.py
It just imports two other python files (where the code that does stuff lives).
You can also manually type these import
commands on the lldb console:
This way you can keep editing and reimporting the file in the debugger, even while it sits on a breakpoint! This makes iterating and developing summaries very easy.
(Tip: make sure you don’t paste in the the filename with extra whitespace characters after it or you’ll see an invalid pathname
error.)
Python file
So what do these imported python files do?
Firstly, they define a __lbdb_init_module
function which gets called on script import. Usually this will have a few HandleCommand
calls that tell LLDB which summaries and synthetics to add.
The string you pass to HandleCommand
will get run — just as if you had pasted it into the lldb console.
Note that print
statements get output to the lldb console.
import sys
import lldb
def __lldb_init_module(debugger, dict):
print("loading melatonin audio sparklines for JUCE AudioBlocks...")
debugger.HandleCommand('type synthetic add -x "juce::dsp::AudioBlock<" --python-class sparklines.AudioBlockChannelsProvider -w juce')
debugger.HandleCommand('type summary add -x "juce::dsp::AudioBlock<" -F sparklines.audio_block_summary -w juce')
Customizing Summaries
Summary strings are the nice and easy way to format types. If you just want to display some text or some values of members, --summary-string
is the way to go.
Just specify the format you want directly in the HandleCommand
call:
debugger.HandleCommand('type summary add juce::String --summary-string "${var.text.data}" -w juce')
In a summary, var
always refers to the variable the summary is being created for.
You can drill down and access the summaries for public or private members like I do above (text
and data
are both private members of this type) as well as do a bit of formatting.
The -w
assigns the summary to a category.
Categories can then be enabled and disabled like so: type category enable juce
.
via python function
If you need to do some fancier formatting, specify a python file and function with the -F
option, like so:
debugger.HandleCommand('type summary add juce::ValueTree -F juce_lldb_xcode.value_tree_summary -w juce')
Python API
The python API is somewhat tersely documented, so code examples are your friend.
To start with: SBValue
is the python representation of a variable in your code.
This is what gets passed into to your summary function. In code examples, it’s passed in with the name valueObject
.
Here are some tips on SBValue
‘s methods:
GetChildMemberWithName()
This digs down through members of your variable, handing you back the SBValue
representation of that child.
Private members too! This is important, as it’s hard (possible, but ugly) to call functions in your code on/with an SBValue
. So private members are your new best friend.
Warning: if you are provided synthetic children for a type (more on that later), you replaced the “natural” child members with your synthetics. That means the members of the type are removed and you can only drill down into the child members you manually defined! More on that later.
GetValue() returns a string
GetValue()
returns a string representation of your variable.
You can use GetValueAsSigned()
and GetValueAsUnsigned()
for integer representations.
GetSummary() grabs type summaries
LLDB provides built-in summaries for some basic types, such as strings.
Using GetSummary()
in your type summary is useful for things like containers or when you want your children to have nice summaries too.
An example I have for the the JUCE framework:
A juce::String
summary is used in the juce::var
summary which is used in the juce::NamedValueSet::NamedValue
summary which is used in the juce::NamedValueSet
summary, which is used in the juce::ValueTree
summary!
Synthetic Children
This lets you replace the children (members) that lldb shows with custom (synthetic) children.
Basically, you need to craft a class with specific methods, as seen in the docs.
Note: The __init__
method of the synthetic provider is called when you hover over a type in your IDE and the a type summary is about to be displayed. This means: For summaries, “natural” children are already replaced by your new “synthetic” children. If you want to refer to an original child member in a python function, store it manually in __init__
, or use GetNonSyntheticValue()
.
get_child_index
Note that LLDB uses the brackets operator for array type objects. This is why you’ll see a lot of examples that uses a pattern that strip brackets off like so:
def get_child_index(self, name):
return int(name.lstrip('[').rstrip(']'))
CreateChildAtOffset
Likewise, array examples often use CreateChildAtOffset
in the get_child_at_index
method and manually create children with string names like [2]
.
def get_child_at_index(self, index):
offset = index * self.data_size
return self.first_element.CreateChildAtOffset('[' + str(index) + ']', offset, self.data_type)
Note that we’re relying on self.data_size
, self.data_type
and self.first_element
in this particular array example. Those are specific to this example good place to set things like that would be the update
method:
def update(self):
# dig down and find the pointer to the start of the data
self.first_element = self.valueObject.GetChildMemberWithName('values').GetChildMemberWithName('elements').GetChildMemberWithName('data')
# type of first template argument. For example juce::Array<int> would be int
self.data_type = self.valueObject.GetType().GetTemplateArgumentType(0)
self.data_size = self.data_type.GetByteSize()
CreateValueFromData
This is a higher octane way to create children, letting you construct them from data you have in python (like a string you manually constructed).
Examples and other resources
My audio sparklines example (displaying real time audio in your IDE!) might interest you.
There are some examples in the lldb repo and some formatters for the JUCE C++ framework I’ve contributed to.
Mark Mossberg (Ableton) wrote some tips about lldb formatters.
Let me know on twitter if you have a go at it — it’ll help justify the time I spent digging in!
Leave a Reply