Расширение 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 later for 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 underlying 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, 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; 
            } 
     
            /// <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.

    Всем удачи.
    • +25
    • 17.7k
    • 5
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 5

      +1
      Data Display Debugger умеет такое делать уже давно:
      image
        0
        Я так понимаю это чисто юниксовая штука, но все равно интересно как он это делает. Надо будет почитать.
          0
          Эта «штука» прекрасно работает под Cygwin, а GDB есть под Windows в нативном виде, так что под Windows все работает.
          Причем разбор классов он делает без костыля с маркером, а на основе отладочной информации.
        +1
        Для решения проблемы визуализации отлаживаемых объектов
        не нужен экстеншен студии :).

        Уже много лет пользуемся этим механизмом в компании. У нас написана middleware библиотечка которая умеет визуализировать наши сущности. Клиентам ее не даем потому что посчитали что она слишком специфична чтобы быть интересной широкой публике. К тому же есть проблема с Microsoft.VisualStudio.DebuggerVisualizers.DLL. Она в каждой версии студии своя. И сложно сделать чтобы визуализация сразу под несколькими версиями студии работала нормально.
          0
          Так это вы про C#, c С++ все намного сложнее

        Only users with full accounts can post comments. Log in, please.