- PVSM.RU - https://www.pvsm.ru -
Вы жили своей обычной жизнью, но внезапно, всё поменялось. Возможно, вы устроились в новое место, сменили команду или из вашей компании ушёл сотрудник.
Теперь вы отвечаете за кодовую базу на C++. Она большая, сложная и своеобразная; достаточно слишком долго на неё посмотреть, как она начинает разваливаться разными интересными способами. Иными словами, это легаси.
Но баги всё равно как-то нужно устранять, а ещё добавлять новые фичи. То есть вам нельзя просто закрыть на неё глаза или что ещё лучше, взорвать её динамитом. Она важна для компании. По крайней мере, для тех, кто платит вам зарплату. А значит, важна для вас.
И что делать теперь?
Не волнуйтесь, у меня такое случалось очень много раз и в разных компаниях (кто-то язвительный может спросить: а разве кодовые базы на C++ бывают какими-то другими?), выход есть, он не особо сложен и поможет вам действительно устранять баги, добавлять фичи, а то и когда-нибудь переписать её.
В этой статье я расскажу о том, что оказалось полезным для меня, и о том, чего стоит всячески избегать.
Буду справедливым к C++: я не ненавижу его, просто этот язык — один из тех, которые люди используют неправильно, что неизбежно приводит к ужасному хаосу. C++ здесь всего лишь жертва; не беспокойтесь, комитет по развитию C++ всё исправит в C++45, добавив в стандартную библиотеку std::cmake
, и вы увидите, как всё заиграет новыми красками… Впрочем, давайте вернёмся к теме статьи.
Вот краткое описание шагов, которые нужно предпринять:
В целом ваша цель заключается в том, чтобы прикладывать минимальный объём усилий для обеспечения приемлемого уровня безопасности, удобства разработки, корректности и производительности проекта. Очень важно всегда помнить об этом. Об этом, а не о «чистом коде», использовании фич нового крутого языка и так далее.
Итак, давайте приступим!
Кстати, всё изложенное в статье применимо и к кодовой базе на чистом C, и к смешанной кодовой базе на C и C++. Так что если это ваш случай, продолжайте читать!
Вы думали, я собираюсь сравнивать разные санитайзеры, флаги компиляции или системы сборки? Ну уж нет, прежде чем приступать к работе, надо поговорить с людьми. Звучит безумно, да?
Разработка ПО должна быть устойчивым и самоподдерживающимся процессом, а не тем, что вызывает выгорание спустя несколько месяцев или лет. Мы не можем заниматься ею сверхурочно, в «марше смерти» и даже в одиночку! Нам нужно убедить людей поддержать нашу инициативу, донести до них, что и почему мы делаем. И это относится ко всем: к вашему руководителю, к коллегам, даже к нетехническим специалистам. И кто знает, возможно, вернувшись из отпуска, вы увидите, что люди продолжают вашу работу, даже когда вас нет на месте.
По сути, я имею в виду следующее: объясните проблему доходчиво, перечислив несколько простых фактов, предложите решение и график его реализации. Очень просто, правда? Например:
А вот чего стоит полностью избегать (разумеется, все случаи полностью выдуманные и никогда-а-а со мной не происходили):
Итак, допустим, вы заручились поддержкой всех важных для проекта людей. Перейдём к процессу:
По моему опыту, при таком подходе все будут довольны и смогут вносить изменения, которые действительно нужны.
Отлично, а теперь перейдём к делу!
Это очень важно, но не во многих проектах делают это. Составьте README (у вас ведь есть README, так?). Это просто список пар <архитектура>-<операционная система>
, например, x86_64-linux
или aarch64-darwin
, которые официально поддерживает ваша кодовая база. Крайне важно, чтобы сборка работала на каждой из них, а ещё, как мы увидим ниже, это позволяет избавляться от захламления платформами, которые вы не поддерживаете.
Если вы хотите углубиться, можно даже записать конкретные версии архитектуры, например, ARMV6, ARMv7 и так далее.
Это позволяет ответить на важные вопросы, в том числе:
char
быть 7-битным?И ещё важный пункт: в этом списке совершенно точно должны быть рабочие станции разработчиков. И это приводит нас к следующему пункту:
Вы бы удивились, узнав, сколько есть реальных кодовых баз на C++, ставших ядром успешного продукта, зарабатывающего миллионы долларов, которые, по сути, даже не компилируются. Ну, если звёзды сойдутся правильно, то могут и скомпилироваться. Но я говорю не об этом. Я говорю о надёжной беспроблемной сборке на всех поддерживаемых платформах. Никакой возни, никаких «мне наконец-то удалось его собрать спустя три недели мучений» (здесь у меня возникают вьетнамские флэшбэки). Всё «просто работает»TM.
Небольшое отступление: раньше я очень любил заниматься карате. Ходил на 3-4 тренировки в неделю и всё такое прочее. Помню, как один из моих учителей (представьте мудрого азиатского старца… хм, хотя на самом деле мой учитель был белым и лысым… так что представьте Стива Балмера):
Пока ты не освоил этот приём. Иногда ты его делаешь, иногда нет, а значит, не освоил. Когда ешь ложкой, ты промахиваешься мимо рта один раз из пяти?
И этот принцип я взял с собой в разработку ПО. Если «новая фича работает», то она работает каждый раз. Не четыре раза из пяти. То же самое относится и к сборке.
Опыт показал мне, что лучший способ создания ПО быстрым и эффективным образом — это возможность его сборки на своей машине, а в идеале — чтобы он ещё и запускался на этой машине.
Если проект огромен, это может представлять проблему, на вашей машине может даже не оказаться достаточно памяти, чтобы завершить сборку. Можно арендовать где-нибудь большой сервер и выполнять сборки на нём. Неидеально, но лучше, чем ничего.
Ещё одна проблема может заключаться в том, что коду требуется какой-то платформенный API, например, io_uring
в Linux. Здесь может помочь реализация оболочки (shim) или сборка внутри виртуальной машины на вашей рабочей станции. Тоже неидеально, но лучше, чем ничего.
Я проделывал всё это в прошлом, и у меня получалось, но сборка непосредственно на своей машине — лучший из вариантов.
Во-первых, если у вас нет тестов, то сочувствую. Вам будет очень трудно вносить хоть какие-то изменения. Так что напишите тесты, прежде чем вносить любые изменения в код, добейтесь их успешного выполнения, а потом возвращайтесь к чтению. Проще всего перехватывать ввод и вывод программы, запущенной в реальном мире, и писать сквозные тесты на основании этих данных, чем разнообразнее, тем лучше. Это гарантирует только отсутствие регрессий при внесении изменений, а не корректность поведения, но это опять-таки лучше, чем ничего.
Итак, у вас есть набор тестов. Если некоторые тесты не проходят, пока отключите их. Добейтесь, чтобы они проходили успешно, даже если на выполнение всего набора тестов требуется несколько часов. Об этом мы побеспокоимся позже.
В идеале должна быть одна команда для сборки и ещё одна для тестирования. Поначалу этого хватит; если будет что-то более сложное, то соответствующие команды можно поместить в build.sh
и test.sh
, где и будет содержаться весь бедлам.
Ваша цель — сделать так, чтобы даже неспециалист в C++ мог собирать код и тестировать его, не задавая вам вопросов.
Здесь кто-то посоветовал бы документировать структуру проекта, архитектуру и так далее. Но поскольку на следующем этапе мы избавимся от большей части всего этого, рекомендую не тратить на это время, займитесь этим в самом конце.
Ударение здесь на «самые простые». Никаких изменений в системе сборки, никаких героических усилий (я много раз повторяю об этом в статье, но это очень важно).
Вы опять-таки удивитесь, какой объём работы в типичном проекте на C++ выполняет сборка, хотя всего этого не нужно делать. Проверьте, есть ли у вас что-то подобное, и замерьте, помогает ли отключение таких поведений:
unittest++
, собранный как подпроект CMake, я обнаружил, что по умолчанию собирались тесты тестового фреймворка, после чего они выполнялись, и так каждый раз! Безумие. Обычно существует переменная CMake или что-то подобное для отключения такого поведения.mbedtls
. Решила проблему тоже установка переменной CMake для отказа от такого поведения.MYPROJECT_TEST
, которая по умолчанию отключена, и собирать, и выполнять тесты, только когда она включена. Обычно её включают только разработчики, работающие над проектом. То же самое относится к примерам, генерации документации и так далее.mbedtls
потому что она раскрывает множество флагов времени компиляции, позволяющих включать части, которые вам могут быть и не нужны. Остерегайтесь всего, что установлено по умолчанию, и собирайте только то, что нужно!mold
, он поможет без всяких дополнительных затрат. Однако это сильно зависит от количества компонуемых библиотек, от того, является ли это узким местом и так далееЗакончив с этим, можно попробовать кое-что ещё, хотя выгода обычно гораздо меньше, а то и отрицательна:
Как только цикл итераций станет приемлемым, можно приступать к изучению кода под микроскопом. Если сборка длится годами, то пока не стоит стремиться к внесению изменений в код.
Я видел случаи, когда тридцать, а иногда и больше процентов кодовой базы на самом деле висело мёртвым кодом. За эти строки кода вы платите при каждой компиляции, когда хотите выполнить рефакторинг и так далее. Так что вырежьте их.
Вот несколько советов:
-Wunused-xxx
, например, -Wunused-function
. Они отлавливают кое-что, но не всё. Следует разобраться с каждым из этих уведомлений. Обычно для этого достаточно удалить код, повторно собрать проект и снова провести тесты. Готово. В редких случаях это симптом вызова не той функции. Поэтому я не очень горю желанием полностью автоматизировать этот этап. Но если вы уверены в своём наборе тестов, то сделайте это.cppcheck
. По моему опыту, бывает довольно много ложноположительных срабатываний, особенно для виртуальных функций в случае наследования, но плюс здесь в том, что эти инструменты находят неиспользуемые части, которые не замечают компиляторы. Поэтому это аргумент для добавления в свой арсенал линтера, если уж не в CI (подробнее об этом ниже).Плюс всего этого не только в том. что вы увеличите скорость сборки раз в пять, не привнеся ничего плохого, но и в том, что если ваш начальник немного технарь, то ему понравится, что пул-реквесты удаляют тысячи строк кода. И вашим коллегам это тоже понравится.
Не перегибайте палку с правилами линтеров, добавьте несколько самых основных, внедрите их в жизненный цикл разработки, постепенно совершенствуйте правила и устраняйте всплывающие проблемы, а потом двигайтесь дальше. Не пытайтесь включить все правила, это кроличья нора с постепенно уменьшающейся пользой. В прошлом я пользовался clang-tidy
и cppcheck
, они могут быть полезны, но в то же время невероятно медленны и зашумлены, так что имейте в виду. Однако избавиться от линтера никак не получится. Когда вы запустите его впервые. то он выявит столько реальных проблем, что вы удивитесь, почему компилятор ничего не обнаруживает даже при всех включённых уведомлениях.
Дождитесь подходящего момента, когда ни одна из ветвей не будет активна (в противном случае у людей будут возникать ужасные конфликты слияния), произвольным образом выберите стиль оформления кода, выполните единовременное форматирование всей кодовой базы (без исключений), обычно это делают при помощи clang-format
, выполните коммит конфигурации. Всё, готово. Не тратьте нервы на споры о самом форматировании кода. Оно существует только для уменьшения размеров diff и избавления от споров, так что не спорьте о нём!
Как и линтеры, они могут стать кроличьей норой. К сожалению, они совершенно необходимы для выявления и устранения реальных неуловимых багов, влияющих на продакшен.
Неплохо будет начать с -fsanitize=address,undefined
. У них обычно не бывают ложноположительных срабатываний, так что если что-то обнаруживается, приступайте к исправлению. Тесты выполняйте тоже с ними, чтобы выявлять проблемы и в тестах. Я даже слышал о людях, у которых с включёнными санитайзерами работал код в продакшене, поэтому, если ваш бюджет производительности это допускает, это может стать неплохой идеей.
Если компилятор, которым вы пользуетесь (вынужденно) для выпуска кода продакшена, не поддерживает санитайзеры, то можно хотя бы при разработке и выполнении тестов использовать clang или что-то подобное. И здесь вам пригодится результат работы, проделанной с системой сборки: вам должно быть довольно просто использовать другие компиляторы.
Одно можно сказать наверняка: даже в лучшей кодовой базе в мире, реализующей лучшие практики и созданной лучшими разработчиками, через секунду после запуска санитайзеров вы абсолютно точно обнаружите ужасные баги и утечки памяти, которые прятались годами. Так что сделайте это. Имейте в виду, что для их устранения потребуется много труда и рефакторинга. Ещё у каждого санитайзера есть опции, так что может быть полезно изучить их, если ваш проект — уникальная снежинка.
И последнее: в идеале все сторонние зависимости тоже должны компилироваться с включёнными санитайзерами при выполнении тестов, чтобы выявлять проблемы [1] и в них тоже.
Как однажды сказал Брайан Кантрилл (цитирую по памяти), «Я убеждён, то основная часть встроенного ПО просто берётся из папки home ноутбука разработчика». Настройка CI — это быстрый и не требующий затрат процесс, автоматизирующий всё то хорошее, что мы настроили выше (линтеры, форматирование кода, тесты и так далее). И благодаря ему мы можем при каждом изменении создать двоичные файлы продакшена в чистой среде. Если вы разработчик, но всё ещё этого не делаете, то, по моему мнению, застряли в прошлом веке.
И вишенка на торте: большинство систем CI позволяют выполнять этапы для матрицы различных платформ! Поэтому вы можете действительно проверить, что список поддерживаемых платформ — это не просто теория, а реальность.
Обычно конвейер выглядит как make all test lint fmt
, так что ничего особо сложного тут нет. Просто сделайте так, чтобы проблемы, о которых сообщают инструменты (линтеры, санитайзеры и так далее) приводили к прекращению работы конвейера, иначе никто их не заметит и не исправит.
Эта тема хорошо изучена, так что особо многого здесь говорить не буду. Достаточно сказать, что большой объём кода часто можно существенно упростить.
Вспоминаю, как итеративно упрощал сложный класс, который вручную распределял и (иногда) освобождал память, должен был работать с какими-то универсальными данными и так далее. Как оказалось, единственное, что делал класс — это распределял указатель, а позже проверял, является ли он нулевым. Вот и всё. По мне, так это обычный boolean. True/false, и больше ничего.
Мне кажется, что для этого этапа сложнее всего наметить график, потому что каждый раунд упрощения открывает новые возможности для дальнейшего упрощения. Воспользуйтесь здесь своей интуицией и давайте консервативные оценки. Сосредоточьтесь на осязаемых целях: безопасности, корректности и производительности; сторонитесь субъективных критериев наподобие «чистого кода».
По моему опыту, апгрейд используемого в проекте стандарта C++ может иногда помочь с упрощением кода, например, для замены вручную инкрементирующего итераторы кода на цикл for (auto x : items)
, но помните, что это лишь средство, а не цель. Если вам нужен лишь std::clamp
, просто напишите его самостоятельно.
Прямо сейчас я занимаюсь этим по работе, и сама по себе эта тема заслуживает отдельной статьи. Тут есть множество тонкостей. Делайте это, только если есть веские причины.
Вот и всё, теперь у вас есть осязаемый пошаговый план по выходу из сложной ситуации с появлением сложной старой кодовой базы на C++. Я только что закончил реализацию этого плана при работе над проектом, и теперь взаимодействие с ним стало гораздо более терпимым. Те коллеги, которые раньше и на десять километров не желали подходить к этой кодовой базе, теперь вносят существенный вклад в неё. И это очень радостно.
Есть ещё важные темы, которые я хотел упомянуть, но в конечном итоге не стал, например, абсолютная необходимость возможности локального выполнения кода в отладчике, фаззинга, сканирования зависимостей на уязвимости и так далее. Возможно, напишу ещё одну статью!
Этот раздел очень субъективен, это лишь моё личное предвзятое мнение.
Существует горячо обсуждаемая тема, которую я до этого момента тщательно обходил — управление зависимостями. Если вкратце, то в C++ его нет вообще. Большинство людей полагается на использование системного менеджера пакетов; это легко заметить, потому что их README выглядит так:
В Ubuntu 20.04: `sudo apt install [сто строк пакетов]`
В macOS: `brew install [сто строк пакетов с немного другими именами]`
Во всех остальных: ну, не повезло тебе, друг. Наверно, стоит выбрать популярную ОС и переустановить приложение ¯_(ツ)_/¯
И так далее. Я и сам так делал. И я считаю, что это ужасная идея. Вот почему:
-march
, отладочная информация и так далее. Или пакеты собраны при помощи отличающейся от используемой вами версии компилятора C++ и ломают ABI C++ между ними.Вы, наверно, подумаете: знаю, я буду использовать новые крутые менеджеры пакетов для C++, Conan, vcpkg и им подобные! Не торопитесь:
2.16.12
на 2.23.0
. Что произошло с версиями между ними? Они уязвимы и их не следует использовать? Кто знает! Для доступных версий всё равно не перечислены уязвимости безопасности! И, разумеется, в прошлом у меня был проект, который использовал версию 2.17
…Если у вас сложилась ситуация, в которой они подходят, то это великолепно и гораздо лучше сложившегося у меня представления об использовании пакетов. Просто я пока ещё ни разу не сталкивался с проектом, где можно было бы ими воспользоваться, всегда что-то мешало.
Так что же я рекомендую? Старые добрые подмодули (submodule) git и компиляцию из исходников. Да, это трудоёмко, но в то же время:
git checkout
Для компиляции каждой зависимости в каждом подмодуле может быть достаточно простого add_subdirectory
при помощи CMake, или может потребоваться ручное git submodule foreach make
.
Если подмодули использовать никак невозможно, то можно всё равно компилировать всё из исходников, но делать это вручную, при помощи одного скрипта, который получает каждую зависимость и собирает её. Пример реального использования: Neovim.
Разумеется, если визуализация вашего графа зависимостей в Graphviz выглядит, как тест Роршаха и должен собирать тысячи зависимостей, то реализовать это не так просто, но всё равно возможно при помощи систем сборки наподобие Buck2, выполняющей гибридные локально-удалённые сборки и повторно используещей артефакты сборок между сборками для разных пользователей.
Если взглянуть на ситуацию с менеджерами пакетов для компилируемых языков (Go, Rust и так далее), то можно увидеть, что всё (известные мне) менеджеры выполняют компиляцию из исходников. Это та же технология, минус git, плюс автоматизация.
Я собрал отличные идеи и отзывы читателей (иногда это совокупность нескольких комментариев разных людей, а иногда я перефразирую по памяти, так что простите, если что-то будет не совсем точно):
Автор:
ru_vds
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/continuous-integration/390367
Ссылки в тексте:
[1] проблемы: https://github.com/rxi/microui/pull/67
[2] Conan и mbedtls: https://conan.io/center/recipes/mbedtls
[3] Источник: https://habr.com/ru/companies/ruvds/articles/798453/?utm_source=habrahabr&utm_medium=rss&utm_campaign=798453
Нажмите здесь для печати.