﻿# Интеграция PVS\-Studio в uVision Keil

Я занимаюсь разработкой для встраиваемых систем \(в основном, под STM32 и Миландр\), в качестве основной среды я использую uVision Keil\. И, поскольку пишу я на С и С\+\+, уже долгое время меня мучает вопрос – правильно ли я пишу код? Можно ли так?

![0847_Integrating_PVS-Studio_into_uVision_Keil_ru/image2.png](https://import.viva64.com/docx/blog/0847_Integrating_PVS-Studio_into_uVision_Keil_ru/image2.png)

_Эта статья была [опубликована](https://habr.com/ru/post/568474/) на 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 нет\!\) по трем событиям:

* перед сборкой всего проекта
* перед компиляцией каждого файла
* после сборки всего проекта

Вроде бы, первого и последнего должно быть достаточно\. План вырисовывается вроде бы несложный:

1. перед сборкой всего проекта нужно запустить мониторинг
1. после сборки нужно мониторинг остановить
1. запустить анализ
1. вывести результаты в окно _Build Output_

Быстрые эксперименты показали, что _Build Output_ \(ожидаемо\) ловит весь вывод в _stout_ и _stderr_ для пользовательских скриптов, правда кириллицу показывать отказывается напрочь, поэтому ошибки в этих скриптах превращаются в нечитаемые козябры\. Я это быстро подкостылил с помощью смены кодовой страницы на какую\-то англоязычную, чтобы ошибки выдавались на английском\.

Окей, пройдемся по шагам\.

1. Запустить мониторинг можно с помощью консольной утилиты _CLMonitor_
1. После того, как сборка завершена, запускаем анализ и сохраняем его результат в формате обычного текста\.
1. А потом выводим результаты просто с помощью _more_\.
1. И вуаля, вроде бы все работает\!



Благодаря удачному совпадению \(а может быть и не совпадению, а разработчики 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, поскольку все равно все проекты должны гитоваться\.

Если файл проекта менялся с последнего коммита, значит, добавлялись файлы\!

Но в файле проекта может меняться много чего, там ведь и опции компиляции и черт знает что еще, поэтому я накидал вот такой однострочник:

```cpp
was_changed=$(git diff *.uvproj* | grep "[+,-]\s*<FileName>" \
                                 | sed -e 's#</*FileName>##g')
```

Помните, я чуть раньше говорил, как нехорошо парсить закрытый и недокументированный формат? Так вот, забудьте :D

Ну или можно натурально просто палить все изменения в файле проекта, не вникая в их суть; это будет давать больше ложноположительных срабатываний, но не ложноотрицательных\.

Окей, мы поняли, что набор файлов поменялся – как запускать мониторинг?

А вот тут я не придумал ничего лучше, как выдать пользователю предупреждение и потребовать некоторых ручных манипуляций:

1. Выключить параллельную сборку \(зайти в _Edit\-\>Configuration\-\>Other_ и поставить галку _Disable parallel build_\)
1. Сменить "обычные" скрипты на "мониторящие" — снять и поставить еще две галки в _Options\-\>User_
1. Выполнить полную пересборку проекта
1. Вернуть галки обратно

Минусы этого подхода:

* Необходимость ручных манипуляций при изменении набора файлов\. В принципе, файлы в проект добавляются не так уж часто, но все равно неприятно\.
* Тут мы неявно надеемся, что выключения параллельной сборки будет достаточно для безошибочного мониторинга\.
* Если в проекте несколько конфигураций сборки \(в 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 может остаться запущенным, нужно не забывать его прибивать при запуске компиляции\. Это означает, что компилировать сразу два проекта со сбором дампа – нельзя\. Впрочем, вроде не очень\-то и хочется\.

Собственно собирание дампа делается вот так:

```cpp
CLMonitor.exe saveDump -d "путь_к_дампу\pvs_dump.zip"
```

Когда дамп уже есть, анализ запускается вот так:

```cpp
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 разработала для нас специальную утилиту и несколько скриптов\.

Основная идея состоит в следующем:

1. Запуск скрипта перед сборкой \(с одним набором параметров\), чтобы сформировать дамп окружения, ключей компиляции и т\.д\. Этот запуск создает копию файла проекта, включает в нем формирование Batch\-файла, собирает этот проект, анализирует batch\-файл и удаляет копию\.
1. Запуск скрипта перед компиляцией каждого файла вместо мониторинга запусков компилятора\.
1. Запуск скрипта \(с другим параметром\) после сборки проекта, чтобы выполнить анализ и вывести результат\.

Собственно основной скрипт достаточно объемный и я его здесь целиком приводить не буду \(но [вот он](https://github.com/Amomum/Keil_PVS_Example/blob/master/cl_monitor_dump_filter/CLMonitorDumpFilterLauncher_uVision.bat) на гитхабе\); к тому же, его мне предоставили представители PVS\-Studio :\) Я немножко подправил его, в частности, убрал необходимость вручную указывать путь до папки Keil'a\.

Соответственно, вызовы в данном случае выглядят так:

* Before Compile _\.\\scripts\\\_before\_compile\.bat \#X \#E_
* Before Build/Rebuild _\.\\scripts\\\_before\_build\_dump\.bat \#X \#P "Target 1"_

Здесь "_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 замусоривается

![0847_Integrating_PVS-Studio_into_uVision_Keil_ru/image4.png](https://import.viva64.com/docx/blog/0847_Integrating_PVS-Studio_into_uVision_Keil_ru/image4.png)

Убрать эти надписи у меня не получилось :\(

А поскольку в Keil нет нормального окна "Errors", как в большинстве других IDE, окно Build Output приходится читать постоянно, фильтровать его невозможно; соответственно, эти надписи мешают визуально находить ошибки компиляции и предупреждения от компилятора, особенно если в проекте много файлов\.

* Поскольку спецутилита трогает файл проекта, после компиляции Keil решает, что файл проекта изменился, и предлагает проект перезагрузить\. Если вы согласитесь, то все надписи из Build Output \(в т\.ч\. результаты анализа\) пропадут\.

К счастью, скрипт перед компиляцией каждого файла можно запускать не каждый раз, а только если набор компилируемых файлов менялся\. Но это опять возвращает нас к необходимости ставить и снимать галки вручную\! И, фактически, сводит этот вариант к предыдущему — только тут все галки в одном месте, а не в двух\.

Короче говоря, идеальной интеграции не получилось, но даже так уже существенно лучше, чем вообще никак\.

## Подавление предупреждений

Раз уж зашла речь об интеграции, для полноты картины следует упомянуть разные способы подавления предупреждений\. В принципе, на [сайте PVS\-Studio все расписано](https://pvs-studio.ru/ru/docs/manual/0017/), поэтому я постараюсь не растягивать мысль\. Некоторые варианты я пропущу, поскольку сам не пользуюсь\.

Итак, подавлять предупреждения можно на нескольких уровнях:

|\*\*Способ подавления\*\*|\*\*Когда нужно\*\*|
|---|---|
|Одно конкретное предупреждение для одной конкретной строчки|Если вы точно знаете, что это не ошибка|
|Все предупреждения для какой\\\-нибудь папки только в текущем проекте|Если это подключенная библиотека внутри папки с проектом|
|Класс предупреждений для текущего проекта|Если этот класс анализов все равно не работает|
|Конкретные виды предупреждений в текущем проекте|Для предупреждений, которые обычно не соответствуют реальным ошибкам, но лезут постоянно|
|Конкретные папки на всем компе|Для постоянно используемых библиотек, которые не живут внутри папки с проектом|



Поскольку в embedded есть реальная возможность делать каждый проект самодостаточным \(т\.е\. не зависящим от каких\-либо внешних библиотек, просто клонишь его и он компилируется\), эту самодостаточность хочется сохранять\. Поэтому все скрипты для анализа и файлы для подавления предупреждений тоже нужно будет хранить внутри папки проекта \(разумеется, саму PVS\-Studio придется ставить отдельно\)\.

### Один конкретный варнинг для одной конкретной строчки

До нужной строчки или справа от нее пишется комментарий вида

```cpp
// -V::XXX - Пояснение, почему варнинг подавлен
```

Здесь ХХХ — это номер варнинга\.

Пояснение, на мой взгляд, **крайне важно** писать; иначе совершенно непонятно, почему варнинг подавлен – потому что он ложный или потому, что он просто раздражал программиста, который не смог понять, в чем проблема\.

### Все варнинги для какой\-нибудь папки только в текущем проекте и классы предупреждений для текущего проекта

Это делается с помощью _xml\-файла_ \(который я обычно называю pvs\_settings\.xml\)\. Этот файл путешествует вместе с проектом\.

**Пример:**

```cpp
<?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\. Этот файл тоже путешествует вместе с проектом\. Разумеется, пояснения о причинах игнорирования приветствуются\!

**Пример:**

```cpp
###### 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_ и применяет все файлы оттуда подряд\.

**Пример:**

```cpp
<?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, достаточно сложно\.

Так же стоит отметить, что анализатор все\-таки замедляет сборку, поэтому иногда я его таки отключаю – как правило, во время неистовой отладки, когда приходится постоянно редактировать какой\-нибудь коэффициент в одной строчке\.

Отдельные вопросы, которые стоило задать себе перед тем, как начинать этот процесс:

* Не проще ли было интегрироваться в Eclipse?
* Не проще ли было встроиться в CI, а не в IDE?
* Может быть стоило выработать рефлекс "есть баг — ~~сегодня праздник~~ запусти PVS, а сам думай потом"\.

И, разумеется, [примеры на гитхабе](https://github.com/Amomum/Keil_PVS_Example)\.