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.