name: debug-pretty-printers description: Debug and develop GDB and LLDB pretty printers for ezEngine C++ types. Use this skill when working with ezEngine debugger visualizers in ezEngine-gdb.py, ezEngine.py, or ezEngine.natvis. Covers testing with VisualizerZoo.cpp, common errors, and debugging techniques.
Debugging ezEngine Pretty Printers
This skill provides guidance for developing and debugging pretty printers (debugger visualizers) for ezEngine.
File Locations
All pretty printer files are in Code/Engine/Foundation/:
- GDB:
Code/Engine/Foundation/ezEngine-gdb.py- Python pretty printers for GDB - LLDB:
Code/Engine/Foundation/ezEngine.py- Python pretty printers for LLDB - Natvis:
Code/Engine/Foundation/ezEngine.natvis- Visual Studio/MSVC visualizers (XML) - Auto-load:
.gdbinitin repository root - Automatically loads GDB printers
Testing with VisualizerZoo.cpp
The primary test file for pretty printers is Code/UnitTests/FoundationTest/CodeUtils/VisualizerZoo.cpp. This file contains test variables for all visualized types organized in EZ_TEST_BLOCK sections.
Finding test sections
Search for EZ_TEST_BLOCK macros to find each section:
grep -n "EZ_TEST_BLOCK" Code/UnitTests/FoundationTest/CodeUtils/VisualizerZoo.cpp
Eac hsection is named descriptively, e.g. EZ_TEST_BLOCK(ezTestBlock::Enabled, "Strings") contains tests for the string types.
Setting breakpoints
Each EZ_TEST_BLOCK section ends with EZ_TEST_BOOL(true); - this is where you should set your breakpoint. At this point, all variables in the block are initialized and in scope.
To find the breakpoint line for a section:
# Find the section start, then look for the EZ_TEST_BOOL(true) before the closing brace
grep -n "EZ_TEST_BLOCK.*Strings" Code/UnitTests/FoundationTest/CodeUtils/VisualizerZoo.cpp
# Then search forward from that line for EZ_TEST_BOOL(true)
Or in an editor, search for EZ_TEST_BLOCK.*SectionName, then find the EZ_TEST_BOOL(true); at the end of that block.
Running the test
First, ensure FoundationTest is built:
cmake --build Workspace/copilot --target FoundationTest
Testing GDB printers in batch mode
Use this exact command pattern to test printers (run from the repository root):
gdb -batch \
-ex "set pagination off" \
-ex "source Code/Engine/Foundation/ezEngine-gdb.py" \
-ex "break VisualizerZoo.cpp:LINE_NUMBER" \
-ex "run -run -noGui -all" \
-ex "print variableName" \
./Workspace/copilot-output/Bin/LinuxNinjaGccDebug64/FoundationTest
Replace LINE_NUMBER with the line of EZ_TEST_BOOL(true); in the relevant section.
Key points:
- Use
timeout 30prefix if the test might hang - The breakpoint must be on the
EZ_TEST_BOOL(true);line at the END of the block (all variables initialized) - Add
2>&1 | tail -15to limit output
Example: Testing string printers
Find the Strings section:
grep -n "EZ_TEST_BLOCK.*Strings" Code/UnitTests/FoundationTest/CodeUtils/VisualizerZoo.cppFind the
EZ_TEST_BOOL(true);at the end of that block and note the line numberRun GDB with that breakpoint (from repository root):
gdb -batch \ -ex "set pagination off" \ -ex "source Code/Engine/Foundation/ezEngine-gdb.py" \ -ex "break VisualizerZoo.cpp:101" \ -ex "run -run -noGui -all" \ -ex "print string" \ -ex "print stringView" \ ./Workspace/copilot-output/Bin/LinuxNinjaGccDebug64/FoundationTest
Viewing expanded children
Use set print array on to see children on separate lines:
gdb -batch \
-ex "set pagination off" \
-ex "set print array on" \
-ex "source Code/Engine/Foundation/ezEngine-gdb.py" \
-ex "break VisualizerZoo.cpp:LINE_NUMBER" \
-ex "run -run -noGui -all" \
-ex "print variableName" \
./Workspace/copilot-output/Bin/LinuxNinjaGccDebug64/FoundationTest
ezEngine GDB Printer Structure
Registration pattern (from ezEngine-gdb.py)
def build_pretty_printers():
pp = gdb.printing.RegexpCollectionPrettyPrinter("ezEngine")
# Strings
pp.add_printer('ezHybridStringBase', r'^ezHybridStringBase<.*>$', ezHybridStringPrinter)
pp.add_printer('ezStringBuilder', r'^ezStringBuilder$', ezHybridStringPrinter)
# Containers
pp.add_printer('ezDynamicArray', r'^ezDynamicArray<.*>$', ezDynamicArrayPrinter)
return pp
gdb.printing.register_pretty_printer(gdb.current_objfile(), build_pretty_printers())
Printer class pattern
class ezMyTypePrinter:
def __init__(self, val):
self.val = val
def to_string(self):
"""Return the display string for the value."""
try:
return f"{{ count={count} }}"
except Exception as e:
return f"<error: {e}>"
def children(self):
"""Yield (name, value) tuples for expandable children."""
try:
yield 'm_uiCount', self.val['m_uiCount']
yield 'm_pAllocator', self.val['m_pAllocator']
for i in range(count):
yield f'[{i}]', (m_pElements + i).dereference()
except Exception as e:
yield 'error', str(e)
def display_hint(self):
return 'array' # or 'string' or 'map'
Common Errors and Solutions
"Type is not a template"
Problem: Calling val.type.template_argument(0) on ezStringBuilder (not a template).
Solution: Get size from the actual member instead:
# Instead of: local_storage_size = val.type.template_argument(0)
local_storage_size = m_Data['m_StaticData'].type.sizeof
"Trying to read string with inappropriate type"
Problem: Calling .string() on array address instead of char*.
Solution: Cast to char* first:
char_ptr_type = gdb.lookup_type('char').pointer()
ptr = m_Data['m_StaticData'].address.cast(char_ptr_type)
result = ptr.string(length=count)
"There is no member named X"
Problem: Wrong member name. Check the LLDB or Natvis implementation for correct names.
Solution: Use GDB to inspect the actual type:
gdb -batch -ex "ptype ezHashedString" ./Output/Bin/LinuxNinjaGccDebug64/FoundationTest
Children showing as strings instead of expandable values
Problem: Yielding formatted strings in children() instead of GDB values.
Solution: Yield actual GDB values that have their own printers:
# Wrong - yields a string, not expandable
yield 'c0', f"{{ x={x}, y={y}, z={z} }}"
# Correct - yields the actual value, GDB will use ezVec3Printer on it
yield 'c0', elements[0].address.cast(vec3_type.pointer()).dereference()
ezHybridString inline vs heap storage
The ezHybridStringBase uses inline storage (m_StaticData) when capacity fits, otherwise heap (m_pElements):
def _get_string_data(self):
m_Data = self.val['m_Data']
m_uiCount = int(m_Data['m_uiCount'])
m_uiCapacity = int(m_Data['m_uiCapacity'])
local_storage_size = m_Data['m_StaticData'].type.sizeof
if m_uiCapacity <= local_storage_size:
# Using inline storage
char_ptr_type = gdb.lookup_type('char').pointer()
ptr = m_Data['m_StaticData'].address.cast(char_ptr_type)
else:
# Using heap storage
ptr = m_Data['m_pElements']
return ptr, m_uiCount
Template type lookup for math types
To cast array elements to vector types (e.g., for ezMat3 columns):
elements = self.val['m_fElementsCM']
elem_type = elements[0].type
vec3_type = gdb.lookup_type(f'ezVec3Template<{elem_type}>')
col0 = elements[0].address.cast(vec3_type.pointer()).dereference()
LLDB Comparison
When implementing GDB printers, reference the LLDB implementation in ezEngine.py:
- LLDB uses
set_fields(['m_uiCount', 'm_uiCapacity', 'm_pAllocator'])to add member children - GDB equivalent: yield each field in
children()before array elements - LLDB
GetChildMemberWithName('m_Data')→ GDBself.val['m_Data'] - LLDB
GetValueAsUnsigned(0)→ GDBint(self.val['m_Member'])
Debugging Workflow
- Identify the type: Check which printer handles it in
build_pretty_printers() - Find test variable: Look in VisualizerZoo.cpp for a variable of that type
- Set breakpoint after initialization: Use the table above for correct line numbers
- Test to_string() first: Get the display string working before children()
- Add children() incrementally: Test each child field separately
- Compare with LLDB/Natvis: Use existing implementations as reference for member names
Auto-loading
The .gdbinit file in the repository root automatically loads the printers when debugging in the ezEngine directory. It uses path detection relative to the current working directory.