Native C++ data formatters for debugging Zig programs in LLDB. Shows slices, optionals, error unions, and standard library types in a readable format.
cd zdb
zig build
# Output: zig-out/lib/libzdb.dylibSet up the offset file for your LLDB version:
mkdir -p ~/.config/zdb/offsets
cp offsets/lldb-21.1.7.json ~/.config/zdb/offsets/Add to ~/.lldbinit:
plugin load /path/to/zdb/zig-out/lib/libzdb.dylib
Create demo.zig:
const std = @import("std");
const Color = enum { red, green, blue };
const Point = struct { x: i32, y: i32 };
const Shape = union(enum) {
circle: f32,
rectangle: struct { w: f32, h: f32 },
};
pub fn main() !void {
const greeting: []const u8 = "Hello, zdb!";
const numbers: []const i32 = &.{ 1, 2, 3, 4, 5 };
var maybe: ?i32 = 42;
var nothing: ?i32 = null;
var color: Color = .blue;
var point: Point = .{ .x = 100, .y = 200 };
var shape: Shape = .{ .circle = 3.14 };
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var list: std.ArrayListUnmanaged(i32) = .empty;
defer list.deinit(gpa.allocator());
try list.appendSlice(gpa.allocator(), &.{ 10, 20, 30 });
_ = .{ &greeting, &numbers, &maybe, ¬hing, &color, &point, &shape, &list };
std.debug.print("Ready\n", .{}); // breakpoint here
}Build and debug:
zig build-exe demo.zig -femit-bin=demo
lldb demo -o "plugin load zig-out/lib/libzdb.dylib" -o "b demo.zig:26" -o runActual LLDB output with zdb:
(lldb) frame variable greeting numbers maybe color point list
([]u8) greeting = "Hello, zdb!"
([]i32) numbers = len=5 ptr=0x1000da244
(test_types.Color) color = blue .blue
(demo.Point) point = { .x=100, .y=200 }
(array_list.Aligned(i32,null)) list = len=3 capacity=32
(lldb) p numbers[0]
(int) $0 = 1
(lldb) p maybe.?
(int) $1 = 42
| Pattern | Formatter | Example Output |
|---|---|---|
[]u8, []const u8 |
String | "Hello, World!" |
[]T |
Slice | len=5 ptr=0x100123 |
[N]T |
Array | [5]... |
?T |
Optional | null or 42 |
E!T |
Error Union | error.FileNotFound or value |
union(enum) |
Tagged Union | .circle = 5.0 |
*T |
Pointer | -> 42 or null |
[*]T |
Many Pointer | 0x100123456 |
[*:0]u8 |
C String | "null-terminated" |
[*:s]T |
Sentinel Pointer | 0x100123456 |
module.Type |
Struct/Enum | { .x=1, .y=2 } or .blue |
array_list.* |
ArrayList | len=3 capacity=32 |
hash_map.* |
HashMap | size=5 |
bounded_array.* |
BoundedArray | len=10 |
multi_array_list.* |
MultiArrayList | len=3 capacity=16 |
segmented_list.* |
SegmentedList | len=100 |
LLDB's internal C++ API (TypeCategoryImpl::AddTypeSummary) is not exported - symbols are marked local in liblldb.dylib. zdb bypasses this via offset tables:
┌─────────────────────────────────────────────────────────────┐
│ Plugin Load │
├─────────────────────────────────────────────────────────────┤
│ 1. Parse LLDB version from SBDebugger::GetVersionString() │
│ 2. Load offset JSON from ~/.config/zdb/offsets/ │
│ 3. dlopen liblldb, find reference symbol │
│ 4. Compute base address: ref_addr - ref_offset │
│ 5. Resolve internal functions: base + offset │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Formatter Registration │
├─────────────────────────────────────────────────────────────┤
│ For each type pattern: │
│ 1. SBTypeSummary::CreateWithCallback(callback_fn) │
│ 2. Extract shared_ptr from SBTypeSummary object │
│ 3. Call TypeCategoryImpl::AddTypeSummary via offset │
│ - ARM64 ABI: shared_ptr passed indirectly (pointer) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Runtime Callback │
├─────────────────────────────────────────────────────────────┤
│ When LLDB displays a Zig value: │
│ 1. Type name matches regex pattern │
│ 2. LLDB calls our C++ callback │
│ 3. Callback extracts fields via SBValue API │
│ 4. Writes formatted string to SBStream │
└─────────────────────────────────────────────────────────────┘
| File | Purpose |
|---|---|
shim/shim_callback.cpp |
Plugin entry, formatters, internal API calls |
shim/offset_loader.h |
JSON parsing, symbol resolution |
offsets/lldb-*.json |
Per-version offset tables |
tools/dump_offsets.py |
Generate offset tables for new LLDB versions |
The tricky part is calling TypeCategoryImpl::AddTypeSummary(StringRef, FormatterMatchType, shared_ptr<TypeSummaryImpl>):
thispointer in x0StringRef(ptr + len) in x1, x2FormatterMatchTypeenum in x3shared_ptrpassed indirectly in x4 (pointer to 16-byte struct)
Non-trivial types like shared_ptr (with destructor) are passed by pointer on ARM64, not inline in registers.
zdb requires an offset table matching your LLDB version. When LLDB updates, generate a new table:
# Check your LLDB version
lldb --version
# lldb version 21.1.7
# Generate offset table
python3 tools/dump_offsets.py /opt/homebrew/opt/llvm/lib/liblldb.dylib > offsets/lldb-21.1.7.json
# Install it
mkdir -p ~/.config/zdb/offsets
cp offsets/lldb-21.1.7.json ~/.config/zdb/offsets/The tool uses nm to find internal LLDB symbols and calculates their offsets relative to an exported reference symbol (SBDebugger::Initialize). At runtime, zdb:
- Finds the reference symbol via
dlsym - Computes base address:
ref_addr - ref_offset - Resolves internal functions:
base + symbol_offset
Common LLDB paths:
- macOS Homebrew:
/opt/homebrew/opt/llvm/lib/liblldb.dylib - macOS Xcode:
/Applications/Xcode.app/.../liblldb.dylib - Linux:
/usr/lib/liblldb.so
If dump_offsets.py shows warnings: Some symbols may not exist in your LLDB version. The core symbols (GetCategory, AddTypeSummary, Enable) are required.
# Run automated tests
./test/run_tests.sh
# Manual testing
lldb test/test_types \
-o "plugin load zig-out/lib/libzdb.dylib" \
-o "b test_types.zig:157" \
-o "run" \
-o "frame variable"Tests verify: string slices, int slices, enums, structs, ArrayList, HashMap.
zdb transparently extends the p command to support native Zig syntax:
(lldb) p int_slice[0]
(int) $0 = 1
(lldb) p int_slice[2]
(int) $1 = 3
(lldb) p list[0]
(int) $2 = 10
(lldb) p test_struct.optional_value.?
(int) $3 = 42
(lldb) p test_struct.error_result catch 0
(int) $4 = 100
(lldb) p string_slice
([]u8) $5 = "Hello, zdb debugger!"
Supported Zig syntax:
| Syntax | Transformation | Example |
|---|---|---|
slice[n] |
slice.ptr[n] |
p my_slice[0] |
arraylist[n] |
arraylist.items.ptr[n] |
p list[0] |
optional.? |
optional.data |
p maybe_value.? |
err catch default |
(err.tag == 0 ? err.value : default) |
p result catch 0 |
All transformations are automatic and transparent - just use p as usual.
zdb works with both Apple LLDB (Xcode) and Homebrew LLDB, with some differences:
| Feature | Homebrew LLDB | Apple LLDB (Xcode) |
|---|---|---|
| Type formatters | ✓ frame variable |
✓ frame variable |
p slice[n] |
✓ via p |
✓ via zp |
p optional.? |
✓ via p |
✓ via zp |
p err catch val |
✓ via p |
✗ |
Homebrew LLDB (recommended): Full support. The p command is transparently enhanced with Zig syntax.
Apple LLDB: Type formatters work fully. Expression syntax is available via the zp command (uses regex-based transformation). The p command cannot be overridden due to C++ ABI incompatibility between plugins and Apple's internal LLDB build.
# Apple LLDB usage
(lldb) frame variable my_slice # Formatters work
(lldb) zp my_slice[0] # Zig expression syntax
(lldb) zp maybe_value.? # Optional unwrap| Feature | zdb | zig-lldb |
|---|---|---|
| Installation | Plugin, no rebuild | Rebuild LLDB from source |
| Type formatting | ✓ | ✓ |
p slice[n] |
✓ | ✓ |
p optional.? |
✓ | ✓ |
p err catch val |
✓ | ✓ |
| Variables view children | ✗ | ✓ |
| Full TypeSystem | ✗ | ✓ |
| Works with stock LLDB | ✓ | ✗ |
zdb provides type formatters and Zig expression syntax via automatic transformation. No LLDB rebuild required.
zig-lldb (Jacob Shtoyer's fork) implements a full TypeSystemZig with deep DWARF integration. Requires ~1hr to rebuild LLDB from source.
Variables View Expansion: IDE variables view cannot expand slices to show [0], [1], ... elements. This requires LLDB synthetic children providers, which cannot be registered from a plugin due to C++ ABI barriers (std::function type erasure incompatibility between plugin and LLDB builds).
Workaround: Use the p command with Zig syntax:
(lldb) p my_slice[0] # Works!
(lldb) p my_slice[5] # Works!
This limitation does not affect zig-lldb, which implements a full TypeSystem with native DWARF integration.
MIT