Розширення Visual Studio для візуалізації користувальницьких класів в режимі налагодження

Доброго часу доби,

У цій статті я хочу розповісти про створення розширення для 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 < int> у режимі відладки:



Створення розширення

Для створення розширень потрібно встановити 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 for later reading vector elements. 
// An std::vector < int> variable has the following nodes in raw view: 
// myVector 
// + std::_Vector_alloc<0,std::_Vec_base_types<int,std::allocator<int> > > 
// + std::_Vector_val<std::_Simple_types<int> > 
// + std::_Container_base12 
// + _Myfirst 
// + _Mylast 
// + _Myend 

// This is the транспортний base class of std::vector (std::_Vector_val<std::_Simple_types<int> > 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, read out, ref unreadable); 
Debug.Assert(hr == VSConstants.S_OK, "IDebugMemoryBytes.ReadAt failed"); 

// Create data series that will be needed by the плоттера 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 плоттера window to show vector contents 
PlotterWindow plotterWindow = new PlotterWindow(); 
WindowInteropHelper helper = new WindowInteropHelper(plotterWindow); 
helper.Owner = (IntPtr)ownerHwnd; 
plotterWindow.ShowModal(series); 

return hr; 
} 

/// <summary> 
/// Helper method to return the child property at the given index 
/ / / < /summary> 
/ / / < param name="index">The index of the child property</param> 
/ / / < param name="debugPropertyInfo">The parent property</param> 
/ / / < returns>Child property at index</returns> 
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<char>. У векторі ми будемо зберігати рядок-маркер, з якої потім можна буде знайти сериализованный об'єкт в shared memory. Далі, кожен не константный метод класу додаємо виклик функції збереження класу в shared memory. Тепер при кожній зміні класу, він буде зберігати себе в shared memory.
У самому розширення: дістаємо з об'єкта рядок-маркер, використовуючи метод з прикладу MS. Далі, по маркеру дістаємо з shared memory сериализованный об'єкт і десериализуем його. У результаті ми маємо копію об'єкта в нашому розширення. Ну а далі вже справа техніки. З об'єкта дістаємо корисні нам дані і як-то показуємо їх в зручному вигляді.

HabraLine Debug Visualizer

Для демонстрації цієї ідеї був написаний приклад розширення. Так само для демонстрації роботи розширення було написано найпростіша бібліотека. У цій бібліотеці всього два класу: HabraPoint і HabraLine. Плюс пара класів, необхідних для серіалізації та роботи з shared memory. Клас HabraLine — це просто відрізок. Для серіалізації та роботи з shared memory використовується boost. Після встановлення розширення, у нас з'являється можливість візуалізувати значення змінних типу HabraLine.

Подивитися розширення у дії можна на короткому відео:



Посилання на джерело розширення: ТЫНЦ
Посилання на демонстраційний проект: ТЫНЦ

Сподіваюся ця стаття буде комусь корисна і надихне на написання корисних розширень до Visual Studio.

Всім удачі.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.