This article is part 5 of the Natvis-like experience with GDB pretty printers series:
It is not easy to do things right inside GDB pretty printers, especially at the beginning. Here are some extra tools I used and some ways to looks into specific debuggers.
Python imports
GDB pretty printers can be loaded by executing source path/to/printers.py in GDB console (which is usually automated with .gdbinit).
It is possible to execute it many times, and each new execution overwrites the previous one.
However, it works fine only if you have all the pretty printers code in one file.
Otherwise Python's built-in import caching stands in the way: Python reuses old code behind imports by default.
I solved the problem by adding this hack in the main Python file:
# force reloading all our modules marked as "uncached"
# this allows us to refresh our pretty printers without restarting TDM
uncached_modules_names = []
for modname in sys.modules:
if modname.startswith('tdmuc_'):
uncached_modules_names.append(modname)
for modname in uncached_modules_names:
del sys.modules[modname]
All the files I want to hot-reload have their names prefixed with tdmuc_.
A somewhat related issue is how to do relative import of the files. In the end, I hacked it like this:
# GDB executes this file from god-knows-where
# I have no idea how to do a relative import in this case
basedir = os.path.dirname(__file__)
sys.path.append(basedir)
...
import tdmuc_idlib
With these tweaks, I can find GDB console in any IDE and manually execute source command again after editing the pretty printers, then probably do one step to force a refresh of all values in the GUI. As far as I remember, MSVC refreshes debugger visualization automatically every time you edit and save the natvis file using its built-in editor.
Debuggers hang
Initially I was developing the pretty printers in VS Code, and quickly noticed that sometimes gdb hangs completely when stopping at breakpoint. Here are several things I tried to debug the issue:
-
I implemented verbose logging of my Python code using sys.settrace, including all the calls, returns, and exceptions, parameters and return values. Everything properly indented to see the call structure.
-
I switched all the
childrenmethods of pretty printers from Python generators to returning lists. This makes Python traces much easier to understand. -
I used strace to intercept stdin/stdout of gdb process so that I can see which MI commands GDB receives (and which responses it returns). This is what helped me in the end.
VS Code
The first debugger to debug was VS Code, where I noticed reproducible hanging at a specific breakpoint. Looking at the stdin log, I saw the last MI command:
1027-stack-list-arguments 2 0 1
After that GDB is calling my pretty printers all the time and never stops.
According to MI docs, the first argument 2 here means --simple-values, which means "print the name, type and value for simple data types, and the name and type for arrays, structures and unions". A rather vague specification, I would say. The true meaning can be found in function mi_simple_type_p: non-simple types are arrays, structs, unions, and references to them, everything else is considered simple, including pointers. And since pointer is considered simple, its value is printed... and by default it means deep-printed, which typically hangs when pointers are expandable.
Here is the bug report.
One workaround which probably helps is to execute set print max-depth 0 in GDB, but now I get crashes instead of freezes.
CLion
This is the only IDE where I have not experienced GDB hanging yet. It looks like it has a correct implementation of pretty printers and can handle any loops in the children graph.
KDevelop
KDevelop has the same problem but with local variables instead of function parameters. Its last MI command before hanging is:
46-stack-list-locals --thread 1 --frame 0 --simple-values
Here is the bug report.
gdbgui
This one was a bit harder to analyze since it uses a different file to communicate with GDB (it was file descriptor 13 for me). It hangs on the same location with the following last words:
1-stack-list-variables --simple-values
Here is the bug report.
Code Blocks
Code::Blocks hangs much more easily that the first two IDEs. I don't even need to set a breakpoint at a special place: just expanding the "game" global variable causes gdb to hang. The problematic spot I used previously also works to reproduce the issue.
When I look at the stdin/stdout traces, I saw that Code::Blocks does not use GDB/MI (machine interface) intended for IDEs. Instead, it seems to use the normal interface that we usually use when we run GDB directly without IDE.
The last command I see in stdin is:
info args
This one should display the values of all function arguments, In the typical GDB tradition, "show" actually means deep-print. Code::Blocks calls info locals as well.
QtCreator
For some reason, I have not managed to enable GDB pretty printers in QtCreator. If I go to GDB console and enable them manually, they do work inside GDB console but the GUI watch still does not show anything useful.
Performance
Performance of value inspection can become a big problem in a debugger. Type matching is executed once per every value touched during debugger update. In the end, I replaced the regexes with custom trie + wildcard matching, and also added some early-out by type code in matching.
Unfortunately, IDEs have some unavoidable issues with performance.
Recall the idea that make_synthetic should accept a lambda? Expensive traversal of linked lists should not be executed unless the synthetic is expanded. Well, it turns out it was rather pointless, because CLion calls children method immediately when the value is displayed in IDE, even if it is not expanded. The call to children method is here:
def cidr_make_var(val, parent_cidr_var, exp, lang, register=True, raw=False):
...
# has_more
# compute only for dynamic vars, for non-dynamic vars clients should only use the numchild attribute
if dynamic and hasattr(children_val_pretty_printer, 'children'):
has_more = False
for child in children_val_pretty_printer.children():
has_more = True
break
result['has_more'] = has_more
...
GDB documentation advises returning a generator instead of an explicit list from children method. Which makes perfect sense, since CLion by default only displays the first 50 elements even if I expand the value. Indeed, if I have an idList with 65536 elements expanded (the entity array in the game is like that), CLion works much faster with generators than with explicit lists.
To fix this problem, I replaced explicit lists with generators in a few performance-critical functions:
array_children_list--- for large arrayslinked_list_children_list--- for linked listsRawSubclassPrinter.children--- raw members of large structs
I did not modify all the custom printers I already had: they still returns explicit lists, and use 'plus' operator and append for concatenation. To make it work, I wrapped the three optimized functions with wrap_children_generator decorator that converts a generator into custom container called LazyArray. This container can be built either from list or from iterable, supports concatenation, evaluates generators lazily with caching, implements the basic interface of list. The main benefit of using a decorator here is that I can switch generators off by changing a single flag: in that case the generators will be unrolled into explicit lists immediately.
Conclusion
So, after about two months of working on GDB pretty printers on my free weekends and about 1000 lines of code in the base library, I finally got it working the way I wanted! I would say I am satisfied with the result. Although I have not done much real debugging with it yet, since I have basically turned myself into a Python developer lately =) But we'll see in the future.
I have translated the majority of our natvis definitions into GDB. That's about 500 lines of code, covering 54 types out of 95 natvis definitions. Not all of the natvis definitions were ported, I skipped some declarations from the rarely visited corners of the codebase.
I am quite disappointed with the path I had to walk to get this result. I'm not happy with how much code is required on top of the GDB pretty printers to make them work well. This is too huge of an entry barrier! Putting such features into some popular support library could help, but until such a library exists pretty printers are going to be too weak compared to natvis.
Finally, GDB should use properly limited deep-printing by default instead of that hanging stuff!
Gallery
Some screenshots without pretty printers:
And with everything enabled: