Replace the custom toDebugGraph() method that generated JSON directly in C++ with a generic GDB Python script (debug_graph.py). The script traverses pointers and structures to build a graph representation for the VS Code Debug Visualizer, enabling external pointer display and more extensible visualization.
Update launch configurations to load the script and to disable array/string truncation (needed for the visualizer JSON). Add a debugAdapterConfigurations entry in settings.json to use the expression template $debug_graph(${expr}) for the cppdbg adapter.
293 lines
11 KiB
Python
293 lines
11 KiB
Python
"""Generic "pointer-memory structure" -> Debug Visualizer JSON walker.
|
|
|
|
Works on any struct/union reachable from an expression, purely by reading
|
|
DWARF debug info through gdb's Python API -- no changes to the debuggee's
|
|
source are required, and it works the same way for any language gdb (or
|
|
rust-gdb) understands.
|
|
|
|
Rule: every member of a struct/union becomes one row in that node's field
|
|
table. A member whose type is a pointer becomes an edge (and, if non-null,
|
|
is recursed into); everything else is just stringified in place. A visited-
|
|
address set makes this safe for cycles, so it handles lists, trees, and
|
|
arbitrary (possibly cyclic) graphs uniformly.
|
|
|
|
Loaded via the "source" gdb command (see .vscode/launch.json), this defines
|
|
a convenience function usable directly from the Debug Visualizer's Watch
|
|
box:
|
|
|
|
$debug_graph(list)
|
|
$debug_graph(list, "top", top) # extra name/pointer marker pairs
|
|
"""
|
|
|
|
import gdb
|
|
import json
|
|
|
|
# Marks the start of every payload this module returns (see json.dumps'
|
|
# default separators below): used to recognize our own values in
|
|
# _JsonResultPrettyPrinter without touching the user's own char* variables.
|
|
_RESULT_MARKER = '{"kind": {"nodeTable"'
|
|
|
|
|
|
def _addr_str(addr_int):
|
|
return hex(addr_int)
|
|
|
|
|
|
def _format_scalar(val):
|
|
try:
|
|
return str(val)
|
|
except gdb.error as e:
|
|
return "<error: %s>" % e
|
|
|
|
|
|
def _is_variant_like(t):
|
|
"""True for gdb's DWARF encoding of a Rust enum: a struct where one or
|
|
more discriminant fields are marked "artificial" alongside the real
|
|
variant payload fields. Deliberately loose (a C++ class with a vtable
|
|
pointer can also have an artificial field) -- callers must tolerate
|
|
_flatten_variant() finding no accessible variant and falling back
|
|
gracefully."""
|
|
if t.code != gdb.TYPE_CODE_STRUCT:
|
|
return False
|
|
try:
|
|
fields = t.fields()
|
|
except TypeError:
|
|
return False
|
|
has_artificial = any(getattr(f, "artificial", False) for f in fields)
|
|
has_payload = any(not getattr(f, "artificial", False) for f in fields)
|
|
return has_artificial and has_payload
|
|
|
|
|
|
def _flatten_variant(val, t, label):
|
|
"""For a live enum variant, yields (label, value) leaves to treat as if
|
|
they were direct fields of the enclosing struct -- unwrapping the
|
|
tuple-style __0 payload field so `next: Option<Box<Node<T>>>` reads
|
|
exactly like a plain `next` pointer field. Yields nothing for a unit
|
|
variant (e.g. None) or when no variant is accessible.
|
|
|
|
Which variant is live is determined by trying each non-artificial
|
|
field and seeing which one gdb accepts -- accessing an inactive Rust
|
|
enum variant raises gdb.error -- rather than parsing gdb's printed
|
|
text, since that text's format depends on the debugger's current
|
|
language setting."""
|
|
for f in t.fields():
|
|
if f.name is None or getattr(f, "artificial", False):
|
|
continue
|
|
try:
|
|
variant_val = val[f.name]
|
|
except gdb.error:
|
|
continue
|
|
variant_type = variant_val.type.strip_typedefs()
|
|
if variant_type.code not in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION):
|
|
return # unit variant (e.g. None): no payload to flatten
|
|
payload_fields = [pf for pf in variant_type.fields() if pf.name is not None]
|
|
for pf in payload_fields:
|
|
sub_label = (
|
|
label if (pf.name == "__0" and len(payload_fields) == 1) else "%s.%s" % (label, pf.name)
|
|
)
|
|
yield sub_label, variant_val[pf.name]
|
|
return
|
|
|
|
|
|
def _process_member(label, member_val, node_id, fields_out, edges, nodes, visited):
|
|
member_type = member_val.type.strip_typedefs()
|
|
|
|
if member_type.code == gdb.TYPE_CODE_PTR:
|
|
try:
|
|
ptr_int = int(member_val)
|
|
except gdb.error:
|
|
ptr_int = 0
|
|
fields_out.append({"name": label, "value": _addr_str(ptr_int), "isPointer": True})
|
|
if ptr_int:
|
|
edges.append((node_id, _addr_str(ptr_int), label))
|
|
_visit_pointer(member_val, ptr_int, nodes, edges, visited)
|
|
return
|
|
|
|
if _is_variant_like(member_type):
|
|
leaves = list(_flatten_variant(member_val, member_type, label))
|
|
if leaves:
|
|
for leaf_label, leaf_val in leaves:
|
|
_process_member(leaf_label, leaf_val, node_id, fields_out, edges, nodes, visited)
|
|
else:
|
|
# Unit variant (e.g. None) or an active variant we couldn't
|
|
# resolve -- fall back to gdb's own rendering of it.
|
|
fields_out.append(
|
|
{"name": label, "value": _format_scalar(member_val), "isPointer": False}
|
|
)
|
|
return
|
|
|
|
fields_out.append({"name": label, "value": _format_scalar(member_val), "isPointer": False})
|
|
|
|
|
|
def _describe_struct(val, node_id, nodes, edges, visited):
|
|
"""val: a gdb.Value of struct/union type (already dereferenced)."""
|
|
fields_out = []
|
|
t = val.type.strip_typedefs()
|
|
for f in t.fields():
|
|
if f.name is None or getattr(f, "is_base_class", False):
|
|
continue # anonymous member / base-class subobject
|
|
try:
|
|
member_val = val[f.name]
|
|
except gdb.error:
|
|
continue
|
|
_process_member(f.name, member_val, node_id, fields_out, edges, nodes, visited)
|
|
|
|
nodes[node_id] = fields_out
|
|
|
|
|
|
def _visit_pointer(ptr_val, ptr_int, nodes, edges, visited):
|
|
node_id = _addr_str(ptr_int)
|
|
if node_id in visited:
|
|
return
|
|
visited.add(node_id)
|
|
|
|
pointee_type = ptr_val.type.strip_typedefs().target().strip_typedefs()
|
|
pointee = ptr_val.dereference()
|
|
|
|
if pointee_type.code in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION):
|
|
_describe_struct(pointee, node_id, nodes, edges, visited)
|
|
else:
|
|
# Pointer to a scalar (e.g. int*): still worth a leaf node.
|
|
nodes[node_id] = [
|
|
{"name": "value", "value": _format_scalar(pointee), "isPointer": False}
|
|
]
|
|
|
|
|
|
def build_graph_json(root_val, roots):
|
|
nodes = {}
|
|
edges = []
|
|
visited = set()
|
|
|
|
t = root_val.type.strip_typedefs()
|
|
if t.code == gdb.TYPE_CODE_PTR:
|
|
ptr_int = int(root_val)
|
|
if ptr_int:
|
|
_visit_pointer(root_val, ptr_int, nodes, edges, visited)
|
|
elif t.code in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION):
|
|
node_id = _addr_str(int(root_val.address))
|
|
visited.add(node_id)
|
|
_describe_struct(root_val, node_id, nodes, edges, visited)
|
|
else:
|
|
nodes["root"] = [
|
|
{"name": "value", "value": _format_scalar(root_val), "isPointer": False}
|
|
]
|
|
|
|
nodes_json = [{"id": nid, "fields": fields} for nid, fields in nodes.items()]
|
|
edges_json = [
|
|
{"from": src, "to": dst, "label": label}
|
|
for (src, dst, label) in edges
|
|
if dst in nodes
|
|
]
|
|
roots_json = [{"name": name, "value": value} for (name, value) in roots]
|
|
|
|
return json.dumps(
|
|
{
|
|
"kind": {"nodeTable": True},
|
|
"nodes": nodes_json,
|
|
"edges": edges_json,
|
|
"roots": roots_json,
|
|
}
|
|
)
|
|
|
|
|
|
def _as_debugger_string(text):
|
|
"""Wrap `text` as a gdb.Value the Watch box will print as a proper
|
|
quoted string. Letting Python's str->gdb.Value auto-conversion do this
|
|
(i.e. just `return text`) produces a plain char array, which gdb's
|
|
*Rust* value printer renders as a bracketed list of byte codes instead
|
|
of text -- a display-time quirk based on the currently active
|
|
language, not something fixable by constructing the value differently.
|
|
Evaluating a string literal instead makes gdb's own (Rust- or
|
|
C++-aware, whichever is active) expression parser build a proper
|
|
string value, which prints correctly either way.
|
|
|
|
Under C++, VS Code's debug adapter (MIEngine) additionally gets this
|
|
value through gdb's MI *variable-object* protocol (`-var-create`), not
|
|
the plain CLI/`-data-evaluate-expression` path -- and for a raw
|
|
`char[N]` array, that protocol reports the "value" as just a length
|
|
placeholder like "[830]" with 830 one-character children, discarding
|
|
the actual text entirely. `std::string` doesn't have this problem
|
|
because libstdc++ registers a pretty-printer for it that overrides the
|
|
default array display with a plain string and no children -- the
|
|
_JsonResultPrettyPrinter registered below does the same thing for our
|
|
values specifically, without needing std::string to exist in the
|
|
debuggee (it wouldn't, since nothing in the target program uses it)."""
|
|
escaped = text.replace("\\", "\\\\").replace('"', '\\"')
|
|
return gdb.parse_and_eval('"%s"' % escaped)
|
|
|
|
|
|
class _JsonResultPrettyPrinter:
|
|
"""Makes gdb's MI variable-object protocol (what VS Code's C++ debug
|
|
adapter uses for Watch/evaluate) show our returned string in full, with
|
|
no children -- exactly what libstdc++'s std::string pretty-printer
|
|
does for MIEngine, which is why that approach worked when toDebugGraph
|
|
was a real std::string-returning C++ method."""
|
|
|
|
def __init__(self, val):
|
|
self._val = val
|
|
|
|
def to_string(self):
|
|
try:
|
|
return self._val.string()
|
|
except (gdb.error, UnicodeDecodeError):
|
|
return str(self._val)
|
|
|
|
def display_hint(self):
|
|
return "string"
|
|
|
|
|
|
def _pretty_print_lookup(val):
|
|
t = val.type.strip_typedefs()
|
|
if t.code == gdb.TYPE_CODE_PTR:
|
|
elem_type = t.target().strip_typedefs()
|
|
elif t.code == gdb.TYPE_CODE_ARRAY:
|
|
elem_type = t.target().strip_typedefs()
|
|
else:
|
|
return None
|
|
if elem_type.code not in (gdb.TYPE_CODE_INT, gdb.TYPE_CODE_CHAR) or elem_type.sizeof != 1:
|
|
return None # not a char-like element type
|
|
try:
|
|
prefix = val.string(length=len(_RESULT_MARKER))
|
|
except (gdb.error, UnicodeDecodeError):
|
|
return None
|
|
if prefix != _RESULT_MARKER:
|
|
return None
|
|
return _JsonResultPrettyPrinter(val)
|
|
|
|
|
|
gdb.pretty_printers.append(_pretty_print_lookup)
|
|
|
|
|
|
class DebugGraphFunction(gdb.Function):
|
|
"""$debug_graph(EXPR[, name, ptr]...) -- render EXPR's pointer-linked
|
|
structure as Debug Visualizer JSON. Extra name/pointer argument pairs
|
|
are drawn as external marker pointers into the graph."""
|
|
|
|
def __init__(self):
|
|
super(DebugGraphFunction, self).__init__("debug_graph")
|
|
|
|
def invoke(self, *args):
|
|
if len(args) == 0:
|
|
raise gdb.GdbError("usage: $debug_graph(expr, [name, ptr]...)")
|
|
|
|
root = args[0]
|
|
extra = args[1:]
|
|
roots = []
|
|
i = 0
|
|
while i + 1 < len(extra):
|
|
name_val, ptr_val = extra[i], extra[i + 1]
|
|
try:
|
|
name = name_val.string()
|
|
except (gdb.error, TypeError):
|
|
name = str(name_val)
|
|
try:
|
|
ptr_int = int(ptr_val)
|
|
except gdb.error:
|
|
ptr_int = 0
|
|
roots.append((name, _addr_str(ptr_int)))
|
|
i += 2
|
|
|
|
return _as_debugger_string(build_graph_json(root, roots))
|
|
|
|
|
|
DebugGraphFunction()
|