Я занимаюсь разработкой для встраиваемых систем (в основном, под STM32 и Миландр), в качестве основной среды я использую uVision Keil. И, поскольку пишу я на С и С++, уже долгое время меня мучает вопрос – правильно ли я пишу код? Можно ли так?
Эта статья была опубликована на habr.com. Перепечатка и перевод сделаны с разрешения автора.
Не, он, конечно, компилируется, но это же С++, язык, где "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 (ожидаемо) ловит весь вывод в stout и stderr для пользовательских скриптов, правда кириллицу показывать отказывается напрочь, поэтому ошибки в этих скриптах превращаются в нечитаемые козябры. Я это быстро подкостылил с помощью смены кодовой страницы на какую-то англоязычную, чтобы ошибки выдавались на английском.
Окей, пройдемся по шагам.
Благодаря удачному совпадению (а может быть и не совпадению, а разработчики PVS-Studio сделали это специально?), формат строк с предупреждениями совпадает с форматом, который использует Keil, поэтому переход к проблемной строке по двойному клику просто заработал.
Казалось бы, пост на этом можно завершать?
К сожалению, нет.
Через некоторое время я заметил небольшую странность – пересобираю один и тот же проект без изменений, целиком, а результаты анализа PVS-Studio – разные! В них то пропадала, то появлялась ошибка в одном из файлов.
И началась эпическая переписка с техподдержкой, которая – исключительно по моей вине – растянулась почти на год (!). Вот честное слово — техподдержка у PVS-Studio – натурально лучшая из всех, с кем я общался, а общался я со многими, от российских производителей микросхем, где человек поздравлял меня с "днём пирожков с малиновым вареньем" (нет, это не шутка) до крупнейших зарубежных компаний, где меня месяцами футболили от человека к человеку :)
Тут же я со стыдом признаюсь, что отвечал существенно медленнее, чем отвечали мне... частично меня оправдывает необходимость заниматься основными рабочими задачами, но только частично.
Энивей, проблема оказалось достаточно проста – мониторинг запусков компилятора не волшебный. Если компилятор слишком быстро скомпилировал файл, то факт запуска может быть пропущен. Разумеется, слишком быстро – это понятие относительное, на это влияет множество параметров окружения, количество уже запущенных сторонних процессов и тому подобное, но судя по всему, ключевым фактором является распараллеливание сборки. Если включена параллельная сборка, то шансы пропустить какой-нибудь запуск достаточно велики. Если она выключена, то пропусков – по крайней мере, на доступных мне машинах и на нескольких проектах – не наблюдалось.
Окей. Что же с этим делать?
Вариант "в лоб" — выключить параллельную сборку (ну или выключать ее иногда, для анализа). Плохой вариант, потому что:
Едем дальше. Мне довольно быстро показалось, что использовать мониторинг вообще как-то глупо, ведь в файле проекта содержится вся нужная информация – какие файлы будут скомпилированы, с какими ключами и тому подобным. Почему бы просто не парсить этот файл?
Но этот вариант хорош только на бумаге. С одной стороны, непонятно, кто должен этим парсингом заниматься? Конечно, мы купили лицензию, но это не значит, что представителей PVS-Studio можно теперь бесконечно эксплуатировать. Для них мы со своим Keil'ом явно не в приоритете, интегрироваться в каждую встречную-поперечную среду экономически нецелесообразно, собственно, поэтому и предлагается универсальное решение с мониторингом.
К тому же формат проекта хоть и представляет собой по сути xml, является закрытым, а значит, может в любой момент измениться кардинальным и непредсказуемым образом по желанию левой пятки вендора.
Плюс, если я правильно понял, для запуска анализа информации, содержащейся только в файле проекта, все-таки недостаточно.
В Keil'е есть странная функция, которой я так и не смог найти применения – создание batch-файла сборки. В этот файл попадает вся необходимая PVS-Studio информация и включается этот файл одной галкой!
К сожалению, эта галка заодно ломает инкрементальную сборку, то есть любая компиляция становится полной перекомпиляцией. Это опять-таки очень печально сказывается на времени сборки, поэтому этот вариант мы тоже с грустью отметаем.
Мониторинг не успевает отловить запуск компилятора? Так давайте заставим его компилировать подольше!
С помощью флага ‑‑preinclude это принудительно инклудилось в каждый.срр-файл в проекте.
Эти варианты отметаются, поскольку замедляют компиляцию (а еще, потому что это наркомания).
Остались два варианта, которыми мы в итоге и пользуемся. Оба обладают плюсами и минусами, поэтому идеальными я их назвать не могу, но, как говорится, лучшее – враг хорошего.
Первый вариант – не мониторить компиляцию каждый раз. Вполне достаточно ведь получить набор файлов, которые компилируются. Меняется этот набор не так уж часто – только когда в проект добавляют новые файлы (ну, или убирают старые).
Таким образом, этот вариант делится на две стадии:
Как детектировать изменение списка файлов? Наверняка можно разными способами, я же пошел первым пришедшим мне на ум путем – через git, поскольку все равно все проекты должны гитоваться.
Если файл проекта менялся с последнего коммита, значит, добавлялись файлы!
Но в файле проекта может меняться много чего, там ведь и опции компиляции и черт знает что еще, поэтому я накидал вот такой однострочник:
was_changed=$(git diff *.uvproj* | grep "[+,-]\s*<FileName>" \
| sed -e 's#</*FileName>##g')
Помните, я чуть раньше говорил, как нехорошо парсить закрытый и недокументированный формат? Так вот, забудьте :D
Ну или можно натурально просто палить все изменения в файле проекта, не вникая в их суть; это будет давать больше ложноположительных срабатываний, но не ложноотрицательных.
Окей, мы поняли, что набор файлов поменялся – как запускать мониторинг?
А вот тут я не придумал ничего лучше, как выдать пользователю предупреждение и потребовать некоторых ручных манипуляций:
Минусы этого подхода:
Всякие не очень интересные подробности:
Собственно собирание дампа делается вот так:
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, поэтому переход по двойному клику на предупреждение просто работает :)
Поскольку ручные манипуляции – это все-таки довольно грустно, после перебрасывания идеями, команда PVS-Studio разработала для нас специальную утилиту и несколько скриптов.
Основная идея состоит в следующем:
Собственно основной скрипт достаточно объемный и я его здесь целиком приводить не буду (но вот он на гитхабе); к тому же, его мне предоставили представители PVS-Studio :) Я немножко подправил его, в частности, убрал необходимость вручную указывать путь до папки Keil'a.
Соответственно, вызовы в данном случае выглядят так:
Здесь "Target 1" — имя вашего текущего таргета, должно быть в кавычках
Буквы с решетками – Keil называет это "Key Sequence" — по сути это build variables, переменные окружения для сборки.
Плюсы по сравнению с предыдущим:
Минусы:
Это минус только по сравнению с предыдущим вариантом, где это название указывать не нужно вообще :) К счастью, делать это нужно только при создании конфигурации – но вручную.
Убрать эти надписи у меня не получилось :(
А поскольку в Keil нет нормального окна "Errors", как в большинстве других IDE, окно Build Output приходится читать постоянно, фильтровать его невозможно; соответственно, эти надписи мешают визуально находить ошибки компиляции и предупреждения от компилятора, особенно если в проекте много файлов.
К счастью, скрипт перед компиляцией каждого файла можно запускать не каждый раз, а только если набор компилируемых файлов менялся. Но это опять возвращает нас к необходимости ставить и снимать галки вручную! И, фактически, сводит этот вариант к предыдущему — только тут все галки в одном месте, а не в двух.
Короче говоря, идеальной интеграции не получилось, но даже так уже существенно лучше, чем вообще никак.
Раз уж зашла речь об интеграции, для полноты картины следует упомянуть разные способы подавления предупреждений. В принципе, на сайте PVS-Studio все расписано, поэтому я постараюсь не растягивать мысль. Некоторые варианты я пропущу, поскольку сам не пользуюсь.
Итак, подавлять предупреждения можно на нескольких уровнях:
Способ подавления |
Когда нужно |
---|---|
Одно конкретное предупреждение для одной конкретной строчки |
Если вы точно знаете, что это не ошибка |
Все предупреждения для какой-нибудь папки только в текущем проекте |
Если это подключенная библиотека внутри папки с проектом |
Класс предупреждений для текущего проекта |
Если этот класс анализов все равно не работает |
Конкретные виды предупреждений в текущем проекте |
Для предупреждений, которые обычно не соответствуют реальным ошибкам, но лезут постоянно |
Конкретные папки на всем компе |
Для постоянно используемых библиотек, которые не живут внутри папки с проектом |
Поскольку в embedded есть реальная возможность делать каждый проект самодостаточным (т.е. не зависящим от каких-либо внешних библиотек, просто клонишь его и он компилируется), эту самодостаточность хочется сохранять. Поэтому все скрипты для анализа и файлы для подавления предупреждений тоже нужно будет хранить внутри папки проекта (разумеется, саму PVS-Studio придется ставить отдельно).
До нужной строчки или справа от нее пишется комментарий вида
// -V::XXX - Пояснение, почему варнинг подавлен
Здесь ХХХ — это номер варнинга.
Пояснение, на мой взгляд, крайне важно писать; иначе совершенно непонятно, почему варнинг подавлен – потому что он ложный или потому, что он просто раздражал программиста, который не смог понять, в чем проблема.
Это делается с помощью xml-файла (который я обычно называю pvs_settings.xml). Этот файл путешествует вместе с проектом.
Пример:
<?xml version="1.0" encoding="utf-8"?>
<ApplicationSettings xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- Import settings (mostly exclude paths) from global settings -->
<AutoSettingsImport>true</AutoSettingsImport>
<PathMasks>
<!-- Exclude this paths from analysis -->
<string>\cmsis\</string>
<string>\spl\</string>
</PathMasks>
<!-- Disable 64-bit errors -->
<Disable64BitAnalysis>true</Disable64BitAnalysis>
</ApplicationSettings>
Это делается с помощью файла 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-файлом, в локальном файле должна быть строка <AutoSettingsImport>true</AutoSettingsImport>. PVS-Studio просто смотрит в папку %APPDATA%\PVS-Studio\SettingsImports и применяет все файлы оттуда подряд.
Пример:
<?xml version="1.0" encoding="utf-8"?>
<ApplicationSettings xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<PathMasks>
<!-- Exclude this paths from analysis -->
<string>\boost\</string>
<string>\zlib\</string>
<string>\png\</string>
<string>\libpng\</string>
<string>\pnglib\</string>
<string>\freetype\</string>
<string>\ImageMagick\</string>
<string>\jpeglib\</string>
<string>\libxml\</string>
<string>\libxslt\</string>
<string>\tifflib\</string>
<string>\wxWidgets\</string>
<string>\libtiff\</string>
<string>\mesa\</string>
<string>\cximage\</string>
<string>\bzip2\</string>
</PathMasks>
</ApplicationSettings>
Интегрировать PVS-Studio в Кейл можно, хотя все полученные решения не отличаются элегантностью и требует некоторых ручных манипуляций.
Я пользуюсь ей уже несколько лет и в целом доволен, потому что чувствую себя в большей безопасности от собственной глупости :)
Отмечу, что подсчитать пользу от постоянного анализа достаточно сложно – ведь ошибка исправляется практически сразу, как только анализатор выдает соответствующее предупреждение. Оценить, сколько времени бы я эту проблему искал сам, без PVS-Studio, достаточно сложно.
Так же стоит отметить, что анализатор все-таки замедляет сборку, поэтому иногда я его таки отключаю – как правило, во время неистовой отладки, когда приходится постоянно редактировать какой-нибудь коэффициент в одной строчке.
Отдельные вопросы, которые стоило задать себе перед тем, как начинать этот процесс:
И, разумеется, примеры на гитхабе.