Search code examples
c++debuggingcontainerslldb

How to create a custom debug visualizer in CLion for a complex container?


I recently made a custom container that stores multiple type arrays in a single contiguous memory block with custom stride mechanism instead of SoA or AoS.

The problem is that all standard debug visualizers I use fail to correctly display my container because of its unconventional memory layout. My container's iterators, element retrieval, and memory management are so non-standard that the debuggers simply can't parse its internal structure.

StrideArray<int, float, char>

This visualization is just so you could understand how the internal works without having to read much of the implementation details.

Memory Layout
Block
   +--------------------------------+
   |  int[]  |  float[]  |  char[]  |
   +--------------------------------+
   ^         ^           ^
   |       offset 1      |       
   |                     |
   |                  offset 2
  base

Logical
    [int1][float1][char1]
    [int2][float2][char2]
    [int3][float3][char3]

The container one-time allocates a tuple then call init_arrays() to create a big, single memory blob and manage the offsets with pointer arithmetics.

std::tuple<T *...> arrays = {}; /* single allocation */

void init_arrays(const char *data)
{
    size_t offset = 0;
    ([&]<size_t... I>(std::index_sequence<I...>)
    {
        ((std::get<I>(arrays) = reinterpret_cast<...>(
              const_cast<char *>(data) + offset), /* single memory blob*/
          offset += cap * sizeof(...)), ...);
    })(std::make_index_sequence<sizeof...(T)> {});
}

/* at a given index, reconstruct tuple elements */
value_type get_element(size_type index) const noexcept
{
    return std::apply([&](auto *... ptrs)
    {
        return std::make_tuple(ptrs[index]...);
    }, arrays);
}

Reproducibles

Simply copy the snippet alongside the container implementation and inspect in such a way that allows you to see the debug visualization in according to your own IDE.

Also note that the container satisfies the C++ STL requirements

#include <stride_array.hpp>

int main()
{
    StrideArray<int, float, char> strd_arr = {};
    strd_arr.push_back(0, 3.14, 'a'); /* [0]: 0, [1]: 3.14, [2]: 'a' */
    strd_arr.get<1>(0); /* get index 0 of array 1 */
    strd_arr.size(); /* size */
    strd_arr.capacity(); /* capacity */

    /* please refer to the implementation for API reference */
    return 0;
}

I read some posts and articles about LLDB pretty printers and debugger extension techniques as well as old StackOverflow posts, but for CLion specifically, there's almost no reproducible documentation. I also have attempted to create basic Python-based pretty printers similar to GDB approaches but these didn't map well enough to CLion's environment.

So the question is, how can I make a custom debug visualizer that can parse this kind of memory layout and correctly display container size, capacity, and elements, as well as handling the heterogeneous nature of the container. It is also worth to know that the implementation uses template parameter pack expansion, dynamic tuple-based array creation and custom element retrieval.

I'm looking for a reliable method to make my container's internals readable during debugging, preferably with a solution that works directly in CLion's interface.

By the way, this container is specifically for scenarios where I have to maintain multiple parallel arrays of different types with minimal memory overhead like an AST.

Just to clarify, I am asking "how do I write debugger extension code that can understand this complex template metaprogramming and memory layout?"


Solution

  • If you are using lldb, you can do this with the "synthetic child providers":

    https://lldb.llvm.org/use/variable.html#synthetic-children

    The provider gets passed the value it is to analyze as an SBValue. That will know all the type-based children of the value. So you can fetch that data from the object you are to format, and can do your pointer arithmetic to figure out the address of each of the logical elements of the data structure, then use SBValue.CreateValueFromAddress to produce those elements as the "synthetic children" of the array. lldb also has a SBData that will help you pull builtin-type fields from a buffer - if that's useful.

    The way lldb works, values with a synthetic child providers transparently report the synthetic children rather than the ones from the underlying type. So the UI will without intervention see your presentation children rather than the strictly type based ones. So it doesn't require any special comprehension on the part of the UI.

    For instance, that's how you see std::vector as a vector in elements, rather than all the fields that sit between std::vector and the actual data store.