From 49c95dbe7273aae6d99bf0a472389a4fc5d113b9 Mon Sep 17 00:00:00 2001 From: Andrey Bychkov Date: Thu, 27 Aug 2026 18:03:25 +0300 Subject: [PATCH] add Qt Creator dumper for PIP types and PINetworkAddress GDB printer --- .gitignore | 1 + doc/pages/debugging.md | 23 +++ tools/gdb/pip_pp.py | 22 ++- tools/gdb/test/demo_pp.cpp | 5 + tools/gdb/test/test_pp.sh | 4 + tools/qtcreator/pip_types.py | 286 +++++++++++++++++++++++++++++++++++ 6 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 tools/qtcreator/pip_types.py diff --git a/.gitignore b/.gitignore index c420a4bb..88a0819e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ CMakeLists.txt.user* /build* /AGENTS.md /plans +__pycache__ diff --git a/doc/pages/debugging.md b/doc/pages/debugging.md index 64c63c14..d8bedfb6 100644 --- a/doc/pages/debugging.md +++ b/doc/pages/debugging.md @@ -17,6 +17,7 @@ PIP ships a GDB pretty-printer script, `tools/gdb/pip_pp.py`, that makes the bas | \a PISet | `{3, 5}` | | \a PIPair | `{"k", 7}` | | \a PIVariant | `PIVariant(pivInt, 4 bytes)` | +| \a PINetworkAddress | `192.168.1.10:8080` | Nested containers are printed recursively, e.g. `PIVector` prints as `{"a", "bb"}`. Pointers to the covered types show the address and the content: @@ -62,6 +63,27 @@ add-auto-load-safe-path The stub is installed only for shared builds; for static builds use the manual `source` command. +\section qtcreator Qt Creator + +Qt Creator's Variables/Expressions views do not use GDB +pretty-printers; they run their own Python dumper. PIP provides the +dumper module `tools/qtcreator/pip_types.py`, which covers the same +types as `pip_pp.py`. To enable it: + +1. Open the debugger settings (Qt Creator -> Settings -> Debugger). +2. Switch to the "Variables and expressions" tab. +3. In the "Extra dumper file" field (tooltip: "Path to a Python file + containing additional data dumpers"), enter the path to the file: + +\code +/path/to/pip/tools/qtcreator/pip_types.py +\endcode + +After that PIString, PIVector, PIMap and the other types from +the table above show their content in the Variables/Expressions views. +No changes to the GDB setup (`.gdbinit`, auto-load stub) are needed for +Qt Creator; those keep working for the command-line GDB. + \section verifying Verifying the installation A check script builds a demo with all covered types and asserts the expected @@ -88,6 +110,7 @@ tools/gdb/test/test_pp.sh build_linux | \a PISet | `{3, 5}` | | \a PIPair | `{"k", 7}` | | \a PIVariant | `PIVariant(pivInt, 4 bytes)` | +| \a PINetworkAddress | `192.168.1.10:8080` | Вложенные контейнеры выводятся рекурсивно, например `PIVector` выводится как `{"a", "bb"}`. Указатели на поддерживаемые типы показывают diff --git a/tools/gdb/pip_pp.py b/tools/gdb/pip_pp.py index e8a0a735..3270a8fc 100644 --- a/tools/gdb/pip_pp.py +++ b/tools/gdb/pip_pp.py @@ -3,7 +3,8 @@ GDB pretty-printers for the PIP (Platform-Independent Primitives) library. Covered types: PIString, PIByteArray, PIChar, PIVector, PIDeque, - PIStringList, PIMap, PISet, PIPair, PIVariant + PIStringList, PIMap, PISet, PIPair, PIVariant, + PINetworkAddress Usage ----- @@ -248,6 +249,24 @@ class _PIPairPrinter(_Printer): return None +class _PINetworkAddressPrinter(_Printer): + """PINetworkAddress: union {uint ip_; uchar ip_b[4]}; ushort port_. + + The IP bytes are stored in ip_b[0..3] in octet order, so they are + printed as-is (no byte-swap, works for any endianness). + Printed as "i.i.i.i:p". + """ + + def to_string(self): + try: + # Mask against a signed char type: octets must be 0..255. + b = [int(self._val["ip_b"][i]) & 0xFF for i in range(4)] + port = int(self._val["port_"]) + return self._prefix + "%d.%d.%d.%d:%d" % (b[0], b[1], b[2], b[3], port) + except Exception: + return None + + class _PIVariantPrinter(_Printer): """PIVariant: PIByteArray _content (serialized), Type _type (enum). @@ -282,6 +301,7 @@ _PATTERNS = [ (re.compile(r"^PISet<.*>$"), lambda v, p: _PISetPrinter(v, p)), (re.compile(r"^PIPair<.*>$"), lambda v, p: _PIPairPrinter(v, p)), (re.compile(r"^PIVariant$"), lambda v, p: _PIVariantPrinter(v, p)), + (re.compile(r"^PINetworkAddress$"), lambda v, p: _PINetworkAddressPrinter(v, p)), ] diff --git a/tools/gdb/test/demo_pp.cpp b/tools/gdb/test/demo_pp.cpp index 4ea4383e..48ca1bcc 100644 --- a/tools/gdb/test/demo_pp.cpp +++ b/tools/gdb/test/demo_pp.cpp @@ -3,6 +3,7 @@ #include "pibytearray.h" #include "pichar.h" #include "pimap.h" +#include "pinetworkaddress.h" #include "pipair.h" #include "piset.h" #include "pistring.h" @@ -28,6 +29,8 @@ static PIString * ptr_str = 0; static PIString * ptr_null = 0; static PIChar chr_a; static PIChar chr_cyr; +static PINetworkAddress addr; +static PINetworkAddress addr_null; namespace { struct PPInit { @@ -58,6 +61,8 @@ struct PPInit { chr_a = PIChar('a'); chr_cyr = PIChar((char16_t)0x043C); + addr = PINetworkAddress(PIString("192.168.1.10"), 8080); + str_esc = "a\"b\\c\nd\te"; } }; diff --git a/tools/gdb/test/test_pp.sh b/tools/gdb/test/test_pp.sh index c207d7cc..a5018d0f 100755 --- a/tools/gdb/test/test_pp.sh +++ b/tools/gdb/test/test_pp.sh @@ -68,6 +68,8 @@ OUT="$(gdb -batch -nx \ -ex "print pair_str_int" \ -ex "print var_int" \ -ex "print var_invalid" \ + -ex "print addr" \ + -ex "print addr_null" \ -ex "print ptr_str" \ -ex "print *ptr_str" \ "${WORK}/demo_pp" 2>&1)" @@ -102,6 +104,8 @@ check '^\$[0-9]+ = \{"one", "two"\}$' "PIStringList" check '^\$[0-9]+ = \{"k", 7\}$' "PIPair" check '^\$[0-9]+ = PIVariant\(pivInt, 4 bytes\)$' "PIVariant typed" check '^\$[0-9]+ = PIVariant\(pivInvalid, 0 bytes\)$' "PIVariant invalid" +check '^\$[0-9]+ = 192\.168\.1\.10:8080$' "PINetworkAddress" +check '^\$[0-9]+ = 0\.0\.0\.0:0$' "PINetworkAddress null" check '^\$[0-9]+ = \(PIString \*\) 0x[0-9a-f]+ "hello"$' "pointer to PIString" check '^\$[0-9]+ = "hello"$' "dereferenced pointer" diff --git a/tools/qtcreator/pip_types.py b/tools/qtcreator/pip_types.py new file mode 100644 index 00000000..b9ffe49d --- /dev/null +++ b/tools/qtcreator/pip_types.py @@ -0,0 +1,286 @@ +# Data dumpers for the PIP (Platform-Independent Primitives) library. +# +# Makes the Qt Creator Variables/Expressions views show the content of the +# basic PIP types instead of their raw internals (the Qt Creator dumper +# does not use GDB pretty-printers, it needs its own dumpers; this module +# mirrors tools/gdb/pip_pp.py): +# +# PIString -> hello +# PIByteArray -> {0x48, 0x65, 0x6c, 0x6c, 0x6f} +# PIChar -> 'a' or U+043m +# PIVector -> [1, 2, 3] +# PIDeque -> [1, 2, 3] +# PIStringList -> ["one", "two"] +# PIMap -> {k: v} +# PISet -> [3, 5] +# PIPair -> (k, v) +# PIVariant -> PIVariant(pivInt, 4 bytes) +# PINetworkAddress -> 192.168.1.10:8080 +# +# Enable: Qt Creator -> Settings -> Debugger -> "Variables and expressions" +# tab -> "Extra dumper file" -> point to this file. +# +# Requirements: the executable and the PIP library are built with debug +# information (-g). All dumpers are read-only and defensive: on any error +# they fall back to the default (raw) member expansion. + +from dumper import Children, SubItem + +# Maximum number of elements printed per container (matches pip_pp.py). +_MAX_ELEMENTS = 32 +# Absolute safety cap: never try to read more elements than this +# (protects against corrupted size fields). +_MAX_READ = 10000000 +# Maximum string/bytearray elements read for the display value. +_MAX_DISPLAY_UNITS = 4096 + + +def _utf16Text(units): + """Build a plain text string from a list of UTF-16 code units.""" + out = [] + i = 0 + n = len(units) + while i < n: + u = units[i] + # Combine surrogate pairs. + if 0xD800 <= u <= 0xDBFF and i + 1 < n and 0xDC00 <= units[i + 1] <= 0xDFFF: + u = 0x10000 + ((u - 0xD800) << 10) + (units[i + 1] - 0xDC00) + i += 2 + else: + i += 1 + if 0 <= u <= 0x10FFFF: + out.append(chr(u)) + else: + out.append("\ufffd") + return "".join(out) + + +def _dequeUnits(dvalue): + """Read UTF-16 units from a PIDeque value, or None on error.""" + size = int(dvalue['pid_size']) + if size < 0 or size > _MAX_READ: + return None + if size == 0: + return [] + data = dvalue['pid_data'] + start = int(dvalue['pid_start']) + if start < 0: + return None + return [int(data[start + i]['ch']) for i in range(min(size, _MAX_DISPLAY_UNITS))] + + +def qdump__PIString(d, value): + """PIString: PIDeque d + cache members. Printed as plain text.""" + try: + size = int(value['d']['pid_size']) + units = _dequeUnits(value['d']) + if units is not None: + text = _utf16Text(units) + if size > _MAX_DISPLAY_UNITS: + text += "..." + # Hex-encode with the utf8 tag so the raw text (quotes, + # backslashes, control chars) never appears unescaped in the + # dumper protocol stream. + d.putValue(d.hexencode(text), "utf8", length=len(text)) + except Exception: + pass + d.putPlainChildren(value) + + +def qdump__PIByteArray(d, value): + """PIByteArray: PIDeque d. Printed as a hex byte list.""" + try: + size = int(value['d']['pid_size']) + if 0 <= size <= _MAX_READ: + if size == 0: + d.putValue('{}') + else: + dvalue = value['d'] + data = dvalue['pid_data'] + start = int(dvalue['pid_start']) + lim = min(size, _MAX_ELEMENTS) + items = ['0x%02x' % int(data[start + i]) for i in range(lim)] + tail = '' if size <= _MAX_ELEMENTS else ', ... (%d total)' % size + d.putValue('{%s%s}' % (', '.join(items), tail)) + except Exception: + pass + d.putPlainChildren(value) + + +def qdump__PIChar(d, value): + """PIChar: single UTF-16 code unit (ushort ch).""" + try: + ch = int(value['ch']) + if 0x20 <= ch <= 0x7E and ch not in (0x22, 0x5C): + d.putValue("'%s'" % chr(ch)) + else: + d.putValue('U+%04x' % ch) + except Exception: + pass + d.putPlainChildren(value) + + +def qdump__PINetworkAddress(d, value): + """PINetworkAddress: union {uint ip_; uchar ip_b[4]}; ushort port_. + + The IP bytes are stored in ip_b[0..3] in octet order, so they are + printed as-is (no byte-swap, works for any endianness). + Printed as "i.i.i.i:p". + """ + try: + try: + # Mask against a signed char type: octets must be 0..255. + b = [int(value['ip_b'][i]) & 0xFF for i in range(4)] + except Exception: + ip = int(value['ip_']) + if d.byteorder == 'big': + b = [(ip >> 24) & 0xFF, (ip >> 16) & 0xFF, + (ip >> 8) & 0xFF, ip & 0xFF] + else: + b = [ip & 0xFF, (ip >> 8) & 0xFF, + (ip >> 16) & 0xFF, (ip >> 24) & 0xFF] + port = int(value['port_']) + d.putValue('%d.%d.%d.%d:%d' % (b[0], b[1], b[2], b[3], port)) + except Exception: + pass + d.putPlainChildren(value) + + +def qdump__PIVector(d, value): + """PIVector: T* piv_data, size_t piv_size, size_t piv_rsize.""" + try: + inner = value.type[0] + size = int(value['piv_size']) + if 0 <= size <= _MAX_READ: + d.putItemCount(size) + d.putPlotData(int(value['piv_data']), size, inner) + return + except Exception: + pass + d.putPlainChildren(value) + + +def _dequeDump(d, value, inner): + """Dump a PIDeque-layout value with the given element type.""" + size = int(value['pid_size']) + start = int(value['pid_start']) + if 0 <= size <= _MAX_READ and 0 <= start: + d.putItemCount(size) + d.putPlotData(int(value['pid_data']) + start * inner.size(), size, inner) + return True + return False + + +def qdump__PIDeque(d, value): + """PIDeque: T* pid_data, size_t pid_size, size_t pid_rsize, size_t pid_start.""" + try: + if _dequeDump(d, value, value.type[0]): + return + except Exception: + pass + d.putPlainChildren(value) + + +def qdump__PIStringList(d, value): + """PIStringList: derived from PIDeque.""" + try: + if _dequeDump(d, value, d.createType('PIString')): + return + except Exception: + pass + d.putPlainChildren(value) + + +def qdump__PIMap(d, value): + """PIMap: PIVector pim_content + PIDeque pim_index.""" + try: + index = value['pim_index'] + size = int(index['pid_size']) + if not (0 <= size <= _MAX_READ): + raise ValueError('bad size') + d.putItemCount(size) + if d.isExpanded(): + start = int(index['pid_start']) + entryType = index.type[0] + entrySize = entryType.size() + indexBase = int(index['pid_data']) + content = value['pim_content'] + valueType = content.type[0] + valueBase = int(content['piv_data']) + with Children(d, size, maxNumChild=1000): + for i in d.childRange(): + entry = d.createValueFromAddress( + indexBase + (start + i) * entrySize, entryType) + key = entry['key'] + val = d.createValueFromAddress( + valueBase + int(entry['index']) * valueType.size(), + valueType) + d.putPairItem(i, (key, val), 'key', 'value') + return + except Exception: + pass + d.putPlainChildren(value) + + +def qdump__PISet(d, value): + """PISet: derived from PIMap. Printed as the key list.""" + try: + index = value['pim_index'] + size = int(index['pid_size']) + if not (0 <= size <= _MAX_READ): + raise ValueError('bad size') + d.putItemCount(size) + if d.isExpanded(): + start = int(index['pid_start']) + entryType = index.type[0] + entrySize = entryType.size() + indexBase = int(index['pid_data']) + with Children(d, size, maxNumChild=1000): + for i in d.childRange(): + entry = d.createValueFromAddress( + indexBase + (start + i) * entrySize, entryType) + d.putSubItem(i, entry['key']) + return + except Exception: + pass + d.putPlainChildren(value) + + +def qdump__PIPair(d, value): + """PIPair: public first, second members.""" + try: + with Children(d): + first = d.putSubItem('first', value['first']) + second = d.putSubItem('second', value['second']) + d.putValue('(%s, %s)' % ( + first.value if first.encoding is None else '...', + second.value if second.encoding is None else '...')) + except Exception: + d.putPlainChildren(value) + + +def _enumName(d, value): + """Name of an enum value (e.g. 'pivInt'), or its number on failure.""" + try: + import gdb + ival = int(value) + etype = gdb.lookup_type(d.type_name(value.typeid)) + if etype.code != gdb.TYPE_CODE_ENUM: + return str(ival) + name = str(gdb.Value(ival).cast(etype)) + return name.split('::')[-1] if '::' in name else name + except Exception: + try: + return str(int(value)) + except Exception: + return '?' + + +def qdump__PIVariant(d, value): + """PIVariant: PIByteArray _content (serialized) + Type _type (enum).""" + try: + nbytes = int(value['_content']['d']['pid_size']) + d.putValue('PIVariant(%s, %d bytes)' % (_enumName(d, value['_type']), nbytes)) + except Exception: + pass + d.putPlainChildren(value)