o2 dev
108 subscribers
49 photos
4 videos
25 files
54 links
About o2 engine development
加入频道
Удобства отладки: .natvis && lldb formatters

Привет! Немного коснемся темы удобства при отладке С++. А точнее о том, как удобно смотреть данные переменных и структур в IDE при дебаге
Порой в реальных проектах классы становятся сложными, в них много полей и даже бывает что они просто плохо написаны их сложно смотреть при отладке. Тебе нужно увидеть значение какой-нибудь переменной, а до нее листать и листать, или разворачивать кучу вложенностей

Поверх этого еще могут быть оптимизационные ухищрения - кастомные коллекции, union'ы и другие страшные штуки, которые в отладке просто не читаемы

Еще бывают довольно простые типы, например математические вектора (x, y, z), цвет (RGBA), UID и тд, в общем что-то простое, но чтобы узнать содержимое - нужно разворачивать. Хотя данные можно было бы уместить в одну строку удобным образом
Для всего этого есть специальные форматтеры, подключаемые к IDE, чтобы упростить вывод сложных объектов. Они говорят IDE в каком виде представлять ту или иную структуру, условное форматирование и форматирование превью объекта - то что может вывести XYZ или RGBA прямо в строке переменной.

Это и есть те самые natvis и lldb formatters. Есть и другие, но я пока использовал только их, поэтому расскажу про них 😁

- natvis - это инструмент чисто под msvs. Работает как конфиг, оперирующий полями объекта. Синтаксис довольно не простой, но возможности довольно обширные: условное форматирование, разворачивание сложных коллекций а-ля деревья/списки

- lldb formatters - это api для python, который позволяет писать скрипты для форматирования отображения в lldb. Тут все просто - пишешь скрипт под определенное API отладчика, внутри делаешь что хочешь. Но есть проблемы с перфомансом
Вывод краткого описания
Полезно для каких-то простых структур, содержимое которых можно уместить в одной строке: цвет RGBA, координаты вектора XYZ, интернированные строки и т.п.

в natvis это делается через тег DisplayString, внутри которого можно форматировать строку из доступных полей класса:
<Type Name="o2::Vertex">
<DisplayString>{{ x: { x } y: { y } z: { z } c: { color } u: { tu } v: { tv }}</DisplayString>
</Type>


Так же можно задавать условие отображения того или иного варианта, что весьма удобно. Вот классический пример для умного указателя: если поинтер не пустой, то показываем его, иначе "empty"
<DisplayString Condition="(mPtr != 0)">{ mPtr }</DisplayString>
<DisplayString Condition="(mPtr == 0)">empty</DisplayString>


в lldb formatter'ах чуть сложнее. Сначала нужно зарегать хендлер для типа:
debugger.HandleCommand('type summary add -F o2_lldb_formatters.vertex_summary "o2::Vertex"')


затем реализовать его:
def get_child_value(valobj, name):
"""Helper to safely get child value"""
try:
child = valobj.GetChildMemberWithName(name)
if child.IsValid():
return child.GetValue()
return "?"
except:
return "?"


def vertex_summary(valobj, internal_dict):
"""Format o2::Vertex"""
x = get_child_value(valobj, "x")
y = get_child_value(valobj, "y")
z = get_child_value(valobj, "z")
color = get_child_value(valobj, "color")
tu = get_child_value(valobj, "tu")
tv = get_child_value(valobj, "tv")
return f"{{ x: {x} y: {y} z: {z} c: {color} u: {tu} v: {tv} }}"

Относительно xml это выглядит понятнее и гибче
Построение дочерних нод и структуры

Когда нужно какие-то поля показать, а какие-то нет. Использовать более читаемые слова или изменить порядок

В natvis это делается с помощью тегов <Expand>, <Synthetic> и <Item>:
- <Expand> - список полей при разворачивании
- <Item> - конкретное поле класса
- <Synthetic> - сложное синтезированное поле класса, которое может быть коллекцией
- <ExpandedItem> - развернутые поля определенного дочернего поля класса

Рассмотри пару примеров. Здесь просто перечисляем некоторые поля класса. Обратите внимание на само описание типа - оно задается для шаблонного типа, через &lt;*&gt; (да, вот такой вот незамысловатый формат с кодированием угловых скобок в xml)
<Type Name="o2::FieldInfo&lt;*&gt;">
<DisplayString>{{ name={ mName } value={ mFieldRef } }}</DisplayString>
<Expand>
<Item Name="name">mName</Item>
<Item Name="value">mFieldRef</Item>
<Item Name="owner">mOwner</Item>
</Expand>
</Type>


Или вот пример посложнее с синтезированными полями. Здесь описан тип меша с кастомными коллекциями вершин и индексов. IDE передаются два параметра: Size - размер коллекции, и ValuePointer - откуда начинать отсчет
<Type Name="o2::Mesh">
<DisplayString>{{ vertx = { vertexCount }/{ mMaxVertexCount } poly = { polyCount }/{ mMaxPolyCount } tex = { mTexture.mTexture } }}</DisplayString>
<Expand>
<Item Name="texture" ExcludeView="simple">mTexture.mTexture</Item
<Synthetic Name="verticies">
<DisplayString>{ vertexCount }/{ mMaxVertexCount }</DisplayString>
<Expand>
<ArrayItems>
<Size>vertexCount</Size>
<ValuePointer>vertices</ValuePointer>
</ArrayItems>
</Expand>
</Synthetic
<Synthetic Name="indexes">
<DisplayString>{ polyCount }/{ mMaxPolyCount }</DisplayString>
<Expand>
<ArrayItems>
<Size>polyCount*3</Size>
<ValuePointer>indexes</ValuePointer>
</ArrayItems>
</Expand>
</Synthetic
</Expand>
</Type>


В целом, формат хоть и немного упоротый, но с ним можно легко разобраться, особенно в эру ИИ, которые справляются с этим на ура.

Здесь в моем движке лежит список .natvis описаний типов, там есть довольно сложные описания типа актора
https://github.com/o2-engine/o2/blob/master/Framework/Platforms/Windows/Framework.natvis
Для lldb все сильно проще - пишется скрипт, который формирует нужную структуру и отображение. Но у меня не получилось это хорошо завести из-за проблем перфоманса. Эти форматтеры вешали отладчик намертво, в итоге остались только простые summary обработчики

https://github.com/o2-engine/o2/blob/master/Framework/Platforms/o2_lldb_formatters.py