Skip to content

Commit

Permalink
initial commit with support for QString and QMap
Browse files Browse the repository at this point in the history
  • Loading branch information
planetmarshall committed Feb 3, 2024
0 parents commit a088f00
Show file tree
Hide file tree
Showing 14 changed files with 333 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build
venv
15 changes: 15 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
stages:
- test

test-formatters:
stage: test
variables:
Qt5_ROOT: /opt/gitlab-runner/builds$QT_SEED_PATH
CMAKE_PRESET: gitlab-linux-gcc-debug
script:
- python -m venv venv
- source venv/bin/activate
- pip install pytest
- pytest test
tags:
- entos-desktop-build-runner
1 change: 1 addition & 0 deletions .lldbinit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
command script import lldb_qt_formatters
8 changes: 8 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
cmake_minimum_required(VERSION 3.22.0)
project(lldb-qt-formatters LANGUAGES CXX)

set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)

add_subdirectory(examples)
32 changes: 32 additions & 0 deletions CMakePresets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"version": 6,
"cmakeMinimumRequired": {
"major": 3,
"minor": 22,
"patch": 0
},
"configurePresets": [
{
"name": "default-debug",
"hidden": true,
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "linux-gcc-debug",
"inherits": ["default-debug"],
"cacheVariables": {
"CMAKE_CXX_COMPILER": "g++"
}
},
{
"name": "gitlab-linux-gcc-debug",
"inherits": ["linux-gcc-debug"],
"cacheVariables": {
"Qt5_ROOT": "$env{Qt5_ROOT}"
}
}
]
}
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
Qt Type Formatters for LLDB
===========================

This project adds various [type formatters](https://lldb.llvm.org/use/variable.html#type-format)
to enable more user friendly display of Qt Core Types in LLDB and IDEs that
support it.

Usage
-----

Load the type formatters in LLDB by executing

```.console
command script import lldb_qt_formatters
```

or alternatively place the above line in a `.lldbinit` file in the root of the project, or in your `$HOME` directory.
To use a project-specific configuration file
you will need to [enable it](https://lldb.llvm.org/man/lldb.html#configuration-files)

### VSCode

VSCode will not source a project `.lldbinit` file even if you enable it as above. To automatically load the
config file, add the following to your `launch.json` file.

```.json
"configurations": [{
"type": "lldb",
...
"initCommands": [
"command source ${workspaceFolder}/.lldbinit"
]
}]

```

Supported Types
---------------

* `QMap<Key, Value>`
* `QMapNode<Key, Value>`
* `QString`


Contribution Guidelines
-----------------------

Contributions are welcomed.
See the guide to [variable formatting](https://lldb.llvm.org/use/variable.html) in LLDB, in addition to the
existing formatters in the [LLDB Source](https://github.com/llvm/llvm-project/tree/main/lldb/examples)

1. Create a test for your formatter. The tests run LLDB against some sample code and examine the formatted
variable output.
2. Implement a new formatter
* Simple types can be supported with a summary string in the `__init__.py` file
(eg, see support for the `QMapNode<Key, Value>` type)
* More complex types can be supported using the Python API (eg `QString`)
* Container types such as `QMap<>` require more elaborate use of the Python API to generate *"Synthetic Children"*

Formatters are registered with their types in the `__init__.py` file.
5 changes: 5 additions & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
find_package(Qt5 REQUIRED COMPONENTS Core)

add_executable(example main.cpp)
target_link_libraries(example PRIVATE Qt5::Core)
target_compile_features(example PRIVATE cxx_std_20)
14 changes: 14 additions & 0 deletions examples/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#include <QMap>
#include <QString>

int main() {
auto hello = QString("Hello World");
auto demosthenes = QString("Οὐχὶ ταὐτὰ παρίσταταί μοι γιγνώσκειν, ὦ ἄνδρες ᾿Αθηναῖοι");

auto map = QMap<QString , uint32_t>{};
map["one"] = 1;
map["forty-two"] = 42;
map["1.21 gigawatts"] = 1210000;

return 0;
}
13 changes: 13 additions & 0 deletions lldb_qt_formatters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from .qstring import *
from .qmap import *


def __lldb_init_module(debugger, dict):
init_commands = [
'type summary add --python-function lldb_qt_formatters.qstring.format_summary "QString"',
'type summary add --summary-string "\{${var.key}: ${var.value}\}" -x "QMapNode<.+>$"',
'type synthetic add --python-class lldb_qt_formatters.qmap.SyntheticChildrenProvider -x "QMap<.+>$"',
'type summary add --expand --python-function lldb_qt_formatters.qmap.format_summary -x "QMap<.+>$"'
]
for command in init_commands:
debugger.HandleCommand(command)
83 changes: 83 additions & 0 deletions lldb_qt_formatters/qmap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from typing import List

from lldb import SBValue


def format_summary(valobj: SBValue, internal_dict, options):
"""
Format a summary string for QMap values
:param valobj:
:param internal_dict: unused
:param options: unused
"""

provider = SyntheticChildrenProvider(valobj.GetNonSyntheticValue(), internal_dict)
return f"Size: {provider.size()}"


def _depth_first_traverse(node: SBValue, nodes: List[int]):
# We only need the address of each node as we're going to read its
# value directly from memory later
nodes.append(node.load_addr)

# QMaps are Red-Black trees but we don't need to know much about the
# details to just extract the data.
left = node.GetChildMemberWithName("left")
if left.GetValueAsUnsigned() != 0:
_depth_first_traverse(left.deref, nodes)

right = node.GetChildMemberWithName("right")
if right.GetValueAsUnsigned() != 0:
_depth_first_traverse(right.deref, nodes)


def _derived_node_type(map_obj: SBValue):
# The actual data in the QMap is in QMapNode<> elements, however QMaps only actually store each node as a
# pointer to a non-templated base class of QMapNode<>. We need to work out what the derived class is supposed to be
# so we can get at the data
map_type = map_obj.type.GetUnqualifiedType()
key_type = map_type.GetTemplateArgumentType(0)
value_type = map_type.GetTemplateArgumentType(1)
node_type = f"QMapNode<{key_type.name}, {value_type.name}>"
# TODO (andrew) this can fail in unusual circumstances - such as trying to use a GCC-built Qt with a Clang-built
# main, but that's only ever going to work by coincidence anyway...
return map_obj.target.FindFirstType(node_type)


class SyntheticChildrenProvider:
def __init__(self, valobj, internal_dict):
self.valobj = valobj
self.node_type = _derived_node_type(self.valobj)

def size(self):
data_member_ptr = self.valobj.GetChildMemberWithName("d")
data_member = data_member_ptr.deref
return data_member.GetChildMemberWithName("size").GetValueAsSigned()

def update(self):
data_member_ptr = self.valobj.GetChildMemberWithName("d")
data_member = data_member_ptr.deref
# The root node is also a QMapNode<> but doesn't contain any
# data we actually need.
root = data_member.GetChildMemberWithName("header")
nodes = []
_depth_first_traverse(root, nodes)
self.size = data_member.GetChildMemberWithName("size").GetValueAsSigned()
self.nodes = [] if self.size == 0 else nodes[1:]

def num_children(self):
return self.size

@staticmethod
def get_child_index(self, name: str):
index = name.lstrip("[").rstrip("]")
return int(index)

def get_child_at_index(self, index):
node_addr = self.nodes[index]
child_node = self.valobj.CreateValueFromAddress(f"[{index}]", node_addr, self.node_type)
return child_node

def has_children(self):
return self.size > 0
26 changes: 26 additions & 0 deletions lldb_qt_formatters/qstring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from lldb import SBError, SBValue


def format_summary(valobj: SBValue, internal_dict, options):
"""
Format a summary string for QString values
:param valobj:
:param internal_dict: unused
:param options: unused
"""
if not valobj.process:
return "Unknown"

data_member = valobj.GetChildMemberWithName("d")
array_member = data_member.deref

offset_in_bytes = array_member.GetChildMemberWithName("offset").GetValueAsUnsigned()
string_length = array_member.GetChildMemberWithName("size").GetValueAsUnsigned()
address = data_member.GetValueAsUnsigned()

error = SBError()
character_size = 2
content = valobj.process.ReadMemory(address + offset_in_bytes, string_length * character_size, error)

return f'"{bytearray(content).decode("utf-16")}"'
43 changes: 43 additions & 0 deletions test/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import os
from subprocess import run
from tempfile import NamedTemporaryFile

import pytest


@pytest.fixture(scope="session")
def exe(tmp_path_factory):
repository_root = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
build_folder = tmp_path_factory.mktemp("build")
cmake_preset = os.environ.get("CMAKE_PRESET", "linux-gcc-debug")
cmake_configure = ["cmake", "--preset", cmake_preset, "-S", repository_root, "-B", build_folder]
cmake_build = ["cmake", "--build", build_folder]
run(cmake_configure)
run(cmake_build)

return build_folder / "bin" / "example"


@pytest.fixture
def lldb(request, exe):
preamble = [
"command script import lldb_qt_formatters",
"breakpoint set --file main.cpp --line 13",
"run"
]
marker = request.node.get_closest_marker("lldb_script")
test_script = marker.args[0] if marker is not None else ""

script = "\n".join(preamble + [test_script.lstrip(" \n")])

with NamedTemporaryFile("w") as fp:
fp.write(script)
fp.flush()

lldb_command = ["lldb", "--batch", "--source", fp.name, exe]
result = run(lldb_command, check=True, text=True, capture_output=True)
yield result.stdout

# TODO (andrew) work out how to just print the lldb output if the test fails
print(result.stdout)

15 changes: 15 additions & 0 deletions test/test_qmap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import re

import pytest


@pytest.mark.lldb_script("frame variable map")
def test_qmap_of_qstring_uint_summary(lldb):
match = re.search(r"\(QMap<.+>\) map = (?P<summary>.+) {", lldb, re.MULTILINE)
assert match.group('summary') == 'Size: 3'


@pytest.mark.lldb_script("frame variable map")
def test_qmap_of_qstring_uint_children(lldb):
match = re.search(r"\[0] = (?P<summary>\{.+})", lldb, re.MULTILINE)
assert match.group('summary') == '{"forty-two": 42}'
16 changes: 16 additions & 0 deletions test/test_qstring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import re

import pytest


@pytest.mark.lldb_script("frame variable hello")
def test_qstring_summary(lldb):
match = re.search(r"\(QString\) hello = (?P<summary>.+)", lldb, re.MULTILINE)
assert match.group('summary') == '"Hello World"'


@pytest.mark.lldb_script("frame variable demosthenes")
def test_qstring_utf8(lldb):
demosthenes = "Οὐχὶ ταὐτὰ παρίσταταί μοι γιγνώσκειν, ὦ ἄνδρες ᾿Αθηναῖοι"
match = re.search(r"\(QString\) demosthenes = (?P<summary>.+)", lldb, re.MULTILINE)
assert match.group('summary') == f'"{demosthenes}"'

0 comments on commit a088f00

Please sign in to comment.