Уроки, извлеченные из переноса проектов 300 C / C ++ на Buck Build

С помощью Buckaroo мы превращаем огромную экосистему проектов C / C ++ в набор легко компонуемых строительных блоков. Мы не можем сделать это в одиночку, но, чтобы дать толчок сообществу, мы взяли на себя задачу перенести 300 проектов в Buck build system.

Выбранные нами библиотеки были основаны на их популярности на GitHub, StackOverflow и запросах из нашего списка рассылки. Они варьировались от крошечных библиотек только для заголовков до монолитных проектов C ++ и старых, но важных библиотек C.

Для каждой библиотеки мы пытались сделать полный перенос на Buck. Было несколько случаев, когда это не сработало; иногда структура проекта была настолько запутанной, что мы решили, что более практично обернуть существующую систему сборки. Мы можем вернуться к этим проектам позже, но по большей части усилия по переносу были успешными.

Изучив так много проектов, мы подумали, что было бы неплохо составить список того, что можно и чего нельзя делать при создании архитектуры чистых библиотек C / C ++.

НЕЛЬЗЯ: объединять файлы .cpp в одну единицу перевода.

Антишаблон, обнаруживаемый во многих проектах C / C ++, - это практика объединения нескольких файлов C / C ++ в одну единицу перевода для компиляции. Теоретически это улучшает время компиляции, потому что шаги препроцессора и синтаксического анализа должны выполняться только один раз для всей сборки. Это верно для одиночной сборки, но лишает вас возможности делать инкрементные сборки. С одной единицей перевода компилятор должен выполнять всю сборку с нуля всякий раз, когда изменяется какой-либо из файлов проекта!

НЕОБХОДИМО: проясните свои зависимости

Использование существующих библиотек - чистый плюс для всех. Код библиотеки лучше тестируется, что отлично для сообщества, и меньше работы повторяется, что отлично для вас! Однако отсутствие доминирующего менеджера пакетов C / C ++ означает, что для этого используются несколько подходов - некоторые неоптимальные.

При написании своей библиотеки убедитесь, что вы четко документируете ее зависимости. Вот несколько разумных способов сделать это:

  • Подмодули Git
  • Список apt-get или brew install команд - хотя пользователи вашей библиотеки могут быть раздражены, если им придется изменить свою систему, чтобы использовать ее!
  • Хороший пакетный менеджер

НЕЛЬЗЯ: используйте include_next, если только в этом нет необходимости.

#include_next - это непонятная функция препроцессора, которая позволяет пользователю включать файл внутрь себя. Он предназначен для исправления системных заголовков; Документы GCC объясняют это следующим образом:

Иногда необходимо изменить содержимое файла заголовка, предоставленного системой, без его непосредственного редактирования. Например, это делает операция GCC fixincludes. Один из способов сделать это - создать новый файл заголовка с тем же именем и вставить его в путь поиска перед исходным заголовком. Это прекрасно работает, если вы хотите полностью заменить старый заголовок. Но что, если вы хотите сослаться на старый заголовок из нового?

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

НЕОБХОДИМО: Храните частные заголовки и экспортированные заголовки отдельно

Поддержание четко определенного API для вашей библиотеки - действительно хорошая идея. Он позволяет выполнять внутренние настройки, не нарушая работу последующих потребителей, и обеспечивает четкую поверхность для модульного тестирования.

В идеале вы должны разделить частные и экспортированные заголовки по отдельным папкам. Мне очень нравится это соглашение:

  • include— заголовки, доступные пользователям библиотеки.
  • private— Заголовки, которые необходимы для компиляции библиотеки, но не должны быть доступны потребителям библиотеки.

НЕЛЬЗЯ: включать файлы .cpp.

Препроцессор C / C ++ невероятно гибок и позволяет делать то, что, вероятно, не следует делать. Один из примеров - это .cpp файлов. Это очень сбивает с толку, поскольку расширение файла больше не соответствует назначению. Этот файл должен быть заголовком? Затем дайте ему расширение .h или .hpp. Этот файл предназначен для компиляции? Тогда не включайте это.

НЕОБХОДИМО: В ваших примерах используйте библиотеку так, как она предназначена для использования.

Много раз я находил примеры, которые были написаны так, как будто они находятся в исходном каталоге библиотеки:

#include “../../things.h”

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

#include <my-library/things.h>

Конечно, это требует, чтобы вы правильно установили пути включения, но хорошая система сборки должна сделать это тривиальным.

НЕЛЬЗЯ: копировать зависимости в свой проект.

Если вы не используете менеджер пакетов, может возникнуть соблазн просто скопировать проекты, от которых вы зависите, в систему управления версиями. Это имеет то преимущество, что все создают один и тот же код, но это может стать кошмаром для потребителей вашей библиотеки.

Например, предположим, что вы являетесь автором библиотеки A, которая зависит от библиотеки Bv1. Однако один из ваших пользователей пишет библиотеку C, которая зависит от A и Bv2. Теперь, когда они пытаются построить ваш проект, они сталкиваются с конфликтами символов и должны либо перейти на Bv1, либо отправить PR A! Намного лучший подход - использовать диспетчер пакетов, который может преобразовать B в версию, которая работает для всех зависимостей в проекте. По крайней мере, использование подмодулей Git может сделать такие обновления более управляемыми.

Исключением из этого правила является включение крошечных библиотек только для заголовков, которые используются для задач разработки, таких как Catch testing framework.

НЕОБХОДИМО: пространство имен ваших файлов заголовков

Вы должны выбрать уникальное имя для своей библиотеки - используйте генератор, если вы застряли, - затем поместите все экспортированные заголовки в папку с этим именем. Это значительно снижает вероятность конфликтов имен с другими библиотеками.

НЕЛЬЗЯ: злоупотреблять препроцессором

Препроцессор C / C ++ сам по себе является языком программирования, позволяющим использовать все виды магии времени компиляции и генерации кода. Проблема в том, что препроцессор может быть очень сложно отладить, поскольку инструменты не так хороши, как те, которые доступны для C / C ++.

Так зачем использовать препроцессор? В прошлом логика заключалась в том, что препроцессор гарантированно работает во время компиляции, и поэтому для достижения максимальной производительности логика должна быть реализована там, где это возможно. С тех пор компиляторы значительно улучшились, и теперь они сделают большую часть этой работы за вас. Таким образом, с помощью современных инструментов лучше избегать реализации логики в препроцессоре, если она может быть реализована на C / C ++. Доверьтесь своему компилятору!

Если вам необходимо использовать препроцессор, например, для реализации бесстековых сопрограмм, постарайтесь минимизировать его использование. Надеюсь, что эти кейсы будут включены в язык C ++ в будущем, как и шаблоны в прошлом.

DO: абстрагируйте различия платформ с помощью файлов

Может возникнуть соблазн добавить пару #ifdef __MACOS__ #endif команд в исходный код, но правда в том, что это очень затрудняет чтение кода. Если вы разделите реализации для конкретной платформы на отдельные файлы, тогда ваша система сборки сможет включать, компилировать и связывать соответствующие файлы. Это делает код более удобным в обслуживании и доступным для новых читателей.

НЕЛЬЗЯ: зависеть от конкретных функций компилятора (если это действительно не нужно).

У всех трех компиляторов C ++ (Clang, VC ++, GCC) есть свои причуды, и можно написать код, который компилируется в одном, но не в другом. Обычно этого можно избежать, придерживаясь стандарта языка (или его подмножества в случае VC ++).

Я понимаю - некоторые из этих специфичных для поставщика функций могут быть удобными (#pragma once) - но, не делая вашу библиотеку переносимой, вы уменьшаете влияние своей работы. Вы теряете удовлетворение от оказания большого влияния на сообщество C ++, а сообщество теряет то, что могло бы стать отличной переносимой библиотекой.

Если вам нужны определенные функции, такие как __builtin_popcount, напишите абстракцию над встроенной версией, а также над переносимой версией. Затем разделите реализации на отдельные файлы и сообщите вашей системе сборки о ваших намерениях. А еще лучше повторно использовать уже написанную абстракцию.

НЕОБХОДИМО: использовать папки для разделения файлов по категориям

Листинг файлов проекта вручную довольно утомительно. Распространенное решение - использовать команду glob. Glob - очень мощный инструмент, но вы можете значительно упростить задачу, разместив файлы в удобном для глобуса виде. Это означает разбиение файлов на логические папки в зависимости от их назначения.

Плохо организованный проект

.
├── common.cpp
├── foo.cpp
├── pthread.cpp
└── win_thread.cpp

Аккуратно организованный проект

.
├── common
│   ├── common.cpp
│   └── foo.cpp
├── linux
│   └── pthread.cpp
└── windows
    └── win_thread.cpp

Вот и все! 🙌

Пакеты Buckaroo готовы к использованию прямо сейчас, и мы усиленно работаем над портированием большего количества. Если вам нужна конкретная библиотека, создайте вопрос в списке желаний. Или (что еще лучше), если вы хотите внести свой вклад, пиарщики всегда приветствуются!

Хакерский полдень - это то, с чего хакеры начинают свои дни. Мы часть семьи @AMI. Сейчас мы принимаем заявки и рады обсуждать рекламные и спонсорские возможности.

Если вам понравился этот рассказ, мы рекомендуем прочитать наши Последние технические истории и Современные технические истории. До следующего раза не воспринимайте реалии мира как должное!