reflect_dx: auto-generated debugger pretty-printers and docs
Library repo: github.com/wrocpp/reflect_dx · v0.1.0 · MIT. Two tools in one repo:
reflect-pretty(debugger visualisation) +reflect-docs(documentation).
Two universal C++ papercuts this post closes.
Papercut #1: debugger pretty-printers
The default LLDB / GDB / Visual Studio debugger view of a nested struct is a tree of braces:
(lldb) p user
(User) {
name = {
_M_local_buf = ...
_M_string_length = 5
}
age = 40
admin = true
home = {
city = { _M_local_buf = ..., _M_string_length = 6 }
postal_code = 12345
}
}
You can make this nicer by writing a pretty-printer: a .natvis XML blob for Visual Studio, a Python formatter for LLDB, a Python formatter for GDB. Every team I’ve worked with writes one for their critical types and then watches it go stale as the types evolve.
reflect-pretty emits the formatter at build time from your reflected types:
$ reflect-pretty include/models.hpp \
--out-natvis debug/models.natvis \
--out-lldb debug/models.lldb.py \
--out-gdb debug/models.gdb.py
Now:
(lldb) p user
(User) { name="Filip" age=40 admin=true home=Address{city="Warsaw" postal=12345} }
Change a struct, re-run the tool, the formatter follows.
Papercut #2: documentation
Doxygen is 30 years old and still the standard. Its output looks 30 years old too. Modern alternatives (Sphinx-cpp, Hugo-doc) mostly parse Doxygen XML under the hood.
The real problem: Doxygen doesn’t know about your annotations, the [[=json_name("id")]] that names the JSON wire field, the [[=gen::range(0, 150)]] that constrains property test inputs. Those are where the actual semantic information lives in a reflection-driven codebase, and Doxygen can’t see them.
reflect-docs walks your headers with reflection, extracts identifiers + annotations + comments, and emits modern markdown / HTML:
$ reflect-docs include/models.hpp --out docs/api/
Produces docs/api/user.md:
# `struct User`
Creates a new user account. (from `[[=description("...")]]`)
| Field | Type | Constraints | Description |
|---|---|---|---|
| `name` | `std::string` | — | Display name |
| `age` | `int` | `gen::range(0, 150)` | — |
| `email`| `std::string` | `gen::regex("...")` | — |
| `admin`| `bool` | — | — |
## JSON wire format
```json
{
"name":"filip",
"age":40,
"email":"filip@example.com",
"admin":false
}
(generated from reflect_json schema, shows cross-format renames if any)
Your `struct` declaration is your documentation source of truth.
## How the build step works
`reflect-pretty` and `reflect-docs` are thin binaries built on top of the clang-p2996 compiler frontend as a library. When you run `reflect-pretty models.hpp`, it:
1. Invokes clang-p2996 frontend to parse `models.hpp`.
2. Walks every top-level struct/class reflection (`namespace_members_of(^^your_namespace)`, a P3547-ish capability via namespace reflection).
3. For each type, generates an emitter snippet in the target format:
- natvis: XML with `<DisplayString>` + `<Expand>` blocks.
- LLDB: Python class subclassing `lldb.SBSyntheticValueProvider`.
- GDB: Python class subclassing `gdb.printing.PrettyPrinter`.
4. Writes the output file.
The generated Python/XML files are small, so you can check them in, review diffs, rerun in CI.
## Walking for pretty-printers (sketch)
```cpp
template <typename T>
consteval std::string natvis_snippet() {
std::string out;
out += "<Type Name=\"";
out += std::meta::display_string_of(^^T);
out += "\">\n <DisplayString>{" ;
constexpr auto ctx = std::meta::access_context::unchecked();
bool first = true;
template for (constexpr auto m
: std::define_static_array(
std::meta::nonstatic_data_members_of(^^T, ctx))) {
if (!first) out += " ";
first = false;
out += std::meta::identifier_of(m);
out += "={";
out += std::meta::identifier_of(m);
out += "}";
}
out += "}</DisplayString>\n <Expand>";
template for (constexpr auto m
: std::define_static_array(
std::meta::nonstatic_data_members_of(^^T, ctx))) {
out += "\n <Item Name=\"";
out += std::meta::identifier_of(m);
out += "\">";
out += std::meta::identifier_of(m);
out += "</Item>";
}
out += "\n </Expand>\n</Type>";
return out;
}
One function walks the fields, emits the visualisation. reflect-pretty composes this over every struct it finds in the header, producing a .natvis file.
Comparison
| Tool | Source of truth | Manual per-field writing | Stays in sync |
|---|---|---|---|
| Hand-written natvis | .natvis files | Yes | No |
| Visual Studio native | debug info | No | Partial (works for primitives) |
| LLDB / GDB pythons | .py files | Yes | No |
Rust #[derive(Debug)] |
The struct | No (auto) | Yes |
| reflect_dx | The struct | No (reflection) | Yes (re-run on change) |
Why both tools in one repo
reflect-pretty and reflect-docs share 80% of their plumbing: a frontend wrapper around clang-p2996 that exposes reflection to a host program as a C++ library. Splitting them would duplicate that infrastructure. Keeping them together lets teams opt into one or both without adding a second dependency.
Future sibling tool in the same repo: reflect-bindings, which emits Python / Lua / WASM bindings from a C++ header. Follow-up post.
What the library ships
Under github.com/wrocpp/reflect_dx:
reflect-prettyCLI, emits .natvis / .lldb.py / .gdb.py.reflect-docsCLI, emits markdown / HTML / JSON Schema.libreflect_dx_core.a, the shared clang-p2996-frontend wrapper.- CMake integration:
reflect_dx_generate()CMake function drops the outputs into your build tree automatically. - Sample
.natvis/ LLDB output for common STL types.
Next in the reflection arc
- Post 23: reflect_telemetry, compile-time metrics for production.
- Companion Monday short: “How LLDB summary providers actually work”.