Если вы пишете код для обработки изображений на С++, вы наверняка используете замечательную библиотеку OpenCV. Уверен, вам не раз хотелось посмотреть на изображения в процессе отладки вашего кода. Для этого можно использовать такие удобные функции как imshow или imwrite. Однако это требует модификации исходного кода, а любая современная IDE во время отладки позволяет смотреть значения переменных на лету. Вот было бы здорово так же смотреть изображения?
Если в качестве IDE вы пользуетесь Visual Studio, то знаете, что с .NET в этом плане всё проще. Однако речь идёт про OpenCV, а это только native C++, только хардкор. В этой статье я расскажу, как всё-таки заставить Visual Studio показывать изображения прямо в процессе отладки и дам ссылку на готовое решение. А также коротко расскажу о способах кастомизации Visual Studio.
Введение
Для тех, кто просто хочет воспользоваться готовым решением, сразу приведу ссылки:
- NativeViewer на SourceForge
- NativeViewer for Visual Studio 2010 в Visual Studio Extensions Gallery
- NativeViewer for Visual Studio 2010 в Visual Studio Extensions Gallery
Остальным я кратко расскажу об инструментах, с помощью которых можно в принципе кастомизировать Visual Studio, и более подробно о реализации решения одним из них. Все эксперименты выполнялись в Visual Studio 2010, готовое решение было протестировано в Visual Studio 2005, 2008, 2010, 2012 (и по идее должно работать в 2003).
Поиск готового решения
Когда мне по работе потребовалось отлаживать код на С++, использующий OpenCV для работы с изображениями, мне сразу же захотелось уметь смотреть их непосредственно во время отладки. Я радостно полез в гугл, и начал искать готовые решения. И… ничего не нашёл. Ни для изображений OpenCV, ни для вообще визуализации чего-либо из нативного C++ кода. Я поинтересовался у общественности, на что внушающий доверие резидент мне сказал, что отладчик нативного кода в принципе не может быть расширен подобным образом. В этот момент я уверился, что велосипед ещё не изобретён.
Возможности кастомизации Visual Studio
Чтобы выбрать подходящий инструмент для визуализатора, нужно хотя бы приблизительно представлять, с каких сторон вообще можно расширять Visual Studio. Для начала нам понадобится знать два понятия:
- DTE Интерфейс взаимодействия с Visual Studio, который был введен в .NET-версиях (начиная с 2003). С его помощью можно получить объекты, которые предоставляют доступ к самым разным частям Visual Studio — структуре решения, настройкам оболочки, активному окну, даже к отладчику.
- MEF Managed Extensibility Framework — более новая модель, доступная начиная с Visual Studio 2010. Позволяет осуществлять более тесную интеграцию и обеспечивает взаимодействие MEF-компонент, которые в свою очередь могут использовать DTE.
Далее представлен список сущностей, используемых для кастомизации Visual Studio. Список составлен с учётом конкретной задачи, поэтому не претендует на полноту.
- Макросы (Macros) Отлично подходят для автоматизации каких-либо действий, выполняемых над исходным кодом, структурой проекта и т.п. С их помощью можно выполнить проверку стиля кода, сгенерировать шаблоны, записать и воспроизвести некоторые действия пользователя. Имеют доступ к DTE. Доступны в меню Tools→Macros.
- Дополнения (Add-Ins) Появились начиная с Visual Studio .NET. Используются для любых мыслимых кастомизаций, от улучшенной подсветки синтаксиса, до интеграции системы контроля версий. Имеют доступ к DTE. Доступны через Tools→Add-In Manager.
- Расширения (Extensions) Появились начиная с Visual Studio 2010, являются более продвинутой версией дополнений (список отличий тут). Взаимодействуют с MEF. Доступны через Tools→Extension Manager.
- Визуализаторы (Visualizers) Предназначены для отображения объектов в процессе отладки. Доступны при нажатии маленькой иконки с лупой рядом с именем переменной при отображении её содержимого.
Казалось бы, визуализаторы — именно то, что нужно. Но вот незадача — работают они только с managed C++. Первые три варианта (макросы, дополнения и расширения) могут получить доступ к памяти отлаживаемого процесса только с помощью объекта Debugger, у которого из подходящих функций есть только ExecuteStatement и GetExpression. Однако обе они так или иначе возвращают результаты в виде строк, ограниченных по размеру. Конечно, можно постараться по кусочкам выдрать содержимое изображения и перевести назад в бинарный вид, но получается как-то очень криво.
Существует ещё такой волшебный файлик под названием autoexp.dat. Он устанавливается вместе с Visual Studio и обычно лежит по адресу
С:Program Files (x86)Microsoft Visual Studio <vs_version>Common7PackagesDebuggerautoexp.dat
В нём описаны правила, в соответствии с которыми отображается содержимое переменных при отладке нативного C++ кода. Именно он заставляет, например, содержимое контейнера std::vector выглядеть так, как слева, а не так, как справа:
И вот как он это делает:
;------------------------------------------------------------------------------ ; std::vector from <vector> ;------------------------------------------------------------------------------ ; vector is previewed with "[<size>](<elements>)". ; It has [size] and [capacity] children, followed by its elements. ; The other containers follow its example. std::vector<*>{ preview ( #( "[", $e._Mylast - $e._Myfirst, "](", #array( expr: $e._Myfirst[$i], size: $e._Mylast - $e._Myfirst ), ")" ) ) children ( #( #([size] : $e._Mylast - $e._Myfirst), #([capacity] : $e._Myend - $e._Myfirst), #array( expr: $e._Myfirst[$i], size: $e._Mylast - $e._Myfirst ) ) ) } std::_Vector_iterator<*>|std::_Vector_const_iterator<*>{ preview ( *$e._Ptr ) children ( #([ptr] : $e._Ptr) ) }
Это старая, плохо документированная технология. Поддерживается ещё начиная с Visual C++ 6.0. Небольшое пособие по использование здесь. Однако так вышло, что именно она спасёт отца русской демократии позволит нам получить нормальный доступ к памяти отлаживаемого процесса. Кроме того, если вы часто работаете со сложными многоуровневыми структурами (например, считаете геометрию с помощью CGAL), пара вручную добавленных правил в этом файле может серьёзно упростить вам жизнь.
Оказывается, что синтаксис файла autoexp.dat позволяет не только писать выражения для, например, адресной арифметики, но и вообще вызывать любой сторонний код! Это делается с помощью специальной конструкции $ADDIN. За неимением лучшего, именно этим инструментом я и воспользовался для своего визуализатора.
Решение на базе Expression Evaluator Add-In
Небольшое руководство по написанию библиотек для файла autoexp.dat доступно в MSDN, там они его называют Expression Evaluator Add-In. Для вызова библиотечной функции достаточно указать путь к .dll-файлу и имя вызываемой функции.
cv::Mat=$ADDIN(NativeViewer.dll,CvMatViewer)
Прототип функции в dll выглядит следующим образом:
HRESULT WINAPI CvMatViewer(DWORD dwAddress, DEBUGHELPER* pHelper,
int nBase, BOOL bUniStrings, char* pResult, size_t max, DWORD reserved)
В аргументе pHelper передаётся указатель на структуру DEBUGHELPER, которая предоставляет функции для обращения к памяти отлаживаемого процесса и возвращает адрес объекта, отображаемого в отладчике:
typedef struct tagDEBUGHELPER
{
DWORD dwVersion;
HRESULT (WINAPI *ReadDebuggeeMemory)( struct tagDEBUGHELPER *pThis, DWORD dwAddr, DWORD nWant, VOID* pWhere, DWORD *nGot );
// from here only when dwVersion >= 0x20000
DWORDLONG (WINAPI *GetRealAddress)( struct tagDEBUGHELPER *pThis );
HRESULT (WINAPI *ReadDebuggeeMemoryEx)( struct tagDEBUGHELPER *pThis, DWORDLONG qwAddr, DWORD nWant, VOID* pWhere, DWORD *nGot );
int (WINAPI *GetProcessorType)( struct tagDEBUGHELPER *pThis );
} DEBUGHELPER;
В pResult нужно сохранить строку, показываемую отладчиком. По идее, это всё, что должна делать данная функция. Но кто же нас остановит? Доступ к памяти у нас уже есть. А дальше, как говорится, дело техники.
Извлечение содержимого изображения
Единственная информация о типе, доступная нам в данный момент — это его название, cv::Mat. Это значит, что придётся вычислить адреса полей на основе .h-файла какой-то конкретной версии OpenCV. Однако не думаю, что в дальнейшем структура полей этого фундаментального класса будет изменяться.
Я не буду подробно останавливаться на технических моментах, типа как считать объект из памяти зная его адрес и имея .h-файл с его описанием, исходники выложены на SourceForge. Отмечу только, что структура pHelper позволяет узнать битность отлаживаемого процесса. А это значит, что нужно учесть, что указатели могут иметь размер как 4, так и 8 байт. Когда мы считали все поля объекта cv::Mat, мы можем точно так же считать содержимое самого изображения, т.к. его адрес находится в поле data.
Форматирование строки отображения
Ну, раз уж мы всё равно здесь, давайте действительно отформатируем строчку, отображаемую отладчиком. А то стандартные внутренности выглядят не очень красиво. Сделаем что-нибудь типа такого:
Визуализация изображения
Собственно, ради чего мы все здесь и собрались. Раз уж теперь у нас есть изображение, можно делать визуализацию с помощью .NET. Я использовал старую добрую Windows Forms и C#. Форма с диалогом для визуализации находится в отдельной сборке, ей в конструкторе передаётся объект System.Bitmap. Чтобы его сконструировать, оригинальная dll собиралась с поддержкой .NET, с ключом /clr. Это позволило использовать C++/CLI.
Я переживал, что вызов .NET-диалога из нативной библиотеки вызовет сложности и придётся самому создавать AppDomain. Однако, поскольку студия сама является .NET-приложением, ничего такого делать не пришлось. Всё сразу заработало, ну я и не стал разбираться подробнее. Отмечу только, что пришлось показывать диалог из отдельной нити с параметром ApartmentState::STA, иначе возникали проблемы при открытии дополнительных диалогов, например для сохранения изображения на диск.
Теперь мы столкнулись с главным недостатком выбранного подхода — невозможно определить, вызывается функция при форматировании вывода в окно Watch, или при наведении пользователем мыши на переменную в редакторе. Если каждый раз показывать окно с изображением, этим невозможно будет пользоваться.
Единственное адекватное решение, которое я смог придумать, таково: окно показывается только в том случае, если нажата какая-либо спец-клавиша. А именно, Ctrl. В противном случае только форматируется строка вывода. Таким образом, чтобы посмотреть изображение в процессе отладки, пользователю надо зажать Ctrl, навести на переменную в редакторе и тогда выскочит окно. Кривовато, да, но по факту пользоваться оказалось достаточно удобно.
При показе окна с изображением процесс Visual Studio блокируется, т.к. формально мы находимся в функции форматирования вывода отладчика. Это неприятно, но ничего фатального в этом нет, т.к. нет нужды одновременно изучать изображение и продолжать взаимодействовать со студией.
Интеграция с Visual Studio
Решение есть, теперь его надо упаковать. Для этого замечательно подходит существующий механизм расширений. Изначально я ориентировался на Visual Studio 2010, так что выбрал именно более современные расширения, а не более универсальные дополнения.
Создание расширений хорошо документировано. Для начала, нам понадобится Visual Studio SDK (для 2010 или 2012). После установки, можно будет создать новый проект Visual C#→Extensibility→Visual Studio Package. Запустится мастер, который спросит информацию о расширении:
По умолчанию он предлагает создать команду меню (Menu Comand), панель инструментов (Tool Window) или подкрутить редактор (Custom Editor). Нам не понадобится ничего из этого. Дело в том, что расширение представляет из себя файл VSIX, который является просто zip-архивом. При установке в Visual Studio, она распаковывает его в одну из пользовательских директорий (подробнее здесь). Там могут быть библиотеки, взаимодействующие с MEF, но могут быть и любые другие файлы. Например, наша библиотека с Expression Evaluator Add-In.
Возникает небольшая проблема с загрузкой сборки с формой для визуализации. Если добавить её в References, то её поиск будет происходить в каталоге с исполнимым файлом devenv.exe и в GAC. Она же находится в директории с расширением, поэтому её приходится грузить вручную при помощи функции Assembly::LoadFrom. Диалог затем создаётся и показывается с помощью reflection-методов.
Мне хотелось сделать настройки, которые можно было бы менять из меню Tools→Options. Это хорошо описано в руководстве в MSDN. Там описано, как создавать стандартную сетку ключ/значение, и произвольную форму:
Последняя форма понадобится для автоматического добавления строчки в файл autoexp.dat. К сожалению, не существует возможности выполнить какую-либо команду в момент установки расширения VSIX (см. здесь таблицу Supported Capabilities). Поэтому после установки расширения, пользователю нужно зайти на эту страницу и нажать кнопку «Add entry».
Чтобы найти файл autoexp.dat, нужно знать, где установлена Visual Studio. Это можно посмотреть в реестре, но лучше спросить у неё самой с помощью DTE. Как получить объект DTE из расширения написано здесь. Свойство FullName вернёт полный путь к файлу devenv.exe.
Приятно удивила Visual Studio 2012. В ней наконец-то предложена замена морально устаревшему файлу autoexp.dat — технология NATVIS. Документации как таковой пока нет, но есть хорошее описание в блоге Microsoft. И, слава обратной совместимости, они оставили возможность вызова стороннего кода через тот же механизм, только теперь он указывается в аргументе LegacyAddin. Единственное описание, что я нашёл, в ответе к этому вопросу. Чувствуется, что технология совсем недавно анонсирована.
Огромный плюс NATIVS заключается в том, что теперь правила визуализации (оформленные в виде отдельных XML-файлов с расширением natvis) могут быть раскиданы по разным директориям. В том числе по пользовательским, а также они могут содержаться в расширениях. Для этого .nativs-файл достаточно добавить в Assets при сборке расширения. Поэтому в расширении NativeViewer для Visual Studio 2012 нет страницы с интеграцией и оно работает из коробки.
По поводу отладки VSIX-расширений. Это оказалось реализовано очень удобно. При запуске VSIX-проекта открывается экспериментальная копия Visual Studio, в которую установлена текущая версия расширения. Отладка работает нормально, мне удавалось отлаживать в исходной студии нативный код в dll, которую грузила экспериментальная копия.
Так как расширения появились только в Visual Studio 2010, на более ранние версии их придётся устанавливать вручную. Пока я написал руководство, а по-хорошему, конечно, следует сделать инсталлятор.
Заключение
В этой статье я описал решение проблемы, с которой столкнулся в процессе разработки. Примечателен тот факт, что оно предназначено для популярных инструментов разработки, но, как ни странно, не имеет аналогов. Ну, по крайней мере я не смог найти ничего похожего.
В этой статье я постарался меньше акцентировать внимание на технических деталях и компенсировать это обилием ссылок. Интересующиеся могут смотреть исходный код и задавать вопросы. Если какой-то из аспектов покажется общественности достаточно интересным, я могу написать о нём более подробную статью.
Я очень надеюсь, что это расширение многим окажется полезным. Если кто-то хочет принять участие в развитии проекта — пишите.
Ссылки
- NativeViewer на SourceForge
- NativeViewer for Visual Studio 2010 в Visual Studio Extensions Gallery
- NativeViewer for Visual Studio 2010 в Visual Studio Extensions Gallery
- Debug in Visual Studio using NativeViewer
- EEAddIn Sample: Debugging Expression Evaluator Add-In
- Anatomy of a VSIX Package
- Building and publishing an extension for Visual Studio 2010
- VSX FAQ
- Writing debugger type visualizers for C++ using .natvis files
- Walkthrough: Creating an Options Page
Автор: mmatrosov