Расширение Visual Studio для визуализации пользовательских классов в режиме отладки03.11.2014 21:03
Доброго времени суток, В этой статье я хочу рассказать о создании расширения для Visual Studio, которое помогает визуализировать сложные пользовательские классы в процессе отладки приложения.
ПредысторияВ своем проекте мы активно используем отечественное геометрическое ядро C3D Kernel. Эта библиотека предоставляет большое количество классов для работы с кривыми, телами, поверхностями и т.п. Эти классы имеют сложную структуру и в процессе отладки приложения, используя стандартные средства визуализации Visual Studio, трудно понять, какая, например, поверхность хранится в конкретной переменной. А при отладке сложных алгоритмов очень важно понимать, что происходит с объектом на каждом шаге алгоритма.Мы пытались обойти эту проблему различными способами. Например, выписывали координаты точек на листочек, если речь шла о простой двумерной кривой. А потом по точкам рисовали эту кривую. Второй вариант решения проблемы: сохранять в нужный момент объект в файл, а затем открывать этот файл в тестовой утилите из поставки библиотеки. Это действительно помогает при отладке, но требует довольно много ручной работы. Нужно вставить код сохранения объекта в файл, перекомпилировать приложение, выполнить необходимые действия в самом приложении для запуска конкретного алгоритма, далее открыть в утилите сохраненный файл, посмотреть результат, внести при необходимости исправления в алгоритм и повторить всю процедуру опять. В целом терпимо, но хотелось иметь возможность прямо в Visual Studio в режиме отладки навести на нужную переменную и в удобном виде посмотреть, как выглядит, хранящийся там объект.
Visual Studio Extension
В поисках решения этой проблемы я наткнулся на расширение для Visual Studio Image Watch от самой Microsoft для OpenSource библиотеки OpenCV. Это расширение позволяет просматривать в процессе отладки содержимое переменных типа cv: Mat, читай bitmap’ов. Тогда пришла идея написать похожее расширение, но для наших типов. К сожалению, найти исходный код этого расширения в открытом доступе не удалось, что на мой взгляд странно. Пришлось по крупицам собирать информацию, о том как писать подобные расширения для Visual Studio. С документацией по этой теме на msdn все печально. И примеров не очень много, а точнее один std: vector visualizer. Который еще не так то просто найти. Суть примера: визуализация на графике int чисел, лежащих в std: vector в режиме отладки:
Создание расширения
Для создания расширений нужно установить Visual Studio SDK. После установки в мастере проектов появляется новый тип проекта:
Мастер создания нового проекта создаст все необходимые файлы и сконфигурирует проект.Я не буду повторять описание из примера от Microsoft, там уже кратко описаны шаги для создания расширения. Всем заинтересовавшимся рекомендую посмотреть описание к этому примеру. В этой статье я хотел затронуть те моменты, которые не описаны в этом примере.
Получение значения переменной
Переменная, содержимое которой мы хотим посмотреть, и само расширение располагаются в разных процессах. Из этого примера было по-прежнему непонятно, как получить данные из более сложных пользовательских типов. В примере демонстрируется прием, когда используя интерфейс IDebugProperty3, мы узнаем адрес первого элемента в векторе и адрес последнего элемента. Вычитанием адресов находим размер участка памяти и затем копируем этот участок памяти к себе в процесс. Приведу здесь код из примера: Получение данных из объекта
public int DisplayValue (uint ownerHwnd, uint visualizerId, IDebugProperty3 debugProperty)
{
int hr = VSConstants.S_OK;
DEBUG_PROPERTY_INFO[] propertyInfo = new DEBUG_PROPERTY_INFO[1];
hr = debugProperty.GetPropertyInfo (
enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_ALL,
10 /* Radix */,
10000 /* Eval Timeout */,
new IDebugReference2[] { },
0,
propertyInfo);
Debug.Assert (hr == VSConstants.S_OK, «IDebugProperty3.GetPropertyInfo failed»);
// std: vector internally keeps pointers to the first and last elements of the dynamic array
// First get the values of those members. We are going to use them later for reading vector elements.
// An std: vector variable has the following nodes in raw view:
// myVector
// + std::_Vector_alloc<0,std::_Vec_base_types > >
// + std::_Vector_val >
// + std::_Container_base12
// + _Myfirst
// + _Mylast
// + _Myend
// This is the underlying base class of std: vector (std::_Vector_val > node above)
DEBUG_PROPERTY_INFO vectorBaseClassNode = GetChildPropertyAt (0, GetChildPropertyAt (0, propertyInfo[0]));
// myFirstInfo member points to the first element
DEBUG_PROPERTY_INFO myFirstInfo = GetChildPropertyAt (1, vectorBaseClassNode);
// myLastInfo member points to the last element
DEBUG_PROPERTY_INFO myLastInfo = GetChildPropertyAt (2, vectorBaseClassNode);
// Vector length can be calculated by the difference between myFirstInfo and myLastInfo pointers
ulong startAddress = ulong.Parse (myFirstInfo.bstrValue.Substring (2), System.Globalization.NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture);
ulong endAddress = ulong.Parse (myLastInfo.bstrValue.Substring (2), System.Globalization.NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture);
uint vectorLength = (uint)(endAddress — startAddress) / elementSize;
// Now that we have the address of the first element and the length of the vector,
// we can read the vector elements from the debuggee memory.
IDebugMemoryContext2 memoryContext;
hr = myFirstInfo.pProperty.GetMemoryContext (out memoryContext);
Debug.Assert (hr == VSConstants.S_OK, «IDebugProperty.GetMemoryContext failed»);
IDebugMemoryBytes2 memoryBytes;
hr = myFirstInfo.pProperty.GetMemoryBytes (out memoryBytes);
Debug.Assert (hr == VSConstants.S_OK, «IDebugProperty.GetMemoryBytes failed»);
// Allocate buffer on our side for copied vector elements
byte[] vectorBytes = new byte[elementSize * vectorLength];
uint read = 0;
uint unreadable = 0;
hr = memoryBytes.ReadAt (memoryContext, elementSize * vectorLength, vectorBytes, out read, ref unreadable);
Debug.Assert (hr == VSConstants.S_OK, «IDebugMemoryBytes.ReadAt failed»);
// Create data series that will be needed by the plotter window and add vector elements to the series
Series series = new Series ();
series.Name = propertyInfo[0].bstrName;
for (int i = 0; i < vectorLength; i++)
{
series.Points.AddXY(i, BitConverter.ToUInt32(vectorBytes, (int)(i * elementSize)));
}
// Invoke plotter window to show vector contents
PlotterWindow plotterWindow = new PlotterWindow();
WindowInteropHelper helper = new WindowInteropHelper(plotterWindow);
helper.Owner = (IntPtr)ownerHwnd;
plotterWindow.ShowModal(series);
return hr;
}
///
/// Helper method to return the child property at the given index
///
/// The index of the child property
/// The parent property
/// Child property at index
public DEBUG_PROPERTY_INFO GetChildPropertyAt (int index, DEBUG_PROPERTY_INFO debugPropertyInfo)
{
int hr = VSConstants.S_OK;
DEBUG_PROPERTY_INFO[] childInfo = new DEBUG_PROPERTY_INFO[1];
IEnumDebugPropertyInfo2 enumDebugPropertyInfo;
Guid guid = Guid.Empty;
hr = debugPropertyInfo.pProperty.EnumChildren (
enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_VALUE | enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_PROP | enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_VALUE_RAW,
10, /* Radix */
ref guid,
enum_DBG_ATTRIB_FLAGS.DBG_ATTRIB_CHILD_ALL,
null,
10000, /* Eval Timeout */
out enumDebugPropertyInfo);
Debug.Assert (hr == VSConstants.S_OK, «GetChildPropertyAt: EnumChildren failed»);
if (enumDebugPropertyInfo!= null)
{
uint childCount;
hr = enumDebugPropertyInfo.GetCount (out childCount);
Debug.Assert (hr == VSConstants.S_OK, «GetChildPropertyAt: IEnumDebugPropertyInfo2.GetCount failed»);
Debug.Assert (childCount > index, «Given child index out of bounds»);
hr = enumDebugPropertyInfo.Skip ((uint)index);
Debug.Assert (hr == VSConstants.S_OK, «GetChildPropertyAt: IEnumDebugPropertyInfo2.Skip failed»);
uint fetched;
hr = enumDebugPropertyInfo.Next (1, childInfo, out fetched);
Debug.Assert (hr == VSConstants.S_OK, «GetChildPropertyAt: IEnumDebugPropertyInfo2.Next failed»);
}
return childInfo[0];
}
Все бы ничего, но здесь показано, как достать данные из объекта, если эти данные хранятся в едином участке памяти. Видимо похожий подход использует и сам MS в своем расширение Image Watch. Там изображение тоже хранится в едином куске памяти и есть указатель на начало этого куска.А что делать, если пользовательский тип имеет сложную иерархическую структуру и не похож на обычный массив данных? Все еще хуже, если класс хранит указатели на базовые классы других классов. Восстановить такой объект по кусочкам кажется нереальной задачей. Плюс такая конструкция очень хрупкая — при добавлении в какой-то промежуточный класс нового члена расширение перестает работать. В идеале мне хотелось получить сам объект или его копию. К сожалению, я не нашел способа, как такое провернуть оставаясь исключительно в рамках одного лишь расширения. Но зная, что нужные нам классы умеют сериализовывать себя в файл или в буфер в памяти, я решил, что можно использовать гибридный подход: с shared memory и вектором. Это решение не очень изящное и требует правки классов, но вполне рабочее. Плюс ничего лучше не придумалось.Реализация
Суть метода: В каждый класс (который мы хотим дебажить), добавляется специальный класс, содержащий одно поле: std: vector. В векторе мы будем хранить строку-маркер, по которой потом можно будет найти сериализованный объект в shared memory. Далее, в каждый не константный метод класса добавляем вызов функции сохранения класса в shared memory. Теперь при каждом изменении класса, он будет сохранять себя в shared memory.В самом расширении: достаем из объекта строку-маркер, используя метод из примера MS. Далее, по маркеру достаем из shared memory сериализованный объект и десериализуем его. В итоге мы имеем копию объекта в нашем расширении. Ну, а дальше уже дело техники. Из объекта достаем полезные нам данные и как-то показываем их в удобном виде.HabraLine Debug Visualizer
Для демонстрации этой идеи был написан пример расширения. Так же для демонстрации работы расширения была написано простейшая библиотека. В этой библиотеке всего два класса: HabraPoint и HabraLine. Плюс пара классов, необходимых для сериализации и работы с shared memory. Класс HabraLine — это просто отрезок. Для сериализации и работы с shared memory используется boost. После установки расширения, у нас появляется возможность визуализировать значение переменных типа HabraLine.Посмотреть расширение в действии можно на коротком видео:
[embedded content]
Ссылка на исходники расширения: ТЫНЦСсылка на демонстрационный проект: ТЫНЦ
Надеюсь эта статья будет кому-нибудь полезна и вдохновит на написание полезных расширений к Visual Studio.
Всем удачи.
© Habrahabr.ru