Эта статья является продолжением статьи из далекого 2014 года. Напомню, о чем шла речь в прошлой статье.
Какую задачу будем решать
Мы пишем ПО на языке С++, в среде Visual Studio 2015. У нас в проекте, естественно, есть пользовательские типы данных. В качестве примера таких типов могу привести класс MbSolid. Этот класс входит в состав математического ядра C3D и является абстракцией твердого тела. Тело описывается гранями, грани какими-то поверхностями и т.д. Т.е. структура класса довольно сложная, и в процессе отладки собственных алгоритмов хотелось бы визуально прикинуть, какое тело получилось на данный момент.
Картинка из прошлой статьи. В качестве примера пользовательского класса там используется класс отрезка прямой.
Для решения этой задачи было написано расширение для VisualStudio. Тут ничего интересного, ссылки есть в прошлой статье. Но возникла проблема — как в расширении VisualStudio получать данные из адресного пространства отлаживаемого (другого) процесса?
В случае простых типов данных, например массивов, всё не так уж сложно, и Microsoft даже подготовили пример для std::vector. А в нашем случае MbSolid имеет внушительную иерархию наследования и большое количество полей данных.
Модификация пользовательских типов
В прошлой статье был предложен инвазивный вариант решения этой проблемы. В каждый пользовательский класс (который мы хотим визуализировать во время отладки) добавляется поле-маркер. И в каждый не константный метод класса добавляется код для сериализации данных класса в shared memory. В поле-маркере сохраняется адрес в shared memory, куда мы сохранили данные класса. В момент отладки, при просмотре содержимого интересующей нас переменной, расширение VisualStudio находит поле-маркер и десериализует данные из shared memory, ну а дальше как-то визуализирует полученные данные.
По очевидным причинам данное решение не применимо на практике. Особенно если нет доступа к исходному тексту классов, которые мы хотим отлаживать таким способом. Ничего лучше придумать на тот момент не удалось, и эта тема заглохла на несколько лет.
Сервер в пользовательском процессе
И вот недавно пришла идея написать простой сервер, который будет жить в пользовательском процессе в отдельном потоке и отвечать на запросы, пришедшие от нашего расширения VisualStudio. Для сервера за основу был взят проект Microsoft C++ REST SDK. Этот проект позволил быстро написать свой http-сервер, который получает GET-запросы и возвращает описание экземпляра пользовательского класса в json-формате. Напомню, нас интересует визуальное представление экземпляров класса MbSolid (твердые тела).
В запросе к серверу передается адрес переменной в адресном пространстве отлаживаемого процесса. Т.к. сервер живет в том же процессе, то он без проблем получает доступ к данным по запрашиваемому адресу. Сервер, получив адрес экземпляра класса, приводит этот указатель к типу MbSolid*. Далее сервер создает аппроксимацию этого тела в виде полигональной сетки. Сериализует вычисленные вершины и индексы треугольников в json и отправляет ответ. На стороне VisualStudio расширение получает ответ, десериализует данные и отрисовывает полученную полигональную сетку в окне VisualStudio.
В результате расширению в VisualStudio даже не нужно знать структуру пользовательских данных, ему достаточно уметь отправлять правильные GET-запросы, десериализовать ответ и отрисовать треугольники в окне VisualStudio. Сервер можно расширять. Таким способом можно отлаживать любые пользовательские классы, которые можно представить в виде полигональной сетки или набора отрезков прямых, а расширение VisualStudio сможет их визуализировать:
Более того, таким способом можно даже отправлять запросы нашему серверу из браузера и визуализировать данные процесса с помощью WebGL.
Сделал простой демонстрационный пример. Запускаем наше приложение. Открываем страницу примера в браузере, на странице вводим адрес переменной, отправляем запрос к серверу и отрисовываем ответ. Не знаю, зачем это может понадобиться, но штука прикольная
Оживляем сервер
Все бы хорошо. Но есть одна проблема. Когда срабатывает точка останова, студия останавливает все потоки пользовательского процесса. В результате наш сервер тоже останавливается и не может отвечать на запросы. Для обхода этой проблемы используем следующий костыль: текущий поток, в котором сработала точка останова, замораживается, и пользовательский процесс запускается. В этот момент наш сервер оживает и расширение отправляет ему запрос с адресом интересующей нас переменной. После получения ответа выполнение пользовательского процесса снова приостанавливается, и в качестве текущего потока для дебагера устанавливается изначальный поток, в котором сработала точка останова. Для пользователя это выглядит так, будто ничего не произошло и выполнение программы остановилось в точке останова.
В коде расширения VisualStudio этот костыль выглядит следующим образом. Сработала точка останова. Пользователь запрашивает данные интересующей его переменной. В этот момент мы замораживаем текущий поток и запускаем дебагер:
if (dte.Debugger.CurrentMode != EnvDTE.dbgDebugMode.dbgBreakMode)
return;
currentThread = dte.Debugger.CurrentThread;
currentThread.Freeze();
dte.Debugger.Go(false);
Посылаем запрос серверу. Получаем ответ. Останавливаем процесс, размораживаем наш поток:
if (dte.Debugger.CurrentMode == EnvDTE.dbgDebugMode.dbgBreakMode)
return;
dte.Debugger.Break();
if (currentThread != null)
{
currentThread.Thaw();
dte.Debugger.CurrentThread = currentThread;
}
Всё! Мы получили данные от сервера и отрисовали их в окне VisualStudio. Выполнение программы находится в изначальной точке останова.
В завершение хочу отметить, что пока мне непонятны побочные эффекты, которые порождает такой подход. Буду рад, если вы поделитесь вашими соображениями в комментариях к этой статье.
Автор: ershovdz