Интеграция PVS-Studio в uVision Keil
Я занимаюсь разработкой для встраиваемых систем (в основном, под STM32 и Миландр), в качестве основной среды я использую uVision Keil. И, поскольку пишу я на С и С++, уже долгое время меня мучает вопрос — правильно ли я пишу код? Можно ли так?
Не, он конечно компилируется, но это же С++, язык, где «program is ill-formed, no diagnostic required» — это норма.
Соответственно, на протяжении нескольких лет я донимал руководство просьбами купить нам лицензию PVS-Studio и, наконец, когда моя просьба неожиданно совпала с моментом, когда нужно было срочно потратить выделенные на закупку ПО деньги, нам ее все-таки купили!
Радости моей с одной стороны не было предела, но с другой оказалось, что не все так хорошо; сходу PVS-Studio встраивается только в Visual Studio (что порадовало отдел разработки под десктопы) и продукты от Jetbrains (CLion, Rider, Idea, Android Studio), для некоторых других систем сборки тоже предусмотрены готовые сценарии, а вот для Keil«a заявлена только поддержка компилятора — и все. А значит, нужно заниматься интеграцией. Кто будет этим заниматься? Ну, мне же больше всех надо…
Поскольку я не придумал отдельный кусок текста до ката, я просто повторю его тут в качестве введения :)
Я занимаюсь разработкой для встраиваемых систем (в основном, под STM32 и Миландр), в качестве основной среды я использую uVision Keil. И, поскольку пишу я на С и С++, уже долгое время меня мучает вопрос — правильно ли я пишу код? Можно ли так?
Не, он конечно компилируется, но это же С++, язык, где «program is ill-formed, no diagnostic required» — это норма.
Соответственно, на протяжении нескольких лет я донимал руководство просьбами купить нам лицензию PVS-Studio и, наконец, когда моя просьба неожиданно совпала с моментом, когда нужно было срочно потратить выделенные на закупку ПО деньги, нам ее все-таки купили!
Радости моей с одной стороны не было предела, но с другой оказалось, что не все так хорошо; сходу PVS-Studio встраивается только в Visual Studio (что порадовало отдел разработки под десктопы) и продукты от Jetbrains (CLion, Rider, Idea, Android Studio), для некоторых других систем сборки тоже предусмотрены готовые сценарии, а вот для Keil«a заявлена только поддержка компилятора — и все. А значит, нужно заниматься интеграцией. Кто будет этим заниматься? Ну, мне же больше всех надо…
Разумеется, рабочие задачи при этом с меня не спали, поэтому процесс затягивался довольно сильно. Поначалу я, вопреки всем рекомендациям, занимался проверками «только по праздникам», без всякой автоматизации по самому универсальному сценарию — запускать PVS-Studio Standalone, нажимать «Мониторить запуск компилятора», компилировать проект и читать результаты анализа.
Так продолжалось до тех пор, пока однажды я не потратил 3 дня на отладку очень неприятного бага, который отличался совершенно дикими проявлениями в случайные моменты времени. Баг оказался банальным чтением по нулевому указателю (которое на микроконтроллерах зачастую не приводит ни к каким мгновенным ошибкам типа Access Violation).
Быстренько убедившись, что PVS-Studio находит этот баг, я понял, что хватит это терпеть! — и решил-таки заняться интеграцией с Keil«ом.
И давайте сразу же определимся, что я понимаю под интеграцией:
- анализ запускается автоматически, после нажатия на кнопку «скомпилировать»
- результаты анализа выдаются автоматически, желательно в то же окно, что и обычные ошибки компиляции
- двойной клик на ошибку или предупреждение должен автоматически перемещать нас к проблемному месту
Как вы узнаете к концу статьи, в принципе, более-менее получилось –, но с нюансами :)
Keil, насколько мне известно, не предусматривает никаких «нормальных» способов кастомизации типа плагинов или расширений, поэтому единственный способ встроиться в сборку — это Custom Build Steps, которые в Keil«e называются «User Scripts».
В опциях проекта во вкладке Users предусмотрена возможность запуска сторонних программ (только .bat или .exe, даже .cmd нет!) по трем событиям:
- перед сборкой всего проекта
- перед компиляцией каждого файла
- после сборки всего проекта
Вроде бы, первого и последнего должно быть достаточно. План вырисовывается вроде бы несложный:
- перед сборкой всего проекта нужно запустить мониторинг
- после сборки нужно мониторинг остановить
- запустить анализ
- вывести результаты в окно
Build Output
Быстрые эксперименты показали, что Build Output
(ожидаемо) ловит весь вывод в stout
и stderr
для пользовательских скриптов, правда кириллицу показывать отказывается напрочь, поэтому ошибки в этих скриптах превращаются в нечитаемые козябры. Я это быстро подкостылил с помощью смены кодовой страницы на какую-то англоязычную, чтобы ошибки выдавались на английском.
Окей, пройдемся по шагам.
- Запустить мониторинг можно с помощью консольной утилиты
CLMonitor
- После того, как сборка завершена, запускаем анализ и сохраняем его результат в формате обычного текста.
- А потом выводим результаты просто с помощью
more
. - И вуаля, вроде бы все работает!
Благодаря удачному совпадению (а может быть и не совпадению, а разработчики PVS-Studio сделали это специально?), формат строк с предупреждениями совпадает с форматом, который использует Keil, поэтому переход к проблемной строке по двойному клику просто заработал.
Казалось бы, пост на этом можно завершать?
К сожалению, нет.
Через некоторое время я заметил небольшую странность — пересобираю один и тот же проект без изменений, целиком, а результаты анализа PVS-Studio — разные! В них то пропадала, то появлялась ошибка в одном из файлов.
И началась эпическая переписка с техподдержкой, которая — исключительно по моей вине — растянулась почти на год (!). Вот честное слово — техподдержка у PVS-Studio — натурально лучшая из всех, с кем я общался, а общался я со многими, от российских производителей микросхем, где человек поздравлял меня с «днём пирожков с малиновым вареньем» (нет, это не шутка) до крупнейших зарубежных компаний, где меня месяцами футболили от человека к человеку :)
Тут же я со стыдом признаюсь, что отвечал существенно медленнее, чем отвечали мне… частично меня оправдывает необходимость заниматься основными рабочими задачами, но только частично.
Энивей, проблема оказалось достаточно проста — мониторинг запусков компилятора не волшебный. Если компилятор слишком быстро скомпилировал файл, то факт запуска может быть пропущен. Разумеется, слишком быстро — это понятие относительное, на это влияет множество параметров окружения, количество уже запущенных сторонних процессов и тому подобное, но судя по всему, ключевым фактором является распараллеливание сборки. Если включена параллельная сборка, то шансы пропустить какой-нибудь запуск достаточно велики. Если она выключена, то пропусков — по крайней мере, на доступных мне машинах и на нескольких проектах — не наблюдалось.
Окей. Что же с этим делать?
Решение «в лоб»
Вариант «в лоб» — выключить параллельную сборку (ну или выключать ее иногда, для анализа). Плохой вариант, потому что:
- В Keil«e это делается глобально, не для каждого проекта в отдельности; т.е. замедлена будет сборка всех проектов вообще
- Замедление сборки весьма ощутимое; конечно, кому-то время в 1.5–2 минуты кажется не очень большим, но все же это достаточно неприятно, успеваешь отвлечься и потерять фокус
Если же параллельную сборку выключать только иногда, то мы по сути возвращаемся к сценарию «проверки только по праздникам», которого хотим избежать.
Парсинг файл проекта
Едем дальше. Мне довольно быстро показалось, что использовать мониторинг вообще как-то глупо, ведь в файле проекта содержится вся нужная информация — какие файлы будут скомпилированы, с какими ключами и тому подобным. Почему бы просто не парсить этот файл?
Но этот вариант хорош только на бумаге. С одной стороны, непонятно, кто должен этим парсингом заниматься? Конечно, мы купили лицензию, но это не значит, что представителей PVS-Studio можно теперь бесконечно эксплуатировать. Для них мы со своим Keil«ом явно не в приоритете, интегрироваться в каждую встречную-поперечную среду экономически нецелесообразно, собственно, поэтому и предлагается универсальное решение с мониторингом.
К тому же формат проекта хоть и представляет собой по сути xml, является закрытым, а значит, может в любой момент измениться кардинальным и непредсказуемым образом по желанию левой пятки вендора.
Плюс, если я правильно понял, для запуска анализа информации, содержащейся только в файле проекта, все-таки недостаточно.
Batch-файл
В Keil«е есть странная функция, которой я так и не смог найти применения — создание batch-файла сборки. В этот файл попадает вся необходимая PVS-Studio информация и включается этот файл одной галкой!
К сожалению, эта галка заодно ломает инкрементальную сборку, то есть любая компиляция становится полной перекомпиляцией. Это опять-таки очень печально сказывается на времени сборки, поэтому этот вариант мы тоже с грустью отметаем.
Замедлитель компиляции
Мониторинг не успевает отловить запуск компилятора? Так давайте заставим его компилировать подольше!
- Можно просто запускать Process Explorer вместе с Keil«ом. Но не очень понятно, насколько это помогает и почему.
- Поскольку один мой коллега упарывался по шаблонам, я попросил его накидать что-нибудь, что хорошенько грузило компилятор, не оказывая никакого влияния на бинарный файл; он мне предложил свое шаблонное вычисление табличного синуса, которое я, пожалуй не рискну выкладывать на всеобщее обозрение дабы не шокировать почтенную публику (и потому что код не я писал :)
С помощью флага --preinclude
это принудительно инклудилось в каждый.срр-файл в проекте.
Эти варианты отметаются, поскольку замедляют компиляцию (а еще, потому что это наркомания).
Остались два варианта, которыми мы в итоге и пользуемся. Оба обладают плюсами и минусами, поэтому идеальными я их назвать не могу, но, как говорится, лучшее — враг хорошего.
Дамп
Первый вариант — не мониторить компиляцию каждый раз. Вполне достаточно ведь получить набор файлов, которые компилируются. Меняется этот набор не так уж часто — только когда в проект добавляют новые файлы (ну, или убирают старые).
Таким образом, этот вариант делится на две стадии:
- как-то детектировать, что набор файлов в проекте поменялся; в таком случае запускать мониторинг и сохранять результат мониторинга (не анализа)
- если набор файлов не менялся, то просто запускать анализ по сохраненному результату
Как детектировать изменение списка файлов? Наверняка можно разными способами, я же пошел первым пришедшим мне на ум путем — через git, поскольку все равно все проекты должны гитоваться.
Если файл проекта менялся с последнего коммита, значит, добавлялись файлы!
Но в файле проекта может меняться много чего, там ведь и опции компиляции и черт знает что еще, поэтому я накидал вот такой однострочник:
was_changed=$(git diff *.uvproj* | grep "[+,-]\s*
Помните, я чуть раньше говорил, как нехорошо парсить закрытый и недокументированный формат? Так вот, забудьте: D
Ну или можно натурально просто палить все изменения в файле проекта, не вникая в их суть; это будет давать больше ложноположительных срабатываний, но не ложноотрицательных.
Окей, мы поняли, что набор файлов поменялся — как запускать мониторинг?
А вот тут я не придумал ничего лучше, как выдать пользователю предупреждение и потребовать некоторых ручных манипуляций:
- Выключить параллельную сборку (зайти в
Edit->Configuration->Other
и поставить галкуDisable parallel build
) - Сменить «обычные» скрипты на «мониторящие» — снять и поставить еще две галки в
Options->User
- Выполнить полную пересборку проекта
- Вернуть галки обратно
Минусы этого подхода:
Необходимость ручных манипуляций при изменении набора файлов. В принципе, файлы в проект добавляются не так уж часто, но все равно неприятно.
Тут мы неявно надеемся, что выключения параллельной сборки будет достаточно для безошибочного мониторинга.
Если в проекте несколько конфигураций сборки (в Keil это называется «Targets»), то при переключении может быть нужно перегенирировать дамп — если в конфигурациях участвуют разные файлы, разные ключи компиляции, активны разные дефайны и т.д. И вот за этим приходится следить самостоятельно, к сожалению, из Кейла автоматически имя текущей конфигурации никак не вытащить (ну, или я не смог найти как).
- Для проверки измененности нужен git, bash и sed — к счастью, все это входит в комплект поставки git for Windows;, но ограничивает применимость скрипта. Плюс, чтобы проверять измененность файлов через git, нужно, чтобы проект лежал в репозитории; проверить просто произвольную папку не получится.
- Поскольку Keil умеет вызывать только
.bat
и.exe
, приходится оборачивать вызов shell-скрипта в батник. - Git может быть установлен где попало, но может быть и прописан в Path. Чтобы закрыть оба случая я придумал такой немножко упоротый вариант:
"%GIT_BASH_PATH%bash.exe"
— если пукть к bash.exe прописан вPath
, то это просто прокатит, но опционально можно создать переменную окруженияGIT_BASH_PATH
и глобальный Path не засорять. Только вGIT_BASH_PATH
нужно будет поставить слеш в конце пути. - С PVS-Studio та же история
- Если проект не скомпилировался, то clmonitor может остаться запущенным, нужно не забывать его прибивать при запуске компиляции. Это означает, что компилировать сразу два проекта со сбором дампа — нельзя. Впрочем, вроде не очень-то и хочется.
Собственно собирание дампа делается вот так:
CLMonitor.exe saveDump -d "путь_к_дампу\pvs_dump.zip"
Когда дамп уже есть, анализ запускается вот так:
CLMonitor.exe analyzeFromDump -d "путь_к_дампу\pvs_dump.zip"
-l "путь_к_результату\pvs.plog"
-t "путь_к_конфигу\pvs_settings.xml"
-c "путь_к_конфигу\ignore_warnings.pvsconfig"
PlogConverter.exe "путь_к_результату\pvs.plog" --renderTypes=Txt -o "путь_к_результату"
more "путь_к_результату\pvs.plog.txt"
Конфиги pvs_settings.xml
и ignore_warnings.pvsconfig
позволяют подавлять предупреждения, о них подробнее позже.
Собственно вся суть этих действий в том, чтобы из дампа получить результат, отрендерить его в простой текст и вывести текстовый файл в терминал. Как уже говорилось, формат вывода совпадает с ожидаемым для Keil«a, поэтому переход по двойному клику на предупреждение просто работает :)
Утилита CLMonitorDumpFilter
Поскольку ручные манипуляции — это все-таки довольно грустно, после перебрасывания идеями, команда PVS-Studio разработала для нас специальную утилиту и несколько скриптов.
Основная идея состоит в следующем:
- Запуск скрипта перед сборкой (с одним набором параметров), чтобы сформировать дамп окружения, ключей компиляции и т.д. Этот запуск создает копию файла проекта, включает в нем формирование Batch-файла, собирает этот проект, анализирует batch-файл и удаляет копию.
- Запуск скрипта перед компиляцией каждого файла вместо мониторинга запусков компилятора.
- Запуск скрипта (с другим параметром) после сборки проекта, чтобы выполнить анализ и вывести результат.
Собственно основной скрипт достаточно объемный и я его здесь целиком приводить не буду (но вот он на гитхабе); к тому же, его мне предоставили представители PVS-Studio:) Я немножко подправил его, в частности, убрал необходимость вручную указывать путь до папки Keil«a.
Соответственно, вызовы в данном случае выглядят так:
Здесь "Target 1"
— имя вашего текущего таргета, должно быть в кавычках
- After Build
.\scripts\_after_build.bat #X #P
Буквы с решетками — Keil называет это «Key Sequence» — по сути это build variables, переменные окружения для сборки.
#X
— это путь до папки Keil«a,#E
— путь до текущего файла#P
— путь до файла проекта
Плюсы по сравнению с предыдущим:
- Никаких обязательных регулярных ручных действий, нужно только расставить несколько переменных окружения.
- Нет неявной надежды на безошибочный мониторинг, каждый запуск компилятора «перехватывается» скриптом
Минусы:
- В данный момент не поддерживается ARM Compiler version 6 (т.е. armclang)
- Название текущей конфигурации нужно указывать вручную в строке вызова скрипта.
Это минус только по сравнению с предыдущим вариантом, где это название указывать не нужно вообще:) К счастью, делать это нужно только при создании конфигурации –, но вручную.
- Окно Build Output замусоривается
Убрать эти надписи у меня не получилось :(
А поскольку в Keil нет нормального окна «Errors», как в большинстве других IDE, окно Build Output приходится читать постоянно, фильтровать его невозможно; соответственно, эти надписи мешают визуально находить ошибки компиляции и предупреждения от компилятора, особенно если в проекте много файлов.
- Поскольку спецутилита трогает файл проекта, после компиляции Keil решает, что файл проекта изменился, и предлагает проект перезагрузить. Если вы согласитесь, то все надписи из Build Output (в т.ч. результаты анализа) пропадут.
К счастью, скрипт перед компиляцией каждого файла можно запускать не каждый раз, а только если набор компилируемых файлов менялся. Но это опять возвращает нас к необходимости ставить и снимать галки вручную! И, фактически, сводит этот вариант к предыдущему — только тут все галки в одном месте, а не в двух.
Короче говоря, идеальной интеграции не получилось, но даже так уже существенно лучше, чем вообще никак.
Раз уж зашла речь об интеграции, для полноты картины следует упомянуть разные способы подавления предупреждений. В принципе, на сайте PVS Studio все расписано, поэтому я постараюсь не растягивать мысль. Некоторые варианты я пропущу, поскольку сам не пользуюсь.
Итак, подавлять предупреждения можно на нескольких уровнях:
Поскольку в embedded есть реальная возможность делать каждый проект самодостаточным (т.е. не зависящим от каких-либо внешних библиотек, просто клонишь его и он компилируется), эту самодостаточность хочется сохранять. Поэтому все скрипты для анализа и файлы для подавления предупреждений тоже нужно будет хранить внутри папки проекта (разумеется, саму PVS-Studio придется ставить отдельно).
Один конкретный варнинг для одной конкретной строчки
До нужной строчки или справа от нее пишется комментарий вида
// -V::XXX - Пояснение, почему варнинг подавлен
Здесь ХХХ — это номер варнинга.
Пояснение, на мой взгляд, крайне важно писать; иначе совершенно непонятно, почему варнинг подавлен — потому что он ложный или потому, что он просто раздражал программиста, который не смог понять, в чем проблема.
Все варнинги для какой-нибудь папки только в текущем проекте и классы предупреждений для текущего проекта
Это делается с помощью xml-файла
(который я обычно называю pvs_settings.xml). Этот файл путешествует вместе с проектом.
true
\cmsis\
\spl\
true
Конкретные варнинги в текущем проекте
Это делается с помощью файла ignore_warnings.pvsconfig. Этот файл тоже путешествует вместе с проектом. Разумеется, пояснения о причинах игнорирования приветствуются!
###### Common warnings
# ignore 64-bit warnings
// -V::4
# allow C-style cast for primitive integer types (and void)
// -V:int:2005
// -V:char:2005
// -V:short:2005
// -V:uint8_t:2005
// -V:int8_t:2005
// -V:uint16_t:2005
// -V:int16_t:2005
// -V:uint32_t:2005
// -V:int32_t:2005
// -V:uint64_t:2005
// -V:int64_t:2005
// -V:void:2005
# ignore 'The body of the statement should be enclosed in braces'; that doesn't look like a source of errors for us
// -V::2507
###### MISRA
# ignore MISRA C++ 6-6-5 'A function should have a single point of exit at the end.'
# this goes againts our best practises and generally seems outdated
// -V::2506
Конкретные папки на всем компе
Это делается с помощью xml-файлов в папке текущего пользователя. Чтобы они применялись вместе с локальным xml-файлом, в локальном файле должна быть строка
. PVS-Studio просто смотрит в папку %APPDATA%\PVS-Studio\SettingsImports
и применяет все файлы оттуда подряд.
\boost\
\zlib\
\png\
\libpng\
\pnglib\
\freetype\
\ImageMagick\
\jpeglib\
\libxml\
\libxslt\
\tifflib\
\wxWidgets\
\libtiff\
\mesa\
\cximage\
\bzip2\
Интегрировать PVS-Studio в Кейл можно, хотя все полученные решения не отличаются элегантностью и требует некоторых ручных манипуляций.
Я пользуюсь ей уже несколько лет и в целом доволен, потому что чувствую себя в большей безопасности от собственной глупости :)
Отмечу, что подсчитать пользу от постоянного анализа достаточно сложно — ведь ошибка исправляется практически сразу, как только анализатор выдает соответствующее предупреждение. Оценить, сколько времени бы я эту проблему искал сам, без PVS-Studio, достаточно сложно.
Так же стоит отметить, что анализатор все-таки замедляет сборку, поэтому иногда я его таки отключаю — как правило, во время неистовой отладки, когда приходится постоянно редактировать какой-нибудь коэффициент в одной строчке.
Отдельные вопросы, которые стоило задать себе перед тем, как начинать этот процесс:
- Не проще ли было интегрироваться в Eclipse?
- Не проще ли было встроиться в CI, а не в IDE?
- Может быть стоило выработать рефлекс «есть баг — сегодня праздник запусти PVS, а сам думай потом».
И, разумеется, примеры на гитхабе.
P.S. Если кто-нибудь знает, как теперь на хабре делать работающее оглавление, подскажите, пожалуйста, я пост поправлю.