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/CMakeLists.txt b/CMakeLists.txt index 1b81aeab..fa49c805 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -782,7 +782,13 @@ endif() # Install -# Check if system or local install will be used (to system install use "-DLIB=" argument of cmake) + +# GDB pretty-printers (see tools/gdb/): module + auto-load stub named after the library +set(PIP_GDB_PY "${CMAKE_CURRENT_SOURCE_DIR}/tools/gdb/pip_pp.py") +set(PIP_GDB_STUB "${CMAKE_CURRENT_BINARY_DIR}/gdb/libpip.so-gdb.py") +set(PIP_GDB_PY_RELPATH "../share/pip/gdb") + +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/tools/gdb/gdb_autoload_stub.py.in" "${PIP_GDB_STUB}" @ONLY) if(NOT LOCAL) if(WIN32) if(MINGW) @@ -821,6 +827,17 @@ if(NOT LOCAL) endif() install(TARGETS ${PIP_MODULES} DESTINATION ${CMAKE_INSTALL_PREFIX}/lib) endif() + # GDB pretty-printers + if (NOT CROSSTOOLS) + install(FILES "${PIP_GDB_PY}" DESTINATION ${CMAKE_INSTALL_PREFIX}/share/pip/gdb) + if (PIP_LIB_TYPE STREQUAL "SHARED") + if(WIN32) + install(FILES "${PIP_GDB_STUB}" DESTINATION ${CMAKE_INSTALL_PREFIX}/bin RENAME "$-gdb.py") + else() + install(FILES "${PIP_GDB_STUB}" DESTINATION ${CMAKE_INSTALL_PREFIX}/lib RENAME "$-gdb.py") + endif() + endif() + endif() else() if(WIN32) install(TARGETS ${PIP_MODULES} RUNTIME DESTINATION bin) @@ -835,6 +852,15 @@ else() if(HDR_DIRS) install(DIRECTORY ${HDR_DIRS} DESTINATION include/pip) endif() + # GDB pretty-printers + install(FILES "${PIP_GDB_PY}" DESTINATION share/pip/gdb) + if (PIP_LIB_TYPE STREQUAL "SHARED") + if(WIN32) + install(FILES "${PIP_GDB_STUB}" DESTINATION bin RENAME "$-gdb.py") + else() + install(FILES "${PIP_GDB_STUB}" DESTINATION lib RENAME "$-gdb.py") + endif() + endif() endif() file(GLOB CMAKES "cmake/*.cmake" "cmake/*.in") install(FILES ${CMAKES} DESTINATION ${CMAKE_ROOT}/Modules) diff --git a/doc/pages/debugging.md b/doc/pages/debugging.md new file mode 100644 index 00000000..57f62951 --- /dev/null +++ b/doc/pages/debugging.md @@ -0,0 +1,187 @@ +\~english \page debugging Debugging with GDB +\~russian \page debugging Отладка в GDB + +\~english + +PIP ships a GDB pretty-printer script, `tools/gdb/pip_pp.py`, that makes the basic types print their content instead of their raw internals: + +| Type | Example output | +|------|----------------| +| \a PIString | `"hello"` | +| \a PIByteArray | `{0x48, 0x65, 0x6c, 0x6c, 0x6f}` | +| \a PIChar | `'a'` or `U+043c` | +| \a PIVector | `{1, 2, 3}` | +| \a PIDeque | `{1, 2, 3}` | +| \a PIStringList | `{"one", "two"}` | +| \a PIMap | `{"x": 1, "y": 2}` | +| \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: +`(PIString *) 0x7fffffffdac0 "hello"`. + +Printers are read-only and defensive: on any error GDB falls back to the +default (raw) printing. Long containers are truncated, only the first +32 elements are printed by default (the tail is shown as +`..., ... (N total)`); the limit is the `_MAX_ELEMENTS` constant at the +top of `pip_pp.py`. + +Requirements: the executable and the PIP library must be built with debug +information (`-g`). + +\section loading Loading the pretty-printers + +Manual (works with any build tree): + +\code{.gdb} +(gdb) source /path/to/pip/tools/gdb/pip_pp.py +\endcode + +Run it after the executable is loaded (in a session started as +`gdb ./program`, or after the `file` command). + +Automatic: CMake install places the script at +`/share/pip/gdb/pip_pp.py` and an auto-load stub +`/lib/libpip.so-gdb.py` next to the library. GDB evaluates the stub +automatically when libpip.so is loaded (see GDB manual, "Auto-loading +extensions"). If the library is installed outside of GDB's default auto-load +safe path, add the directory once: + +\code{.gdb} +(gdb) add-auto-load-safe-path /your/prefix/lib +\endcode + +Or create /.gdbinit file and write to it next line +\code +add-auto-load-safe-path +\endcode +, where root_dir is top-level directory contains library and pip_pp.py script. + +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 +GDB output: + +\code{.bash} +tools/gdb/test/test_pp.sh build_linux +\endcode + +\~russian + +Вместе с PIP поставляется скрипт pretty-printer'ов для GDB, `tools/gdb/pip_pp.py`, +который выводит содержимое базовых типов вместо их внутренних полей: + +| Тип | Пример вывода | +|-----|---------------| +| \a PIString | `"hello"` | +| \a PIByteArray | `{0x48, 0x65, 0x6c, 0x6c, 0x6f}` | +| \a PIChar | `'a'` или `U+043c` | +| \a PIVector | `{1, 2, 3}` | +| \a PIDeque | `{1, 2, 3}` | +| \a PIStringList | `{"one", "two"}` | +| \a PIMap | `{"x": 1, "y": 2}` | +| \a PISet | `{3, 5}` | +| \a PIPair | `{"k", 7}` | +| \a PIVariant | `PIVariant(pivInt, 4 bytes)` | +| \a PINetworkAddress | `192.168.1.10:8080` | + +Вложенные контейнеры выводятся рекурсивно, например `PIVector` +выводится как `{"a", "bb"}`. Указатели на поддерживаемые типы показывают +адрес и содержимое: `(PIString *) 0x7fffffffdac0 "hello"`. + +Принтеры только читают память и защищены от ошибок: при любом сбое GDB +возвращается к стандартному (сырому) выводу. Длинные контейнеры обрезаются, +по умолчанию выводится первые 32 элемента (хвост показывается как +`..., ... (N total)`); ограничение задаётся константой `_MAX_ELEMENTS` +в начале файла `pip_pp.py`. + +Требование: исполняемый файл и библиотека PIP должны быть собраны с +отладочной информацией (`-g`). + +\section loading Загрузка принтеров + +Вручную (работает с любым деревом сборки): + +\code{.gdb} +(gdb) source /path/to/pip/tools/gdb/pip_pp.py +\endcode + +Выполнять после загрузки исполняемого файла (в сессии, запущенной как +`gdb ./program`, или после команды `file`). + +Автоматически: CMake install ставит скрипт в +`/share/pip/gdb/pip_pp.py` и auto-load заглушку +`/lib/libpip.so-gdb.py` рядом с библиотекой. GDB выполняет заглушку +автоматически при загрузке libpip.so (см. руководство GDB, "Auto-loading +extensions"). Если библиотека установлена вне стандартного auto-load safe path +GDB, добавьте каталог один раз: + +\code{.gdb} +(gdb) add-auto-load-safe-path /your/prefix/lib +\endcode + +Или создайте /.gdbinit файл и запишите туда строку +\code +add-auto-load-safe-path +\endcode +, где root_dir - это корневая директория, в рамках которой находятся библиотека и скрипт pip_pp.py. + +Заглушка устанавливается только для shared-сборок; для статических сборок +используйте ручной `source`. + +\section qtcreator Qt Creator + +Окна Variables/Expressions в Qt Creator не используют pretty-printer'ы +GDB — у них собственный Python-дампер. PIP предоставляет модуль дампера +`tools/qtcreator/pip_types.py`, который покрывает те же типы, что и +`pip_pp.py`. Для включения: + +1. Откройте настройки отладчика (Qt Creator -> Настройки -> Отладчик). +2. Переключитесь на вкладку «Переменные и выражения». +3. В поле "Extra Debugging Helper" укажите путь к файлу: + +\code +/path/to/pip/tools/qtcreator/pip_types.py +\endcode + +После этого PIString, PIVector, PIMap и другие типы из +таблицы выше будут показывать своё содержимое в окнах +«Переменные» и «Выражения». Изменения в настройках GDB (`.gdbinit`, +auto-load заглушка) для Qt Creator не нужны; они продолжают работать +для GDB из командной строки. + +\section verifying Проверка установки + +Скрипт проверки собирает демо со всеми поддерживаемыми типами и сверяет +ожидаемый вывод GDB: + +\code{.bash} +tools/gdb/test/test_pp.sh build_linux +\endcode diff --git a/doc/pages/main.md b/doc/pages/main.md index e5612488..0f461b4e 100644 --- a/doc/pages/main.md +++ b/doc/pages/main.md @@ -20,7 +20,7 @@ serialize custom types with version back-compatibility. Summary one can find at \ref summary page. -Basic using — \ref using_basic. Further topics — \ref using_advanced. Configuration — \ref config. Code generation — \ref code_model. Streams: \ref iostream, \ref chunk_stream. State machine — \ref state_machine. Complex I/O — \ref connection. TCP client-server — \ref client_server. Tiling console — \ref console. Application tools — \ref application. Threading — \ref threading. Examples — \ref examples. +Basic using — \ref using_basic. Further topics — \ref using_advanced. Configuration — \ref config. Code generation — \ref code_model. Streams: \ref iostream, \ref chunk_stream. State machine — \ref state_machine. Complex I/O — \ref connection. TCP client-server — \ref client_server. Tiling console — \ref console. Application tools — \ref application. Threading — \ref threading. Debugging — \ref debugging. Examples — \ref examples. \~russian @@ -41,4 +41,4 @@ PIP также тесно интегрируется с системой сбо Сводку можно найти на странице \ref summary. -Базовое использование — \ref using_basic. Дополнительные темы — \ref using_advanced. Конфигурация — \ref config. Кодогенерация — \ref code_model. Потоки: \ref iostream, \ref chunk_stream. Машина состояний — \ref state_machine. Сложный ввод-вывод — \ref connection. TCP клиент-сервер — \ref client_server. Тайлинговая консоль — \ref console. Инструменты приложения — \ref application. Многопоточность — \ref threading. Примеры — \ref examples. +Базовое использование — \ref using_basic. Дополнительные темы — \ref using_advanced. Конфигурация — \ref config. Кодогенерация — \ref code_model. Потоки: \ref iostream, \ref chunk_stream. Машина состояний — \ref state_machine. Сложный ввод-вывод — \ref connection. TCP клиент-сервер — \ref client_server. Тайлинговая консоль — \ref console. Инструменты приложения — \ref application. Многопоточность — \ref threading. Отладка — \ref debugging. Примеры — \ref examples. diff --git a/libs/main/io_devices/piiodevice.cpp b/libs/main/io_devices/piiodevice.cpp index 61882071..023d4102 100644 --- a/libs/main/io_devices/piiodevice.cpp +++ b/libs/main/io_devices/piiodevice.cpp @@ -327,6 +327,14 @@ void PIIODevice::stopAndWait(PISystemTime timeout) { } +void PIIODevice::shutdown(PISystemTime timeout) { + stop(); + close(); + waitThreadedReadFinished(timeout); + waitThreadedWriteFinished(timeout); +} + + ssize_t PIIODevice::read(void * read_to, ssize_t max_size) { ssize_t ret = readDevice(read_to, max_size); return ret; diff --git a/libs/main/io_devices/piiodevice.h b/libs/main/io_devices/piiodevice.h index 1fc55580..1540c11b 100644 --- a/libs/main/io_devices/piiodevice.h +++ b/libs/main/io_devices/piiodevice.h @@ -331,6 +331,10 @@ public: //! \~russian Останавливает оба фоновых потока и ожидает их завершения. void stopAndWait(PISystemTime timeout = {}); + //! \~english Stops both background threads, close device and waits for threads completion. + //! \~russian Останавливает оба фоновых потока, закрывает устройство и ожидает завершения потоков. + void shutdown(PISystemTime timeout = {}); + //! \~english Interrupts a blocking device operation. //! \~russian Прерывает блокирующую операцию устройства. virtual void interrupt() {} diff --git a/tools/gdb/gdb_autoload_stub.py.in b/tools/gdb/gdb_autoload_stub.py.in new file mode 100644 index 00000000..c94ccbab --- /dev/null +++ b/tools/gdb/gdb_autoload_stub.py.in @@ -0,0 +1,14 @@ +# GDB auto-load stub for the PIP library pretty-printers. +# +# GDB evaluates this file automatically when the shared library it is named +# after (libpip.so) is loaded, see GDB manual "Auto-loading extensions". +# +# Generated by CMake from tools/gdb/gdb_autoload_stub.py.in. Do not edit. +import os +import sys + +_here = os.path.dirname(os.path.realpath(__file__)) +sys.path.insert(0, os.path.join(_here, "@PIP_GDB_PY_RELPATH@")) + +import pip_pp +pip_pp.register_printers(gdb.current_objfile()) diff --git a/tools/gdb/pip_pp.py b/tools/gdb/pip_pp.py new file mode 100644 index 00000000..3270a8fc --- /dev/null +++ b/tools/gdb/pip_pp.py @@ -0,0 +1,375 @@ +""" +GDB pretty-printers for the PIP (Platform-Independent Primitives) library. + +Covered types: + PIString, PIByteArray, PIChar, PIVector, PIDeque, + PIStringList, PIMap, PISet, PIPair, PIVariant, + PINetworkAddress + +Usage +----- +1. Manual (works for any build, requires debug info -g): + + (gdb) source /path/to/pip_pp.py + + Load the file after the executable (or library) is loaded, e.g. in a + session started as ``gdb ./program`` or after the ``file`` command. + +2. Automatic: CMake install places this file at + ``/share/pip/gdb/pip_pp.py`` and an auto-load stub + ``/libpip.so-gdb.py`` next to the library. GDB evaluates the + stub automatically when libpip.so is loaded (see GDB manual, + "Auto-loading extensions"). If the library is installed outside of the + default auto-load safe path, run once in gdb: + + (gdb) add-auto-load-safe-path /your/lib/dir + +All printers are read-only and defensive: on any error they return None +and GDB falls back to its default (raw) printing. + +Long containers are truncated: only the first _MAX_ELEMENTS elements are +printed (default 32; change the constant at the top of this file), the +tail is shown as `..., ... (N total)`. + +Output examples: + + (gdb) p s + $1 = "hello" + (gdb) p v + $2 = {1, 2, 3} + (gdb) p m + $3 = {"a": 1, "b": 2} + (gdb) p &s + $4 = (PIString *) 0x7fffffffdac0 "hello" +""" + +import re + +import gdb + +# Absolute safety cap: never try to read more elements than this +# (protects against corrupted size fields). +_MAX_READ = 10000000 + +# Maximum number of elements printed per container. Change to taste. +_MAX_ELEMENTS = 32 + + +def _max_elements(): + return max(1, _MAX_ELEMENTS) + + +# --------------------------------------------------------------------------- +# helpers +# --------------------------------------------------------------------------- + +def _c_type_name(t): + """Build a C-style name for a type (handles pointers recursively).""" + t = t.unqualified() + if t.code == gdb.TYPE_CODE_PTR: + return _c_type_name(t.target()) + " *" + return t.tag if t.tag is not None else t.name + + +def _escape_char(c): + """Escape a single character for a GDB C-style string literal.""" + o = ord(c) + if c == '"': + return '\\"' + if c == "\\": + return "\\\\" + if c == "\n": + return "\\n" + if c == "\t": + return "\\t" + if c == "\r": + return "\\r" + if o < 0x20 or o == 0x7F: + return "\\x%02x" % o + return c + + +def _utf16_to_string(units): + """Build a GDB string literal 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(_escape_char(chr(u))) + else: + out.append("?") + return '"' + "".join(out) + '"' + + +# --------------------------------------------------------------------------- +# printers +# --------------------------------------------------------------------------- + +class _Printer(gdb.ValuePrinter): + """Base class for PIP pretty-printers. + + Subclasses implement _total() and _item(i) for element containers. + """ + + def __init__(self, val, prefix=""): + self._val = val + self._prefix = prefix + + def _total(self): + raise NotImplementedError + + def _item(self, i): + raise NotImplementedError + + def to_string(self): + try: + total = int(self._total()) + if total < 0 or total > _MAX_READ: + return None + if total == 0: + return self._prefix + "{}" + lim = _max_elements() + count = min(total, lim) + items = [self._item(i) for i in range(count)] + tail = "" if total <= lim else ", ... (%d total)" % total + return self._prefix + "{" + ", ".join(items) + tail + "}" + except Exception: + return None + + +class _PIStringPrinter(_Printer): + """PIString: PIDeque d + cache members.""" + + def to_string(self): + try: + d = self._val["d"] + n = int(d["pid_size"]) + if n < 0 or n > _MAX_READ: + return None + start = int(d["pid_start"]) + data = d["pid_data"] + units = [int(data[start + i]["ch"]) for i in range(n)] + return self._prefix + _utf16_to_string(units) + except Exception: + return None + + +class _PIByteArrayPrinter(_Printer): + """PIByteArray: PIDeque d. Printed as a hex byte list.""" + + def _total(self): + return self._val["d"]["pid_size"] + + def _item(self, i): + d = self._val["d"] + return "0x%02x" % int(d["pid_data"][int(d["pid_start"]) + i]) + + +class _PICharPrinter(_Printer): + """PIChar: single UTF-16 code unit (ushort ch).""" + + def to_string(self): + try: + ch = int(self._val["ch"]) + if 0x20 <= ch <= 0x7E and ch not in (0x22, 0x5C): + return self._prefix + "'%s'" % chr(ch) + return self._prefix + "U+%04x" % ch + except Exception: + return None + + +class _PIVectorPrinter(_Printer): + """PIVector: T* piv_data, size_t piv_size, size_t piv_rsize.""" + + def _total(self): + return self._val["piv_size"] + + def _item(self, i): + return str(self._val["piv_data"][i]) + + +class _PIDequePrinter(_Printer): + """PIDeque: T* pid_data, size_t pid_size, size_t pid_rsize, size_t pid_start. + + Also used for PIStringList (derived from PIDeque). + """ + + def _total(self): + return self._val["pid_size"] + + def _item(self, i): + return str(self._val["pid_data"][int(self._val["pid_start"]) + i]) + + +class _PIMapPrinter(_Printer): + """PIMap: PIVector pim_content + PIDeque pim_index. + + MapIndex{ Key key; size_t index; } is kept sorted by key. + """ + + def _total(self): + return self._val["pim_index"]["pid_size"] + + def _item(self, i): + idx = self._val["pim_index"] + mi = idx["pid_data"][int(idx["pid_start"]) + i] + key = str(mi["key"]) + value = str(self._val["pim_content"]["piv_data"][int(mi["index"])]) + return "%s: %s" % (key, value) + + +class _PISetPrinter(_Printer): + """PISet: derived from PIMap. Printed as key list.""" + + def _total(self): + return self._val["pim_index"]["pid_size"] + + def _item(self, i): + idx = self._val["pim_index"] + mi = idx["pid_data"][int(idx["pid_start"]) + i] + return str(mi["key"]) + + +class _PIPairPrinter(_Printer): + """PIPair: public first, second members.""" + + def to_string(self): + try: + return (self._prefix + "{" + str(self._val["first"]) + + ", " + str(self._val["second"]) + "}") + except Exception: + 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). + + The content is a serialized blob, so the printer shows the type name + and the content size. + """ + + def to_string(self): + try: + t = str(self._val["_type"]) + if "::" in t: + t = t.split("::", 1)[1] + d = self._val["_content"]["d"] + n = int(d["pid_size"]) + return self._prefix + "PIVariant(%s, %d bytes)" % (t, n) + except Exception: + return None + + +# --------------------------------------------------------------------------- +# lookup and registration +# --------------------------------------------------------------------------- + +_PATTERNS = [ + (re.compile(r"^PIString$"), lambda v, p: _PIStringPrinter(v, p)), + (re.compile(r"^PIByteArray$"), lambda v, p: _PIByteArrayPrinter(v, p)), + (re.compile(r"^PIChar$"), lambda v, p: _PICharPrinter(v, p)), + (re.compile(r"^PIStringList$"), lambda v, p: _PIDequePrinter(v, p)), + (re.compile(r"^PIDeque<.*>$"), lambda v, p: _PIDequePrinter(v, p)), + (re.compile(r"^PIVector<.*>$"), lambda v, p: _PIVectorPrinter(v, p)), + (re.compile(r"^PIMap<.*>$"), lambda v, p: _PIMapPrinter(v, p)), + (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)), +] + + +def _pip_pp_lookup(val): + """Pretty-printer lookup function for PIP types. + + Handles both values and pointers to values: + p s -> "hello" + p &s -> (PIString *) 0x7fffffffdac0 "hello" + """ + try: + t = val.type.unqualified() + prefix = "" + if t.code == gdb.TYPE_CODE_PTR: + target = t.target() + if target is None: + return None + addr = int(val) + if addr == 0: + return None + prefix = "(%s) 0x%x " % (_c_type_name(t), addr) + val = val.dereference() + t = target.unqualified() + name = t.tag if t.tag is not None else t.name + if name is None: + return None + for pattern, factory in _PATTERNS: + if pattern.match(name): + return factory(val, prefix) + except Exception: + return None + return None + + +def register_printers(objfile=None): + """Register PIP pretty printers. + + If an objfile is given (or the current objfile is available) the + printers are registered for it (GDB checks the pretty-printer lists + of all objfiles of the current program space when printing values). + Otherwise the printers are registered globally, so they are used + for all values. + + Safe to call multiple times. + """ + if objfile is None: + try: + objfile = gdb.current_objfile() + except gdb.error: + objfile = None + if objfile is not None: + try: + if _pip_pp_lookup not in objfile.pretty_printers: + objfile.pretty_printers.append(_pip_pp_lookup) + return + except Exception: + pass + try: + if _pip_pp_lookup not in gdb.pretty_printers: + gdb.pretty_printers.append(_pip_pp_lookup) + except Exception: + pass + + +# Register for the current objfile when loaded (e.g. `source pip_pp.py` +# in a session with a loaded executable). The auto-load stub calls +# register_printers() explicitly with the objfile of libpip.so. +try: + register_printers() +except Exception: + pass diff --git a/tools/gdb/test/demo_pp.cpp b/tools/gdb/test/demo_pp.cpp new file mode 100644 index 00000000..48ca1bcc --- /dev/null +++ b/tools/gdb/test/demo_pp.cpp @@ -0,0 +1,74 @@ +// Demo for GDB pretty-printers verification. +// Build with -g, load into GDB, `source ../pip_pp.py`, `break main`, `run`, print globals. +#include "pibytearray.h" +#include "pichar.h" +#include "pimap.h" +#include "pinetworkaddress.h" +#include "pipair.h" +#include "piset.h" +#include "pistring.h" +#include "pivariant.h" +#include "pivector.h" + +static PIString str_hello("hello"); +static PIString str_cyr(PIString::fromUTF8("\xd0\xbc\xd0\xb8\xd1\x80")); // "мир" +static PIString str_empty; +static PIString str_esc; +static PIByteArray ba_hello(PIByteArray::fromHex("48656c6c6f")); // "Hello" +static PIVector vec_int; +static PIVector vec_str; +static PIVector> vec_vec; +static PIVector vec_big; +static PIMap map_str_int; +static PISet set_int; +static PIStringList list_str; +static PIPair pair_str_int; +static PIVariant var_int; +static PIVariant var_invalid; +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 { + PPInit() { + vec_int << 1 << 2 << 3; + vec_str << "a" << "bb"; + PIVector inner; + inner << 10; + vec_vec << inner << vec_int; + for (int i = 0; i < 40; ++i) { + vec_big << i; + } + + map_str_int["x"] = 1; + map_str_int["y"] = 2; + + set_int << 5 << 3; + + list_str << "one" << "two"; + + pair_str_int = PIPair("k", 7); + + var_int = PIVariant(42); + var_invalid = PIVariant(); + + ptr_str = &str_hello; + + 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"; + } +}; +static PPInit pp_init; +} // namespace + +int main() { + return 0; +} diff --git a/tools/gdb/test/test_pp.sh b/tools/gdb/test/test_pp.sh new file mode 100755 index 00000000..a5018d0f --- /dev/null +++ b/tools/gdb/test/test_pp.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# +# Verification for the PIP GDB pretty-printers (tools/gdb/pip_pp.py). +# +# Builds demo_pp.cpp with debug info against an already-built PIP library, +# runs it under GDB in batch mode with the pretty-printers loaded, and +# checks that the expected pretty output is produced. +# +# Usage: +# tools/gdb/test/test_pp.sh +# +# must contain libpip.so and pip_export.h, e.g. build_linux. +# Requires g++ and gdb. +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PIP_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" +BUILD_DIR="${1:-}" +if [[ -z "${BUILD_DIR}" ]]; then + echo "usage: $0 (e.g. build_linux)" >&2 + exit 2 +fi +BUILD_DIR="$(cd "${BUILD_DIR}" && pwd)" + +if [[ ! -f "${BUILD_DIR}/libpip.so" ]]; then + echo "error: ${BUILD_DIR}/libpip.so not found" >&2 + exit 2 +fi +if [[ ! -f "${BUILD_DIR}/pip_export.h" ]]; then + echo "error: ${BUILD_DIR}/pip_export.h not found (is this a PIP build dir?)" >&2 + exit 2 +fi + +WORK="$(mktemp -d)" +trap 'rm -rf "${WORK}"' EXIT + +# Include dirs: libs/main plus each of its subdirectories, plus the build dir. +INCS="-I${PIP_ROOT}/libs/main -I${BUILD_DIR}" +for d in "${PIP_ROOT}"/libs/main/*/; do + INCS+=" -I${d%/}" +done + +echo "== building demo ==" +g++ -g -O0 -std=c++11 "${SCRIPT_DIR}/demo_pp.cpp" \ + ${INCS} \ + -L"${BUILD_DIR}" -lpip -Wl,-rpath,"${BUILD_DIR}" \ + -o "${WORK}/demo_pp" + +echo "== running gdb ==" +OUT="$(gdb -batch -nx \ + -ex "source ${PIP_ROOT}/tools/gdb/pip_pp.py" \ + -ex "break main" -ex run \ + -ex "print str_hello" \ + -ex "print str_cyr" \ + -ex "print str_empty" \ + -ex "print str_esc" \ + -ex "print chr_a" \ + -ex "print chr_cyr" \ + -ex "print ba_hello" \ + -ex "print vec_int" \ + -ex "print vec_str" \ + -ex "print vec_vec" \ + -ex "print vec_big" \ + -ex "print map_str_int" \ + -ex "print set_int" \ + -ex "print list_str" \ + -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)" + +echo "${OUT}" | grep -E '^\$[0-9]+ = ' || true + +fail=0 +check() { + local pattern="$1" desc="$2" + if grep -qE -- "${pattern}" <<<"${OUT}"; then + echo "ok ${desc}" + else + echo "FAIL ${desc} (expected to match: ${pattern})" + fail=1 + fi +} + +check '^\$[0-9]+ = "hello"$' "PIString ascii" +check '^\$[0-9]+ = "мир"$' "PIString utf-8" +check '^\$[0-9]+ = ""$' "PIString empty" +check '^\$[0-9]+ = "a\\"b\\\\c\\nd\\te"$' "PIString escapes" +check '^\$[0-9]+ = '"'"'a'"'"'$' "PIChar printable" +check '^\$[0-9]+ = U\+043c$' "PIChar non-ascii" +check '^\$[0-9]+ = \{0x48, 0x65, 0x6c, 0x6c, 0x6f\}$' "PIByteArray hex" +check '^\$[0-9]+ = \{1, 2, 3\}$' "PIVector" +check '^\$[0-9]+ = \{"a", "bb"\}$' "PIVector" +check '^\$[0-9]+ = \{\{10\}, \{1, 2, 3\}\}$' "PIVector>" +check '\.\.\. \(40 total\)\}$' "element count limit" +check '^\$[0-9]+ = \{"x": 1, "y": 2\}$' "PIMap" +check '^\$[0-9]+ = \{3, 5\}$' "PISet" +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" + +if [[ "${fail}" -ne 0 ]]; then + echo "== FAILED ==" + exit 1 +fi +echo "== all checks passed ==" 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)