Как собрать библиотеку c из исходников
БлогNot. Как собрать проект C++ с github из исходников и подключить его к Visual Studio
Как собрать проект C++ с github из исходников и подключить его к Visual Studio
Благодаря менеджеру пакетов winget, уже входящему в актуальные сборки масдайки, теперь в Windows 10 можно инсталлировать приложения одной простой консольной командой (см. также доку от Микрософта).
Но мы рассмотрим сейчас ситуацию, когда у нас есть только ссылка на исходники проекта, скажем, на Гитхабе (возьмём для примера библиотеку для простых чисел primesieve) и нужно каким-то образом скомпилировать внешний проект в своей Studio, чтобы воспользоваться его возможностями в своём приложении.
В противном случае, конечно же, нестандартный include вроде этого, который вы нашли в коде-образце
работать не будет ни за что.
Первым делом скачаем все исходники внешнего проекта «как есть» в архиве .zip, для этого у нас на гитхабе есть кнопка «Download ZIP»:
Как загрузить проект с github в архиве .zip
Развернём проект, не создавая новой папки, если у вашего архиватора нет такого же пункта меню, просто сотрите предлагаемое архиватором имя новой папки, потому что папка уже есть в архиве:
Извлечь внешний проект из архива, не создавая новой папки
Если покопаться в файле readme.md проекта, как правило, можно найти инструкцию по установке (Build instructions) и даже «Detailed build instructions», где говорится, в числе прочего, и о компиляции проекта под Microsoft Visual C++:
Команды cmake для компиляции проекта со страницы документации
Откроем свой «некомпилируемый» без нужной библиотеки проект в Studio (я использую актуальную сборку версии 2019) и обратимся к команде меню Вид — Терминал. Выберем инструмент «Командная строка разработчика» (по умолчанию в новых сборках теперь выбран PowerShell, впрочем, если в документации приведены команды PowerShell, то применяйте их).
У Микрософта инструмент описан вот здесь.
Командная строка разработчика в Studio
В командной строке пишем команды из документации, но сначала, конечно, нужно перейти в ту папку, где у вас развёрнут скачанный проект. Мне понадобилось ввести в консоли следующее, завершая каждую команду нажатием Enter:
— теперь я в нужной папке, так как развернул свой архив в папку d:\temp
Далее как написано:
Можно просто копировать команды со страницы документации, в окне консоли вверху есть стандартная кнопочка «Вставить». А вот точка в записи команд имеет значение, это ссылка на текущую папку!
Ну и, конечно, для другой версии Studio будет другое указание компилятора, узнать своё можно командой
Нужный генератор будет помечен в списке «звёздочкой».
Теперь проект можно открывать в Studio и работать с ним, все нужные файлы есть в папке d:\temp\primesieve-master
Но мы хотим подключить всё, что нужно, к своему имеющемуся проекту, а не пытаться модифицировать чужую библиотеку.
- Меню Проект — Свойства, слева выбираем Свойства конфигурации, C/C++, Общие, раскрываем поле «Дополнительные каталоги включаемых файлов», говорим «Изменить» и показываем на папку D:\Temp\primesieve-master\include . В вашем проекте, как правило, тоже будет вложенная папка include .
- В том же окне выбираем Компоновщик — Общие — Дополнительные каталоги библиотек, «Изменить» и добавляем путь D:\Temp\primesieve-master\Release . Этого может оказаться мало, у вашего проекта и внешнего должны быть выбраны одинаковые конфигурации решения. Так как я выбрал Release для внешнего проекта, то и в своём проекте в списке «Конфигурации решения» (на стандартной панели инструментов) указал Release и платформу x64. Можно было работать и с Debug, но тогда и внешний проект компилируем как Debug и потом выбираем путь D:\Temp\primesieve-master\Debug .
- В списке C/C++ — Создание кода — Библиотека времени выполнения выбрал Многопоточный DLL (/MD), иначе будет «LNK2038: обнаружено несоответствие для ‘RuntimeLibrary’: значение ‘MT_StaticRelease’ не соответствует значению ‘MD_DynamicRelease’ в file.obj».
- Сам файл библиотеки, как правило имеющий тип .lib , тоже нужно прописать. Всё в том же окне свойства проекта выбираем список Компоновщик — Ввод, раскрываем список «Дополнительные зависимости», жмём «Изменить» и указываем в поле ввода имя файла библиотеки primesieve.lib
- На всякий случай, проверяем, что у нас в списке Компоновщик — Система — Подсистема, у меня там просто Консоль (/SUBSYSTEM:CONSOLE) , для других типов проектов может понадобиться изменение и этой настройки.
После этого у меня всё заработало.
Ну а конкретная задача, на которой я проверял библиотеку — печать самых длинных цепочек последовательных простых чисел, в которых разница между соседними значениями строго возрастает или строго убывает, предел счёта равен 1000000, вот сама программа:
Ответы вышли такие:
За счёт хорошо оптимизированного кода библиотеки считается всё мгновенно.
Как написать свою библиотеку на Си
Если ты встал на путь С/С++ разработчика, то скорее всего (помимо использования стандартной библиотеки — libc) рано или поздно вам потребуется занятся разработкой собственных библиотек. Зачем. Причин может быть несколько. Например вы написали свою структуру данных или свой алгоритм, и хотите использовать его повторно или распространять. Так же возможно вы написали несколько утилит и все они используют один и тот же кусок кода (например, как часто это бывает, логгер), и будет логично вынести этот кусок кода в отдельный модуль. Поскольку сопровождать такой код будет проще.
Реализация
И так, давайте начнем с примера. Создадим заголовочный файл somecode.h, который будет содержать объявление некоторой функции. Пусть будет простая функция которая разбивает предложение на слова и печатает каждое слово в новой строке. Простой синтетический пример.
И создадим файл somecode.c, в которой напишем реализацию нашей функции.
Далее создадим файл main.c, где будем вызывать нашу функцию.
Давайте для начала скомпилируем это все самым обычным способом для проверки работоспособности.
- из исходных файлов получаем объектные файлы
- из объектных файлов получаем исполняемый файл
Запускаем исполняемый файл и видим, что все работает.
А теперь рассмотрим пример получения библиотеки и линковки его к исполняемому файлу. Для компиляции используем вызов gcc со следующими опциями.
Из файла с расширением .c мы получаем файл с расширением .so. И так же обратите внимание, что библиотека имеет префикс lib. Еще мы видим, что появились два дополнительных аргумента -shared и -fpic. С помощью опции -shared мы говорим компилятору, что хотим получить а выходе библиотеку. А опция -fpic говорит компилятору, что объектные файлы должны содержать позиционно-независимый код (position independent code), который рекомендуется использовать для динамических библиотек.
Теперь скомпилируем наш исполняемый файл подключив к нему нашу библиотеку. Для этого нужно указать название библиотеки через опцию -l.
И опс.. мы получили ошибку… Линкер говорит нам, что он не знает где лежит наша библиотека. С помощью опции -L указываем текущую директорию, где лежит наша библиотека и компиляция проходит успешно.
Пытаемся запустить нашу и программу и ловим еще одну ошибку в котором говорится, что в процессе загрузки динамических библиотек отсуствует наша библиотека.
По умолчанию в операционной системе есть некоторое количество стандартных директорий, где должны располагатся библиотеки. Посмотреть этот список можно так.
Так же есть возможность задавать дополнительные директории с библиотеками с помощью переменной окружения LD_LIBRARY_PATH. С помощью утилиты ldd посмотрим от каких библиотек зависит наша программа.
Добавим в LD_LIBRARY_PATH текущую директорию.
Видим, что наша библиотека подгрузилась.
И теперь программа запускается и работает.
Если сравнить два исполняемых файла, то видим, что программа, которая использует динамическую библиотеку имеет меньший размер.
Это произошло как раз за счет того, что реализация нашей функции теперь лежит за пределами нашего исполняемого файла. С помощью утилиты objdump можем глянуть внутрь бинарных файлов. И увидим, что во втором бинарном файле реализация функции отсутствует, но присутствует в библиотеке.
Разница в нашем случае может и маленькая, но в масштабах десятков и сотен файлов разница будет значительной.
Так что же мы сделали?
У нас есть исходные файлы, мы скомпилировали их, получив из них динамическую библиотеку. Далее эту библиотеку можем использовать повторно в других проектах.
Примерно те же действия вы будете делать и под windows и под мак, будут немного другие компиляторы, но идея одна.
Руководство по CMake для разработчиков C++ библиотек
В этой статье я расскажу о том, как правильно писать современные CMakeLists.txt файлы для C++ библиотек. Идеи, используемые в ней, основаны на докладе Крейга Скотта (разработчик CMake) и докладе Роберта Шумахера (разработчик vcpkg) c CppCon 2019. Поскольку мне достаточно часто приходится разрабатывать С++ библиотеки, я создал для себя небольшой шаблон cpp-lib-template, который будет использоваться в этой статье в качестве примера.
Замечу, что я предполагаю, что читатель этой статьи имеет опыт работы с CMake, однако, как и я, часто задается вопросом, как правильно ему сделать сборку своей библиотеки.
Введение
Разработчик C++ библиотеки, очевидно, должен дать своим пользователям возможность легко ее использовать. И раз уж мы пишем на компилируемом языке, то к этому относится и то, насколько быстро пользователь сможет пройти путь от клонирования ваших исходников до получения бинарного файла библиотеки под свою платформу.
Когда мы говорим о пользователях библиотеки, то в первую очередь думаем о других разработчиках, которые захотят использовать функциональность библиотеки в своих проектах. Существует два способа, которыми библиотека может интегрироваться в проект:
С помощью find_package — в этом случае библиотека должна предоставить package configuration file, который импортирует в CMakeLists.txt приложения таргет библиотеки. Этот файл, вместе с собранной библиотекой, ее публичными заголовочными файлами и некоторой другой информацией устанавливается в директорию, которая потом указывается в переменной CMAKE_PREFIX_PATH при сборке проекта.
Как подпроект, добавленный в качестве git submodule или с помощью CMake модуля FetchContent — в этом случае приложение использует обычный (не импортированный) таргет библиотеки ( find_package не вызывается), и сборка библиотеки становится этапом сборки самого проекта.
Еще одна категория пользователей библиотеки — мейнтейнеры различных пакетных менеджеров (например, vcpkg, conan и другие), которым нужно собирать библиотеку под десятки различных платформ и конфигураций. Для них важно, чтобы сборкой библиотеки можно было управлять извне, без необходимости внесения патчей в ее CMakeLists.txt.
Исходя из вышесказанного, хорошо сделанная библиотека должна удовлетворять следующим требованиям:
Единообразно интегрироваться и через find_package , и через add_subdirectory / FetchContent , т.е. импортированный таргет и обычный должны быть эквивалентны. В англоязычных источниках это требование часто формулируют как «build interface should match install interface».
В ее CMakeLists.txt не должны хардкодиться опции и флаги компиляции/компоновки кроме тех, которые абсолютно необходимы для сборки библиотеки. В противном случае мейнтейнерам менеджеров пакетов будет проблематично упаковывать библиотеку, так как с вероятностью, стремящейся к 1, на какой-то из платформ некоторые захардкоденные значения окажутся невалидны и придется делать патч для CMakeLists.txt .
Структура директорий
В своих библиотеках я придерживаюсь структуры директорий, представленной ниже. На мой взгляд, она является наиболее распространенной, кроме того, интуитивно разделяет файлы библиотеки на основные компоненты: публичные заголовки ( include/<libname> ), исходники ( src ), утилиты для сборки ( cmake ), примеры ( examples ) и тесты ( tests ).
Пример библиотеки
В дальнейшем в этой статье мы будем рассматривать простую библиотеку mylib, предоставляющую всего одну функцию add , которая возвращает результат сложения двух чисел:
Соответствующий заголовочный файл:
Наконец, файл mylib/export.h :
Файлы export_shared.h и export_static.h генерируются CMake и будут рассмотрены ниже.
Определение проекта
Первое, что нужно сделать в CMakeLists.txt — это создать проект для библиотеки:
Я предпочитаю заранее определить таргет для библиотеки, а инициализировать его ниже в коде, когда определены все необходимые параметры. Здесь же мы определим алиас для библиотеки, который должен иметь то же имя, что и импортируемый таргет. Это позволит пользователям библиотеки легко переключаться между ее подключением через find_package , в результате которого создается импортированный таргет с именем mylib::mylib , и подключением через add_subdirectory / FetchContent , который делает доступным алиас mylib::mylib в их проекте. Таким образом, приложение, использующее нашу библиотеку, в обоих случаях может линковаться к библиотеке с помощью команды:
Переменная is_top_level используется в дальнейшем в нескольких местах для определения, собирается ли библиотека как stand-alone проект или как подпроект. Версии CMake, начиная с 3.21, предоставляют переменную PROJECT_IS_TOP_LEVEL для этой же цели. Можно использовать и ее, но тогда в cmake_minimum_required придется указать версию не ниже 3.21.
Опции сборки
Сразу после определения проекта для библиотеки я рекомендую указать все опции, которые вы предоставляете для управления ее сборкой. Эта информация важна для пользователей библиотеки и поэтому лучше размещать ее в самом начале в одном месте. Типичными опциями являются:
Опции MYLIB_BUILD_* определяют, собирать или нет соответствующий компонент библиотеки. По умолчанию они выключены, потому что пользователи библиотеки, как правило, заинтересованы только в сборке самой библиотеки.
Если библиотека может быть собрана как статически, так и динамически, крайне не рекомендуется использовать кастомную переменную для определения типа сборки, потому что общепринятым стандартом является использование переменной BUILD_SHARED_LIBS , которая обрабатывается самим CMake. Следующий код является примером того, как делать не стоит:
BUILD_SHARED_LIBS , однако, влияет на все библиотеки, попадающие в сборку, а пользователям в редких случаях может понадобиться собрать несколько библиотек иначе, чем указано в BUILD_SHARED_LIBS . Поэтому среди опций библиотеки mylib есть MYLIB_SHARED_LIBS , которая может использоваться для переопределения значения BUILD_SHARED_LIBS .
Переменная MYLIB_INSTALL определяет, нужно ли генерировать таргет для установки mylib. Ее значение по умолчанию определяется тем, собирается ли mylib как отдельный проект, или как подпроект другого проекта.
Наконец, переменная MYLIB_INSTALL_CMAKEDIR позволяет указать, куда устанавливать файл конфигурации пакета (package configuration file), и предназначена в основном для мейнтейнеров менеджеров пакетов. Функция set_if_undefined определена в файле cmake/utils.cmake и аналогична set , но устанавливает значение только если переменная еще не определена (напомню, что мы не хотим переопределять любые переменные, которые установлены через командную строку CMake или в проектах верхнего уровня).
Экспорт символов
В бинарном файле любой динамической библиотеки есть раздел, в котором хранится информация о том, какие функции экспортируются данной библиотекой. Эта информация используется динамическим загрузчиком — компонентом ОС, который занимается тем, что загружает в память динамические библиотеки, нужные приложению. Помимо этого, задача динамического загрузчика заключается в том, чтобы выполнить релокации — заменить заглушки в исполняемом файле приложения на реальные адреса в памяти, куда была загружена динамическая библиотека.
В деталях описание этого процесса можно найти в статьях, ссылки на которые даны в конце поста. Сейчас же для нас важно знать, что от количества экспортируемых функций напрямую зависит, как быстро будут стартовать приложения, использующие вашу библиотеку (чем меньше экспортируемых функций, тем быстрее).
GCC и Clang по умолчанию экспортируют все символы, которые есть в библиотеке (в том числе те, что не являются частью API), что, как мы только что выяснили, негативно сказывается на скорости загрузки. CMake позволяет легко отключить это поведение, установив следующие переменные (замечу, что здесь я тоже использую функцию set_if_undefined , чтобы дать возможность пользователю при необходимости переопределить эти значения):
MSVC по умолчанию ничего не экспортирует, однако CMake предоставляет переменную CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS , которая позволяет получить поведение, аналогичное GCC и Clang. Очевидно, что использовать ее не стоит.
Для экспорта конкретного символа из библиотеки разные компиляторы предоставляют разные директивы. Чтобы не углубляться в особенности компиляторов, можно воспользоваться функцией CMake generate_export_header . Эта функция создает файл, содержащий определение макроса MYLIB_EXPORT , который нужно указывать для экспортируемых символов. Процесс генерации файла элементарен:
В результате в зависимости от типа сборки библиотеки CMake создаст в билд-директории один из двух файлов: export_shared.h или export_static.h .
Объясню, почему используются разные имена в зависимости от типа сборки. Это нужно, чтобы статическую и динамическую версию библиотеки при желании можно было установить в одну директорию. Для этого файлы должны иметь разные имена, чтобы не быть перезаписанными файлом для другого типа сборки. При этом выбор нужного можно сделать в отдельном файле ( mylib/export.h , см. выше) с помощью идентификатора MYLIB_STATIC_DEFINE , который будет определяться только для статического таргета mylib.
Исходники библиотеки
Исходники библиотеки инициализируются с помощью следующих команд:
Я предпочитаю использовать отдельную переменную для хранения публичных заголовков, потому что это лучше отражает разницу между этими файлами и может использоваться потом при создании install-таргета (см. ниже). Функция source_group заставляет CMake сгенерировать такую же структуру директорий в IDE, как и в самом репозитории библиотеки.
Таргет библиотеки
Теперь все готово для того, чтобы проинициализировать таргет библиотеки, который мы создали в самом начале. Делается это с помощью следующих команд:
Пояснения здесь требует, пожалуй, только момент связанный определением MYLIB_STATIC_DEFINE для статического таргета. Это нужно, чтобы файл mylib/export.h включал файл mylib/export_static.h , если используется статическая версия библиотеки, и файл mylib/export_shared.h в противном случае.
Обратите также внимание, что мы не пытаемся установить какие-то флаги компиляции или свойства таргета здесь. Как уже говорилось, это в дальнейшем создаст проблемы сторонним разработчикам, которые будут упаковывать вашу библиотеку под свою платформу. Например, во многих библиотеках, которые могут собираться под Windows, в свойствах таргета библиотеки часто указываются <CONFIG>_POSTFIX , чтобы бинарные файлы библиотеки, собранные под разные конфигурации (static/shared, debug/release), получали разные имена и могли устанавливаться в одну директорию. Если это сделать в CMakeLists.txt библиотеки, отказаться от выбранной разработчиком схемы будет проблематично. Вместо этого, постфиксы можно задать на этапе конфигурирования проекта с помощью переменных CMAKE_<CONFIG>_POSTFIX , указанных явно в командной строке или в пресете (preset), о которых мы поговорим позже.
Install-таргет библиотеки
Поскольку мы держим в уме тот факт, что библиотека может собираться как подпроект, мы предоставляем специальную переменную MYLIB_INSTALL , с помощью которой можно отключить генерацию install-таргета. Кроме того, самому CMake также можно указать, чтобы он не генерировал таргет для install команд с помощью переменной CMAKE_SKIP_INSTALL_RULES . По этой причине, код для создания install-таргета помещен внутрь условия:
Сам код для создания install-таргета имеет такой вид:
В этом коде все достаточно традиционно, отмечу лишь, что в команде install(TARGETS) пути, куда нужно устанавливать основные файлы, можно опустить. В этом случае CMake по умолчанию использует те, которые предоставляются модулем GnuInstallDirs (что правильно, так как их легко переопределить на этапе конфигурации при необходимости).
Кроме того, я рекомендую явно указывать компоненты библиотеки с помощью параметра COMPONENT . В любой библиотеке можно выделить как минимум два компонента:
runtime — то, что нужно, чтобы приложение, использующее библиотеку, в принципе могло запуститься ( so или dll файл); в своих библиотеках я как правило называю этот компонент также, как и саму библиотеку, т.е. <libname>
development — то, что нужно пользователю библиотеки (заголовочные файлы, библиотека импорта, файл конфигурации пакета и т.д.); для этого компонента я использую имя <libname>-dev
Разделение на компоненты позволяет в дальнейшем выполнять установку только необходимых файлов. Например, следующая инструкция установит только runtime-компонент библиотеки mylib:
Замечу, что файлы с определением импортируемых таргетов имеют разные имена в зависимости от типа сборки библиотеки. Это нужно для того, чтобы иметь возможность устанавливать статическую и динамическую версию библиотеки в одну директорию.
При создании install-таргета под Windows дополнительно указывается, как будут устанавливаться pdb-файлы, нужные для отладки библиотеки:
Здесь нужно учесть один момент. Дело в том, что msvc при компиляции генерирует pdb-файл для каждого объектного файла .obj (compiler pdb-файл), а потом компоновщик объединяет их в один pdb-файл (linker pdb-файл) для итогового исполняемого файла (например, .dll ). Когда вы собираете статическую библиотеку (которая по сути своей является таким же объектным файлом), компоновщик не вызывается вообще, только компилятор, который создает compiler pdb-файл.
CMake предоставляет простой способ получить linker pdb-файл: выражение-генератор TARGET_PDB_FILE . К сожалению, с помощью него вы не сможете получить compiler pdb-файл, когда собираете статическую версию библиотеки. В качестве хоть какого-то варианта решения этой проблемы, я предполагаю, что compiler pdb-файл лежит по тому же пути, что и статическая библиотека и имеет то же имя. Тем не менее, это не обязательно должно быть так, поэтому команда для установки pdb-файлов отмечена как OPTIONAL . Если вы можете предложить лучший вариант для обработки compiler pdb-файлов, поделитесь им в комментариях.
Файл конфигурации пакета
Файл конфигурации пакета предоставляется разработчиком библиотеки (я обычно помещаю его в директорию cmake ) и устанавливается в одну директорию с файлами, сгенерированными командой install(EXPORT) и содержащими определения импортированных таргетов библиотеки ( mylib-shared-targets.cmake и mylib-static-targets.cmake ). В этом файле как правило делаются две вещи:
включается mylib-shared-targets.cmake или mylib-static-targets.cmake
с помощью find_dependency находятся все зависимости импортированного таргета
Наша тривиальная библиотека ни от чего не зависит, поэтому ее файл конфигурации имеет такой вид:
Единственное, что заслуживает в нем внимания, это простой алгоритм, по которому определяется, какой таргет (для статической или динамической версии библиотеки) импортировать. Вкратце алгоритм можно описать так: MYLIB_SHARED_LIBS > BUILD_SHARED_LIBS > static > shared.
Другие таргеты
Большинство проектов C++ библиотек содержат примеры использования и тесты. Как правило эти компоненты размещаются в отдельных директориях (обычно /examples и /tests ), в которые я рекомендую поместить CMakeLists.txt для их сборки, чтобы не засорять основной CMakeLists.txt в корне библиотеки. В основной CMakeLists.txt остается добавить лишь вызов add_subdirectory для нужных директорий:
Таргет для тестов
Я считаю полезным, когда с тестами библиотеки можно работать не только как с частью библиотеки, но и как с самостоятельным проектом. Например, это позволяет легко собрать тесты для экземпляра библиотеки, установленного где-то в системе. Поэтому файл tests/CMakeLists.txt начинается с определения проекта для тестов, а также содержит опциональный вызов enable_testing и find_package(mylib) , если тесты собираются как stand-alone проект:
Я использую googletest, который предпочитаю подключать с помощью CMake модуля FetchContent. Документация googletest содержит подробные инструкции как это сделать, поэтому я не буду останавливаться на этом в своем руководстве (тем более, что вы возможно используете другой фреймворк).
Само определение таргета для тестов тривиально и следует тем же принципам, что и определение таргета библиотеки (замечу, что gtest_main это библиотека фреймворка googletest):
В Unix-подобных ОС в бинарном файле есть специальная запись rpath, в которой можно прописать путь к используемым динамическим библиотекам. Когда вы собираете библиотеку и тесты где-то в своей билд-директории, CMake в исполняемый файл mylib-tests добавляет rpath, указывающий на файл библиотеки. Таким образом при запуске mylib-tests динамический компоновщик сумеет найти библиотеку mylib , несмотря на то, что она не установлена по системному пути и не прописана в переменной среды PATH .
К сожалению, в Windows нет аналогичного rpath механизма. Поэтому при сборке тестов для динамической библиотеки возникает проблема с тем, что тесты не запускаются из IDE или с помощью CTest, поскольку DLL-файл библиотеки находится в билд-директории, о которой динамический загрузчик как правило ничего не знает. Чтобы обойти это ограничение, в файле cmake/utils.cmake определена функция win_copy_deps_to_target_dir , которая копирует dll-файл (а также pdb, если мы собираем дебаг-версию) в директорию с исполняемым файлом mylib-tests . В tests/CMakeLists.txt эта функция вызывается после определения таргета следующим образом:
Замечу, что когда mylib-tests собирается как stand-alone проект, я предпочитаю не копировать зависимости в его билд-директорию, поскольку dll-файл может находится в директории, куда не будет доступа на копирование.
Рассмотрим функцию win_copy_deps_to_target_dir поподробнее:
Эта функция создает кастомную команду для указанного таргета, которая будет запускаться после его сборки. Собственно копирование выполняется CMake скриптом cmake/silent_copy.cmake , который после парсинга своих аргументов вызывает:
Возможно у вас возникнет вопрос, почему потребовалось вынести конструкцию $
Также обратите внимание, что начиная с версии 3.21 CMake предоставляет выражение генератора TARGET_RUNTIME_DLLS , которое возвращает абсолютные пути ко всем динамическим библиотекам, от которых зависит таргет (напрямую или транзитивно). Это выражение позволяет значительно упростить использование функции win_copy_deps_to_target_dir , поскольку можно не передавать в нее явным образом зависимости таргета — функция сумеет определить их самостоятельно.
В заключение нам остается только сообщить CTest, какие тесты есть в нашем проекте. Для фреймворка googletest это можно выполнить следующим образом:
Функция gtest_discover_tests определенным образом запускает исполняемый файл mylib-tests , чтобы узнать имена всех тестов, которые в нем содержатся (при этом сами тесты на этом этапе не выполняются), а затем интегрировать их в CTest.
Таргеты для примеров
Таргеты для примеров библиотеки (в директории /examples ) как правило отличаются только именами, поэтому достаточно рассмотреть один:
Как и в случае с тестами, я предпочитаю дать пользователям возможность собирать примеры как отдельные проекты. Кроме того, для удобной отладки примеров под Windows используется знакомая нам по предыдущему разделу функция win_copy_deps_to_target_dir .
Пресеты (presets)
Пресеты (извините, не подобрал хорошего перевода) являются относительно новой возможностью CMake (появились в CMake 3.19), позволяющей вынести параметры сборки из CMakeLists.txt . Как уже говорилось в начале статьи, делать это нужно для того, чтобы ваши проекты без проблем собирались под разные платформы и тулчейны.
Пресет представляет из себя простой json-файл, в котором задаются различные параметры, влияющие на сборку проекта (опции конфигурации, флаги компилятора и т.д.). Существует два типа пресетов:
CMakePresets.json для хранения глобальных настроек — этот файл обычно хранится в репозитории проекта
CMakeUserPresets.json для локальных настроек разработчика — этот файл не нужно хранить в репозитории (у каждого разработчика он как правило свой), и поэтому он указывается в .gitignore
Многие IDE (например, CLion) поддерживают пресеты, позволяя выбирать нужный в своем GUI. Подробнее о пресетах можно почитать в официальной документации, здесь я лишь приведу простой пример для библиотеки mylib:
Ссылки
В заключение приведу некоторые полезные ссылки:
cpp-lib-template — шаблон для C++/CMake библиотеки, который использовался в качестве примера в этой статье
Как собрать библиотеку c из исходников
Как собирать С/С++ проекты в Microsoft Visual Studio?
В наше время open source проекты все популярнее. На площадках открытых проектов, например, на github можно найти множество полезных программ, но они не всегда имеют исполняемые файлы («exe»), поэтому я постараюсь рассказать о том, как можно собрать самостоятельно C/C++ программу, из исходников, написанную на Microsoft Visual Studio.
Первым делом нам необходимо загрузить онлайн установщик Microsoft Visual Studio, с официального сайта. Для компиляции С/С++ проектов нет необходимости во всех пакетах и можно выбрать только те, которые нам необходимы.
Установщик загрузит необходимые пакеты из интернета и установит их.
После установки Visual Studio можно убедиться, что всё работает создав тестовый проект и скомпилировав его. Для этого нажмите в меню «Файл» → «Создать» → «Проект. «
После чего появится диалог выбора типа проекта, где можно выбрать:
- Консольное приложение;
- Классическое приложение;
- Библиотеку динамической компоновки (dll);
- Статическую библиотеку;
В нашем случае для быстрой проверки подойдет консольное приложение, выбираем название и папку проекта , после чего жмём кнопку «ОК» и создается наша программа.
После этого остается остается лишь скомпилировать её, для этого нужно выбрать в меню «Сборка» и нажать на пункт «Собрать решение».
Далее наш проект скомпилируется и в папке проекта появится наш тестовый исполняемый файл («exe»).
Если всё работает как надо, то можно приступать к сборке какого-нибудь другого открытого проекта с github или другого хостинга проектов.
Первым делом нам нужно загрузить исходники проекта. На площадке github это делается довольно просто, жмем на кнопку «Code» и «Download ZIP». После чего нужно распаковать его и можно приступать к сборке.
Ищем файл с расширением «<название_проекта>.vcxproj» и запускаем его. Перед нами появится диалог в котором нам предложат обновить SDK проекта (набор библиотек для разработки, которые Microsoft периодически обновляет) и набор инструментов, жмём обновить.
Теперь наш проект можно собрать, но до сборки необходимо выбрать разрядность проекта (например, для 32 битной системы или 64 битной), а также тип сборки (отладочный режим — debug или release).
Выбираем 64 битную систему и тип сборки релиз, после чего компилируем проект. Как и ранее нужно выбрать в меню «Сборка» и нажать на пункт «Собрать решение».
Некоторые проектам требуется вручную изменить SDK и набор инструментов, на установленный у вас, для этого идём в свойства проекта, выбираем сверху типа сборки и разрядность системы и уже там изменяем SDK и набор инструментов. В выпадающем меню появляются установленные у нас версии, выбираем их и нажимаем «ОК». После чего наш проект скомпилируется.
Бывает, что проект использует сторонние библиотеки, для этого их нужно загрузить отдельно и положить в папку. Узнать путь или изменить его можно в свойстве проекта, в разделе «С/C++» → «Общие» → «Дополнительные каталоги включаемых файлов».
Бывает, что SDK или набор инструментов, в свойстве проекта не изменяется в диалоге, чтобы изменить их нужно записать номер SDK, закрыть Visual Studio и вручную, блокнотом изменить этот номер в файле проекта «<название_проекта>.vcxproj«.
При возникновении других проблем можно попробовать их загуглить, возможно, что кто-то уже сталкивался с ними и решил их.
Библиотеки
Библиотеки позволяют использовать разработанный ранее программный код в различных программах. Таким образом, программист может не разрабатывать часть кода для своей программы, а воспользоваться тем, что входит в состав библиотек.
В языке программирования C код библиотек представляет собой функции, размещенные в файлах, которые скомпилированы в объектные файлы, а те, в свою очередь, объединены в библиотеки. В одной библиотеке объединяются функции, решающие определенный тип задач. Например, существует библиотека математических функций.
У каждой библиотеки должен быть свой заголовочный файл, в котором должны быть описаны прототипы (объявления) всех функций, содержащихся в этой библиотеке. С помощью заголовочных файлов вы «сообщаете» вашему программному коду, какие библиотечные функции есть и как их использовать.
При компиляции программы библиотеки подключаются линковщиком, который вызывается gcc. Если программе требуются только стандартные библиотеки, то дополнительных параметров линковщику передавать не надо (есть исключения). Он «знает», где стандартные библиотеки находятся, и подключит их автоматически. Во всех остальных случаях при компиляции программы требуется указать имя библиотеки и ее местоположение.
Библиотеки бывают двух видов — статические и динамические. Код первых при компиляции полностью входит в состав исполняемого файла, что делает программу легко переносимой. Код динамических библиотек не входит в исполняемый файл, последний содержит лишь ссылку на библиотеку. Если динамическая библиотека будет удалена или перемещена в другое место, то программа работать не будет. С другой стороны, использование динамических библиотек позволяет сократить размер исполняемого файла. Также если в памяти находится две программы, использующие одну и туже динамическую библиотеку, то последняя будет загружена в память лишь единожды.
Далее будет описан пример, в котором создается библиотека, после чего используется при создании программы.
Пример создания библиотеки
Допустим, мы хотим создать код, который в дальнейшем планируем использовать в нескольких проектах. Следовательно, нам требуется создать библиотеку. Исходный код для библиотеки было решено разместить в двух файлах исходного кода.
Также на данный момент у нас есть план первого проекта, использующего эту библиотеку. Сам проект также будет включать два файла.
В итоге, когда все будет сделано, схема каталогов и файлов будет выглядеть так:
Пусть каталоги library и project находятся в одном общем каталоге, например, домашнем каталоге пользователя. Каталог library содержит каталог source с файлами исходных кодов библиотеки. Также в library будут находиться заголовочный файл (содержащий описания функций библиотеки), статическая (libmy1.a) и динамическая (libmy2.so) библиотеки. Каталог project будет содержать файлы исходных кодов проекта и заголовочный файл с описанием функций проекта. Также после компиляции с подключением библиотеки здесь будет располагаться исполняемый файл проекта.
В операционных системах GNU/Linux имена файлов библиотек должны иметь префикс «lib», статические библиотеки — расширение *.a, динамические — *.so.
Для компиляции проекта достаточно иметь только одну библиотеку: статическую или динамическую. В образовательных целях мы получим обе и сначала скомпилируем проект со статической библиотекой, потом — с динамической. Статическая и динамическая «разновидности» одной библиотеки по-идее должны называться одинаково (различаются только расширения). Поскольку у нас обе библиотеки будут находиться в одном каталоге, то чтобы быть уверенными, что при компиляции проекта мы используем ту, которую хотим, их названия различны (libmy1 и libmy2).
Исходный код библиотеки
В файле figure.c содержатся две функции — rect() и diagonals() . Первая принимает в качестве аргументов символ и два числа и «рисует» на экране с помощью указанного символа прямоугольник заданной ширины и высоты. Вторая функция выводит на экране две диагонали квадрата («рисует» крестик).
В файле text.c определена единственная функция, принимающая указатель на символ строки. Функция выводит на экране звездочки в количестве, соответствующем длине указанной строки.
Заголовочный файл можно создать в каталоге source, но мы лучше сохраним его там, где будут библиотеки. В данном случае это на уровень выше (каталог library). Тем самым как бы подчеркивается, что файлы исходных кодов после создания из них библиотеки вообще не нужны пользователям библиотек, они нужны лишь разработчику библиотеки. А вот заголовочный файл библиотеки требуется для ее правильного использования.
Создание статической библиотеки
Статическую библиотеку создать проще, поэтому начнем с нее. Она создается из обычных объектных файлов путем их архивации с помощью утилиты ar.
Все действия, которые описаны ниже выполняются в каталоге library (т.е. туда надо перейти командой cd). Просмотр содержимого каталога выполняется с помощью команды ls или ls -l.
Получаем объектные файлы:
В итоге в каталоге library должно наблюдаться следующее:
Далее используем утилиту ar для создания статической библиотеки:
Параметр r позволяет вставить файлы в архив, если архива нет, то он создается. Далее указывается имя архива, после чего перечисляются файлы, из которых архив создается.
Объектные файлы нам не нужны, поэтому их можно удалить:
В итоге содержимое каталога library должно выглядеть так:
, где libmy1.a — это статическая библиотека.
Создание динамической библиотеки
Объектные файлы для динамической библиотеки компилируются особым образом. Они должны содержать так называемый позиционно-независимый код (position independent code). Наличие такого кода позволяет библиотеке подключаться к программе, когда последняя загружается в память. Это связано с тем, что библиотека и программа не являются единой программой, а значит как угодно могут располагаться в памяти относительно друг друга. Компиляция объектных файлов для динамической библиотеки должна выполняться с опцией -fPIC компилятора gcc:
В отличие от статической библиотеки динамическую создают при помощи gcc указав опцию -shared:
Использованные объектные файлы можно удалить:
В итоге содержимое каталога library:
Использование библиотеки в программе
Исходный код программы
Теперь в каталоге project (который у нас находится на одном уровне файловой иерархии с library) создадим файлы проекта, который будет использовать созданную библиотеку. Поскольку сама программа будет состоять не из одного файла, то придется здесь также создать заголовочный файл.
Функция data() запрашивает у пользователя данные, помещая их в массив strs. Далее вызывает библиотечную функцию diagonals() , которая выводит на экране «крестик». После этого на каждой итерации цикла вызывается библиотечная функция text() , которой передается очередной элемент массива; функция text() выводит на экране звездочки в количестве равному длине переданной через указатель строки.
Обратите внимание на то, как подключается заголовочный файл библиотеки: через относительный адрес. Две точки обозначают переход в каталог на уровень выше, т.е. родительский по отношению к project, после чего путь продолжается во вложенный в родительский каталог library. Можно было бы указать абсолютный путь, например, «/home/pl/c/les22/library/mylib.h». Однако при перемещении каталогов библиотеки и программы на другой компьютер или в другой каталог адрес был бы уже не верным. В случае с относительным адресом требуется лишь сохранять расположение каталогов project и library относительно друг друга.
Здесь два раза вызывается библиотечная функция rect() и один раз функция data() из другого файла проекта. Чтобы сообщить функции main() прототип data() также подключается заголовочный файл проекта.
Файл project.h содержит всего одну строчку:
Из обоих файлов проекта с исходным кодом надо получить объектные файлы для объединения их потом с файлом библиотеки. Сначала мы получим исполняемый файл, содержащий статическую библиотеку, потом — связанный с динамической библиотекой. Однако с какой бы библиотекой мы не компоновали объектные файлы проекта, компилируются они как для статической, так и динамической библиотеки одинаково:
При этом не забудьте сделать каталог project текущим!
Компиляция проекта со статической библиотекой
Теперь в каталоге project есть два объектных файла: main.o и data.o. Их надо скомпилировать в исполняемый файл project, объединив со статической библиотекой libmy1.a. Делается это с помощью такой команды:
Начало команды должно быть понятно: опция -o указывает на то, что компилируется исполняемый файл project из объектных файлов.
Помимо объектных файлов проекта в компиляции участвует и библиотека. Об этом свидетельствует вторая часть команды: -L../library -lmy1. Здесь опция -L указывает на адрес каталога, где находится библиотека, он и следует сразу за ней. После опции -l записывается имя библиотеки, при этом префикс lib и суффикс (неважно .a или .so) усекаются. Обратите внимание, что после данных опций пробел не ставится.
Опцию -L можно не указывать, если библиотека располагается в стандартных для данной системы каталогах для библиотек. Например, в GNU/Linux это /lib/, /urs/lib/ и др.
Запустив исполняемый файл project и выполнив программу, мы увидим на экране примерно следующее:
Посмотрим размер файла project:
Его размер равен 8698 байт.
Компиляция проекта с динамической библиотекой
Теперь удалим исполняемый файл и получим его уже связанным с динамической библиотекой. Команда компиляции с динамической библиотекой выглядит так (одна команда разбита на две строки с помощью обратного слэша и перехода на новую строку):
Здесь в отличии от команды компиляции со статической библиотеки добавлены опции для линковщика: -Wl,-rpath. /library/. -Wl — это обращение к линковщику, -rpath — опция линковщика, ../library/ — значение опции. Получается, что в команде мы два раза указываем местоположение библиотеки: один раз с опцией -L, а второй раз с опцией -rpath. Видимо для того, чтобы понять, почему так следует делать, потребуется более основательно изучить процесс компиляции и компоновки программ на языке C.
Следует заметить, что если вы скомпилируете программу, используя приведенную команду, то исполняемый файл будет запускаться из командной строки только в том случае, если текущий каталог project. Стоит сменить каталог, будет возникать ошибка из-за того, что динамическая библиотека не будет найдена. Но если скомпилировать программу так:
, т.е. указать для линковщика абсолютный адрес, то программа в данной системе будет запускаться из любого каталога.
Размер исполняемого файла проекта, связанного с динамической библиотекой, получился равным 8544 байта. Это немного меньше, чем при компиляции проекта со статической библиотекой. Если посмотреть на размеры библиотек:
, то видно, что динамическая больше статической, хотя исполняемый файл проекта со статической библиотекой больше. Это доказывает, что в исполняемом файле, связанном с динамической библиотекой, присутствует лишь ссылка на нее.
Руководство по CMake для разработчиков C++ библиотек
В этой статье я расскажу о том, как правильно писать современные CMakeLists.txt файлы для C++ библиотек. Идеи, используемые в ней, основаны на докладе Крейга Скотта (разработчик CMake) и докладе Роберта Шумахера (разработчик vcpkg) c CppCon 2019. Поскольку мне достаточно часто приходится разрабатывать С++ библиотеки, я создал для себя небольшой шаблон cpp-lib-template, который будет использоваться в этой статье в качестве примера.
Замечу, что я предполагаю, что читатель этой статьи имеет опыт работы с CMake, однако, как и я, часто задается вопросом, как правильно ему сделать сборку своей библиотеки.
Введение
Разработчик C++ библиотеки, очевидно, должен дать своим пользователям возможность легко ее использовать. И раз уж мы пишем на компилируемом языке, то к этому относится и то, насколько быстро пользователь сможет пройти путь от клонирования ваших исходников до получения бинарного файла библиотеки под свою платформу.
Когда мы говорим о пользователях библиотеки, то в первую очередь думаем о других разработчиках, которые захотят использовать функциональность библиотеки в своих проектах. Существует два способа, которыми библиотека может интегрироваться в проект:
С помощью find_package — в этом случае библиотека должна предоставить package configuration file, который импортирует в CMakeLists.txt приложения таргет библиотеки. Этот файл, вместе с собранной библиотекой, ее публичными заголовочными файлами и некоторой другой информацией устанавливается в директорию, которая потом указывается в переменной CMAKE_PREFIX_PATH при сборке проекта.
Как подпроект, добавленный в качестве git submodule или с помощью CMake модуля FetchContent — в этом случае приложение использует обычный (не импортированный) таргет библиотеки ( find_package не вызывается), и сборка библиотеки становится этапом сборки самого проекта.
Еще одна категория пользователей библиотеки — мейнтейнеры различных пакетных менеджеров (например, vcpkg, conan и другие), которым нужно собирать библиотеку под десятки различных платформ и конфигураций. Для них важно, чтобы сборкой библиотеки можно было управлять извне, без необходимости внесения патчей в ее CMakeLists.txt.
Исходя из вышесказанного, хорошо сделанная библиотека должна удовлетворять следующим требованиям:
Единообразно интегрироваться и через find_package , и через add_subdirectory / FetchContent , т.е. импортированный таргет и обычный должны быть эквивалентны. В англоязычных источниках это требование часто формулируют как «build interface should match install interface».
В ее CMakeLists.txt не должны хардкодиться опции и флаги компиляции/компоновки кроме тех, которые абсолютно необходимы для сборки библиотеки. В противном случае мейнтейнерам менеджеров пакетов будет проблематично упаковывать библиотеку, так как с вероятностью, стремящейся к 1, на какой-то из платформ некоторые захардкоденные значения окажутся невалидны и придется делать патч для CMakeLists.txt .
Структура директорий
В своих библиотеках я придерживаюсь структуры директорий, представленной ниже. На мой взгляд, она является наиболее распространенной, кроме того, интуитивно разделяет файлы библиотеки на основные компоненты: публичные заголовки ( include/<libname> ), исходники ( src ), утилиты для сборки ( cmake ), примеры ( examples ) и тесты ( tests ).
Пример библиотеки
В дальнейшем в этой статье мы будем рассматривать простую библиотеку mylib, предоставляющую всего одну функцию add , которая возвращает результат сложения двух чисел:
Соответствующий заголовочный файл:
Наконец, файл mylib/export.h :
Файлы export_shared.h и export_static.h генерируются CMake и будут рассмотрены ниже.
Определение проекта
Первое, что нужно сделать в CMakeLists.txt — это создать проект для библиотеки:
Я предпочитаю заранее определить таргет для библиотеки, а инициализировать его ниже в коде, когда определены все необходимые параметры. Здесь же мы определим алиас для библиотеки, который должен иметь то же имя, что и импортируемый таргет. Это позволит пользователям библиотеки легко переключаться между ее подключением через find_package , в результате которого создается импортированный таргет с именем mylib::mylib , и подключением через add_subdirectory / FetchContent , который делает доступным алиас mylib::mylib в их проекте. Таким образом, приложение, использующее нашу библиотеку, в обоих случаях может линковаться к библиотеке с помощью команды:
Переменная is_top_level используется в дальнейшем в нескольких местах для определения, собирается ли библиотека как stand-alone проект или как подпроект. Версии CMake, начиная с 3.21, предоставляют переменную PROJECT_IS_TOP_LEVEL для этой же цели. Можно использовать и ее, но тогда в cmake_minimum_required придется указать версию не ниже 3.21.
Опции сборки
Сразу после определения проекта для библиотеки я рекомендую указать все опции, которые вы предоставляете для управления ее сборкой. Эта информация важна для пользователей библиотеки и поэтому лучше размещать ее в самом начале в одном месте. Типичными опциями являются:
Опции MYLIB_BUILD_* определяют, собирать или нет соответствующий компонент библиотеки. По умолчанию они выключены, потому что пользователи библиотеки, как правило, заинтересованы только в сборке самой библиотеки.
Если библиотека может быть собрана как статически, так и динамически, крайне не рекомендуется использовать кастомную переменную для определения типа сборки, потому что общепринятым стандартом является использование переменной BUILD_SHARED_LIBS , которая обрабатывается самим CMake. Следующий код является примером того, как делать не стоит:
BUILD_SHARED_LIBS , однако, влияет на все библиотеки, попадающие в сборку, а пользователям в редких случаях может понадобиться собрать несколько библиотек иначе, чем указано в BUILD_SHARED_LIBS . Поэтому среди опций библиотеки mylib есть MYLIB_SHARED_LIBS , которая может использоваться для переопределения значения BUILD_SHARED_LIBS .
Переменная MYLIB_INSTALL определяет, нужно ли генерировать таргет для установки mylib. Ее значение по умолчанию определяется тем, собирается ли mylib как отдельный проект, или как подпроект другого проекта.
Наконец, переменная MYLIB_INSTALL_CMAKEDIR позволяет указать, куда устанавливать файл конфигурации пакета (package configuration file), и предназначена в основном для мейнтейнеров менеджеров пакетов. Функция set_if_undefined определена в файле cmake/utils.cmake и аналогична set , но устанавливает значение только если переменная еще не определена (напомню, что мы не хотим переопределять любые переменные, которые установлены через командную строку CMake или в проектах верхнего уровня).
Экспорт символов
В бинарном файле любой динамической библиотеки есть раздел, в котором хранится информация о том, какие функции экспортируются данной библиотекой. Эта информация используется динамическим загрузчиком — компонентом ОС, который занимается тем, что загружает в память динамические библиотеки, нужные приложению. Помимо этого, задача динамического загрузчика заключается в том, чтобы выполнить релокации — заменить заглушки в исполняемом файле приложения на реальные адреса в памяти, куда была загружена динамическая библиотека.
В деталях описание этого процесса можно найти в статьях, ссылки на которые даны в конце поста. Сейчас же для нас важно знать, что от количества экспортируемых функций напрямую зависит, как быстро будут стартовать приложения, использующие вашу библиотеку (чем меньше экспортируемых функций, тем быстрее).
GCC и Clang по умолчанию экспортируют все символы, которые есть в библиотеке (в том числе те, что не являются частью API), что, как мы только что выяснили, негативно сказывается на скорости загрузки. CMake позволяет легко отключить это поведение, установив следующие переменные (замечу, что здесь я тоже использую функцию set_if_undefined , чтобы дать возможность пользователю при необходимости переопределить эти значения):
MSVC по умолчанию ничего не экспортирует, однако CMake предоставляет переменную CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS , которая позволяет получить поведение, аналогичное GCC и Clang. Очевидно, что использовать ее не стоит.
Для экспорта конкретного символа из библиотеки разные компиляторы предоставляют разные директивы. Чтобы не углубляться в особенности компиляторов, можно воспользоваться функцией CMake generate_export_header . Эта функция создает файл, содержащий определение макроса MYLIB_EXPORT , который нужно указывать для экспортируемых символов. Процесс генерации файла элементарен:
В результате в зависимости от типа сборки библиотеки CMake создаст в билд-директории один из двух файлов: export_shared.h или export_static.h .
Объясню, почему используются разные имена в зависимости от типа сборки. Это нужно, чтобы статическую и динамическую версию библиотеки при желании можно было установить в одну директорию. Для этого файлы должны иметь разные имена, чтобы не быть перезаписанными файлом для другого типа сборки. При этом выбор нужного можно сделать в отдельном файле ( mylib/export.h , см. выше) с помощью идентификатора MYLIB_STATIC_DEFINE , который будет определяться только для статического таргета mylib.
Исходники библиотеки
Исходники библиотеки инициализируются с помощью следующих команд:
Я предпочитаю использовать отдельную переменную для хранения публичных заголовков, потому что это лучше отражает разницу между этими файлами и может использоваться потом при создании install-таргета (см. ниже). Функция source_group заставляет CMake сгенерировать такую же структуру директорий в IDE, как и в самом репозитории библиотеки.
Таргет библиотеки
Теперь все готово для того, чтобы проинициализировать таргет библиотеки, который мы создали в самом начале. Делается это с помощью следующих команд:
Пояснения здесь требует, пожалуй, только момент связанный определением MYLIB_STATIC_DEFINE для статического таргета. Это нужно, чтобы файл mylib/export.h включал файл mylib/export_static.h , если используется статическая версия библиотеки, и файл mylib/export_shared.h в противном случае.
Обратите также внимание, что мы не пытаемся установить какие-то флаги компиляции или свойства таргета здесь. Как уже говорилось, это в дальнейшем создаст проблемы сторонним разработчикам, которые будут упаковывать вашу библиотеку под свою платформу. Например, во многих библиотеках, которые могут собираться под Windows, в свойствах таргета библиотеки часто указываются <CONFIG>_POSTFIX , чтобы бинарные файлы библиотеки, собранные под разные конфигурации (static/shared, debug/release), получали разные имена и могли устанавливаться в одну директорию. Если это сделать в CMakeLists.txt библиотеки, отказаться от выбранной разработчиком схемы будет проблематично. Вместо этого, постфиксы можно задать на этапе конфигурирования проекта с помощью переменных CMAKE_<CONFIG>_POSTFIX , указанных явно в командной строке или в пресете (preset), о которых мы поговорим позже.
Install-таргет библиотеки
Поскольку мы держим в уме тот факт, что библиотека может собираться как подпроект, мы предоставляем специальную переменную MYLIB_INSTALL , с помощью которой можно отключить генерацию install-таргета. Кроме того, самому CMake также можно указать, чтобы он не генерировал таргет для install команд с помощью переменной CMAKE_SKIP_INSTALL_RULES . По этой причине, код для создания install-таргета помещен внутрь условия:
Сам код для создания install-таргета имеет такой вид:
В этом коде все достаточно традиционно, отмечу лишь, что в команде install(TARGETS) пути, куда нужно устанавливать основные файлы, можно опустить. В этом случае CMake по умолчанию использует те, которые предоставляются модулем GnuInstallDirs (что правильно, так как их легко переопределить на этапе конфигурации при необходимости).
Кроме того, я рекомендую явно указывать компоненты библиотеки с помощью параметра COMPONENT . В любой библиотеке можно выделить как минимум два компонента:
runtime — то, что нужно, чтобы приложение, использующее библиотеку, в принципе могло запуститься ( so или dll файл); в своих библиотеках я как правило называю этот компонент также, как и саму библиотеку, т.е. <libname>
development — то, что нужно пользователю библиотеки (заголовочные файлы, библиотека импорта, файл конфигурации пакета и т.д.); для этого компонента я использую имя <libname>-dev
Разделение на компоненты позволяет в дальнейшем выполнять установку только необходимых файлов. Например, следующая инструкция установит только runtime-компонент библиотеки mylib:
Замечу, что файлы с определением импортируемых таргетов имеют разные имена в зависимости от типа сборки библиотеки. Это нужно для того, чтобы иметь возможность устанавливать статическую и динамическую версию библиотеки в одну директорию.
При создании install-таргета под Windows дополнительно указывается, как будут устанавливаться pdb-файлы, нужные для отладки библиотеки:
Здесь нужно учесть один момент. Дело в том, что msvc при компиляции генерирует pdb-файл для каждого объектного файла .obj (compiler pdb-файл), а потом компоновщик объединяет их в один pdb-файл (linker pdb-файл) для итогового исполняемого файла (например, .dll ). Когда вы собираете статическую библиотеку (которая по сути своей является таким же объектным файлом), компоновщик не вызывается вообще, только компилятор, который создает compiler pdb-файл.
CMake предоставляет простой способ получить linker pdb-файл: выражение-генератор TARGET_PDB_FILE . К сожалению, с помощью него вы не сможете получить compiler pdb-файл, когда собираете статическую версию библиотеки. В качестве хоть какого-то варианта решения этой проблемы, я предполагаю, что compiler pdb-файл лежит по тому же пути, что и статическая библиотека и имеет то же имя. Тем не менее, это не обязательно должно быть так, поэтому команда для установки pdb-файлов отмечена как OPTIONAL . Если вы можете предложить лучший вариант для обработки compiler pdb-файлов, поделитесь им в комментариях.
Файл конфигурации пакета
Файл конфигурации пакета предоставляется разработчиком библиотеки (я обычно помещаю его в директорию cmake ) и устанавливается в одну директорию с файлами, сгенерированными командой install(EXPORT) и содержащими определения импортированных таргетов библиотеки ( mylib-shared-targets.cmake и mylib-static-targets.cmake ). В этом файле как правило делаются две вещи:
включается mylib-shared-targets.cmake или mylib-static-targets.cmake
с помощью find_dependency находятся все зависимости импортированного таргета
Наша тривиальная библиотека ни от чего не зависит, поэтому ее файл конфигурации имеет такой вид:
Единственное, что заслуживает в нем внимания, это простой алгоритм, по которому определяется, какой таргет (для статической или динамической версии библиотеки) импортировать. Вкратце алгоритм можно описать так: MYLIB_SHARED_LIBS > BUILD_SHARED_LIBS > static > shared.
Другие таргеты
Большинство проектов C++ библиотек содержат примеры использования и тесты. Как правило эти компоненты размещаются в отдельных директориях (обычно /examples и /tests ), в которые я рекомендую поместить CMakeLists.txt для их сборки, чтобы не засорять основной CMakeLists.txt в корне библиотеки. В основной CMakeLists.txt остается добавить лишь вызов add_subdirectory для нужных директорий:
Таргет для тестов
Я считаю полезным, когда с тестами библиотеки можно работать не только как с частью библиотеки, но и как с самостоятельным проектом. Например, это позволяет легко собрать тесты для экземпляра библиотеки, установленного где-то в системе. Поэтому файл tests/CMakeLists.txt начинается с определения проекта для тестов, а также содержит опциональный вызов enable_testing и find_package(mylib) , если тесты собираются как stand-alone проект:
Я использую googletest, который предпочитаю подключать с помощью CMake модуля FetchContent. Документация googletest содержит подробные инструкции как это сделать, поэтому я не буду останавливаться на этом в своем руководстве (тем более, что вы возможно используете другой фреймворк).
Само определение таргета для тестов тривиально и следует тем же принципам, что и определение таргета библиотеки (замечу, что gtest_main это библиотека фреймворка googletest):
В Unix-подобных ОС в бинарном файле есть специальная запись rpath, в которой можно прописать путь к используемым динамическим библиотекам. Когда вы собираете библиотеку и тесты где-то в своей билд-директории, CMake в исполняемый файл mylib-tests добавляет rpath, указывающий на файл библиотеки. Таким образом при запуске mylib-tests динамический компоновщик сумеет найти библиотеку mylib , несмотря на то, что она не установлена по системному пути и не прописана в переменной среды PATH .
К сожалению, в Windows нет аналогичного rpath механизма. Поэтому при сборке тестов для динамической библиотеки возникает проблема с тем, что тесты не запускаются из IDE или с помощью CTest, поскольку DLL-файл библиотеки находится в билд-директории, о которой динамический загрузчик как правило ничего не знает. Чтобы обойти это ограничение, в файле cmake/utils.cmake определена функция win_copy_deps_to_target_dir , которая копирует dll-файл (а также pdb, если мы собираем дебаг-версию) в директорию с исполняемым файлом mylib-tests . В tests/CMakeLists.txt эта функция вызывается после определения таргета следующим образом:
Замечу, что когда mylib-tests собирается как stand-alone проект, я предпочитаю не копировать зависимости в его билд-директорию, поскольку dll-файл может находится в директории, куда не будет доступа на копирование.
Рассмотрим функцию win_copy_deps_to_target_dir поподробнее:
Эта функция создает кастомную команду для указанного таргета, которая будет запускаться после его сборки. Собственно копирование выполняется CMake скриптом cmake/silent_copy.cmake , который после парсинга своих аргументов вызывает:
Возможно у вас возникнет вопрос, почему потребовалось вынести конструкцию $ -E copy_if_different <files> <dest> в отдельный скрипт вместо того, чтобы просто указать ее при определении кастомной команды. Дело в том, что в режиме -E CMake выдаст ошибку, если список содержит несуществующие файлы. А это может произойти если, например, библиотека собиралась без отладочной информации (т.е. pdb-файл не был сгенерирован). Можно правильно обработать такие ситуации, но мне показалось проще написать скрипт, который просто игнорирует ошибки, связанные с отсутствующими файлами.
Также обратите внимание, что начиная с версии 3.21 CMake предоставляет выражение генератора TARGET_RUNTIME_DLLS , которое возвращает абсолютные пути ко всем динамическим библиотекам, от которых зависит таргет (напрямую или транзитивно). Это выражение позволяет значительно упростить использование функции win_copy_deps_to_target_dir , поскольку можно не передавать в нее явным образом зависимости таргета — функция сумеет определить их самостоятельно.
В заключение нам остается только сообщить CTest, какие тесты есть в нашем проекте. Для фреймворка googletest это можно выполнить следующим образом:
Функция gtest_discover_tests определенным образом запускает исполняемый файл mylib-tests , чтобы узнать имена всех тестов, которые в нем содержатся (при этом сами тесты на этом этапе не выполняются), а затем интегрировать их в CTest.
Таргеты для примеров
Таргеты для примеров библиотеки (в директории /examples ) как правило отличаются только именами, поэтому достаточно рассмотреть один:
Как и в случае с тестами, я предпочитаю дать пользователям возможность собирать примеры как отдельные проекты. Кроме того, для удобной отладки примеров под Windows используется знакомая нам по предыдущему разделу функция win_copy_deps_to_target_dir .
Пресеты (presets)
Пресеты (извините, не подобрал хорошего перевода) являются относительно новой возможностью CMake (появились в CMake 3.19), позволяющей вынести параметры сборки из CMakeLists.txt . Как уже говорилось в начале статьи, делать это нужно для того, чтобы ваши проекты без проблем собирались под разные платформы и тулчейны.
Пресет представляет из себя простой json-файл, в котором задаются различные параметры, влияющие на сборку проекта (опции конфигурации, флаги компилятора и т.д.). Существует два типа пресетов:
CMakePresets.json для хранения глобальных настроек — этот файл обычно хранится в репозитории проекта
CMakeUserPresets.json для локальных настроек разработчика — этот файл не нужно хранить в репозитории (у каждого разработчика он как правило свой), и поэтому он указывается в .gitignore
Многие IDE (например, CLion) поддерживают пресеты, позволяя выбирать нужный в своем GUI. Подробнее о пресетах можно почитать в официальной документации, здесь я лишь приведу простой пример для библиотеки mylib:
Ссылки
В заключение приведу некоторые полезные ссылки:
cpp-lib-template — шаблон для C++/CMake библиотеки, который использовался в качестве примера в этой статье
Как собрать программу, использующую стороннюю библиотеку
Имеется программа, состоящая из одного .cpp-файла. Она использует библиотеку libcurl. Необходимо скомпилировать это дело под встраиваемый Linux на процессоре Cortex A7 (arm 32-bit).
Как я понимаю, нужно сконфигурировать, а затем собрать библиотеку под нужную архитектуру, после чего собрать саму программу посредством кросс-платформенного компилятора.
Проблема в том, что я не представляю, как указать компилятору, чтобы #include понимал, что библиотека лежит где-то рядом здесь.