Бібліотеки в CMake: STATIC, SHARED, INTERFACE та видимість PUBLIC/PRIVATE
Бібліотеки в CMake: STATIC, SHARED, INTERFACE та видимість PUBLIC/PRIVATE
🎯 Мета лекції
- Розрізняти статичні, динамічні та header-only бібліотеки і знати їхні розширення на різних ОС.
- Винести спільний код у власну бібліотеку і підключити її до програми.
- Пояснити, що означають
PUBLIC,PRIVATE,INTERFACEу командахtarget_*. - Перемикати тип бібліотеки через
BUILD_SHARED_LIBSбез зміни коду.
🔑 Ключові терміни
- Статична бібліотека (static library): архів
.aабо.lib, код якого копіюється у виконуваний файл при лінкуванні. - Динамічна бібліотека (shared library):
.so,.dylibабо.dll, яка завантажується під час запуску. - Видимість (visibility): правило, чи передається налаштування цілі тим, хто її використовує.
- Транзитивна залежність (transitive dependency): налаштування, що проходять через ланцюжок цілей.
Минулого разу всі .cpp потрапляли в одну програму. Якщо той самий код потрібен кільком програмам (наприклад, основній і тестовій), його виносять у бібліотеку: це збірка функцій і класів, яку підключають до різних програм.
Типи бібліотек
CMake підтримує три основні види бібліотек, які створюються командою add_library:
| Тип | Результат | Коли код потрапляє в програму | Розмір програми | Файли потрібні при запуску |
|---|---|---|---|---|
STATIC | .a (Linux, macOS), .lib (Windows) | Під час лінкування, копіюється | Більший | Ні |
SHARED | .so (Linux), .dylib (macOS), .dll (Windows) | Під час запуску програми | Менший | Так, поруч або в системному шляху |
INTERFACE | Файлів не створює | Не потрапляє: лише заголовки | Залежить від коду заголовків | Ні |
- Статична бібліотека вбудовується в програму цілком. Програма самодостатня, але якщо бібліотеку використовують десять програм, її код дублюється десять разів.
- Динамічна бібліотека існує окремим файлом. Кілька програм можуть ділити одну копію, а оновлення бібліотеки не вимагає перекомпіляції програм. Платимо тим, що файл потрібно доставити разом із програмою.
- INTERFACE призначена для header-only бібліотек: у них весь код у
.h(типовий приклад — шаблонні бібліотеки), тому компілювати нічого. Ціль потрібна, щоб передати іншим цілям шляхи до заголовків і прапорці.
Власна бібліотека: проєкт
Винесемо математичні й рядкові функції в бібліотеку, а main.cpp залишимо програмою, яка їх викликає. Функції згруповані в просторах імен MathUtils та StringUtils, щоб не конфліктувати з іншим кодом.
CMakeLists.txt зі статичною бібліотекою
cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0.0)
set(CMAKE_CXX_STANDARD 17)
# Створюємо статичну бібліотеку
add_library(MyUtils STATIC
mylib/math_utils.cpp
mylib/math_utils.h
mylib/string_utils.cpp
mylib/string_utils.h
)
# Де шукати заголовкові файли бібліотеки
target_include_directories(MyUtils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
# Створюємо виконуваний файл
add_executable(MyProject main.cpp)
# Зв'язуємо програму з бібліотекою
target_link_libraries(MyProject MyUtils)
Збірка показує порядок: спершу бібліотека, потім програма, яка від неї залежить. CMake визначив цей порядок сам з рядка target_link_libraries.
Розбір команд
add_library(MyUtils STATIC ...)
Створює ціль-бібліотеку з іменем MyUtils. Ключове слово STATIC задає тип. На диску з'явиться libMyUtils.a (на Windows MyUtils.lib).
target_include_directories(MyUtils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
Додає папку до шляхів пошуку заголовків. Слово PUBLIC означає, що її матимуть не лише файли самої бібліотеки, а й усі, хто з нею лінкується. Завдяки цьому в main.cpp працює #include "mylib/math_utils.h".
target_link_libraries(MyProject MyUtils)
Каже, що MyProject залежить від MyUtils. Крім лінкування, команда передає програмі всі PUBLIC та INTERFACE налаштування бібліотеки. Нижче цей механізм розібрано докладно.
Динамічна бібліотека
Перехід на динамічну бібліотеку на рівні CMakeLists.txt зводиться до заміни одного слова:
cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0.0)
set(CMAKE_CXX_STANDARD 17)
# SHARED: динамічна бібліотека
add_library(MyUtils SHARED
mylib/math_utils.cpp
mylib/math_utils.h
mylib/string_utils.cpp
mylib/string_utils.h
)
target_include_directories(MyUtils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
add_executable(MyProject main.cpp)
target_link_libraries(MyProject MyUtils)
# На Windows .dll має лежати поруч з .exe
if(WIN32)
add_custom_command(TARGET MyProject POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:MyUtils>
$<TARGET_FILE_DIR:MyProject>
)
endif()
На macOS та Linux цей файл збереться без змін, а otool -L (на Linux ldd) покаже залежність програми від динамічної бібліотеки:
Особливості Windows
На Windows .dll має додаткові обмеження:
- Експорт символів. У Linux та macOS усі функції бібліотеки видимі за замовчуванням. У Windows DLL нічого не експортує, доки функції явно не позначені
__declspec(dllexport). Без експорту лінкер програми не знайде жодної функції бібліотеки. - Розташування
.dll. Windows шукає DLL у папці виконуваного файла та вPATH. Тому вище командаPOST_BUILDкопіює.dllпоряд із.exe. Вираз$<TARGET_FILE:MyUtils>— це generator expression: CMake підставляє на його місце реальний шлях до файлу бібліотеки на етапі генерації.
Для навчальних проєктів експорт можна автоматизувати однією змінною, яку слід встановити до add_library:
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
CMake сам згенерує список експортів для всіх функцій і методів. Обмеження: глобальні змінні так не експортуються. Для промислових бібліотек використовують модуль GenerateExportHeader: він створює заголовок із макросом MYUTILS_EXPORT, який розмічає тільки публічний API.
target_compile_definitions(MyUtils PRIVATE BUILDING_DLL). Він працює лише тоді, коли у заголовках є макрос, що перетворює BUILDING_DLL на dllexport. Сам по собі такий рядок нічого не експортує.Вибір типу бібліотеки через опцію
Якщо в add_library не вказати тип, CMake обере його за змінною BUILD_SHARED_LIBS. Це дозволяє перемикати тип із командного рядка без правок CMakeLists.txt:
cmake_minimum_required(VERSION 3.10)
project(MyProject)
set(CMAKE_CXX_STANDARD 17)
# Опція для вибору типу бібліотеки
option(BUILD_SHARED_LIBS "Build shared libraries" OFF)
# Тип не вказано: CMake вибере сам
add_library(MyUtils
mylib/math_utils.cpp
mylib/string_utils.cpp
)
target_include_directories(MyUtils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
add_executable(MyProject main.cpp)
target_link_libraries(MyProject MyUtils)
cmake -S . -B build
cmake --build build
cmake -S . -B build -DBUILD_SHARED_LIBS=ON
cmake --build build
Результат на macOS: у першому випадку в папці збірки створюється libMyUtils.a, у другому libMyUtils.dylib. Обидві програми виводять однакові результати.
BUILD_SHARED_LIBS працює і для сторонніх бібліотек, підключених через add_subdirectory або FetchContent: вони також зберуться як SHARED, якщо їхні CMakeLists.txt не вказують тип явно. Тому змінюйте її на рівні командного рядка, а не всередині проєкту.Видимість: PUBLIC, PRIVATE, INTERFACE
Команди target_include_directories, target_compile_definitions, target_compile_options та target_link_libraries приймають слово, яке визначає, хто побачить налаштування.
add_library(MyLib source.cpp)
# PUBLIC: потрібно і самій MyLib, і тим, хто її використовує
target_include_directories(MyLib PUBLIC include/)
target_compile_definitions(MyLib PUBLIC USE_FEATURE=1)
# PRIVATE: потрібно тільки всередині MyLib
target_include_directories(MyLib PRIVATE internal/)
target_compile_definitions(MyLib PRIVATE INTERNAL_DEBUG=1)
# INTERFACE: самій MyLib не потрібно, але потрібно користувачам
target_include_directories(MyLib INTERFACE external/)

| Слово | Бачить сама бібліотека | Бачать ті, хто її використовує |
|---|---|---|
PRIVATE | Так | Ні |
PUBLIC | Так | Так |
INTERFACE | Ні | Так |
Практична перевірка: що буде, якщо помилитися
Теорію легко забути, тому подивимось на реальні помилки. Бібліотека greet має заголовок у власній папці include/, а програма включає його через <greet/greet.h>:
Заголовок потрібен і самій бібліотеці (greet.cpp), і програмі (main.cpp). Тому правильний варіант — PUBLIC. Подивимось, що буде з двома іншими словами:
PRIVATE. Бібліотека знайшла свій заголовок, але програма — ні, бо налаштування їй не передалося.INTERFACE. Навпаки: програма отримала шлях, а самій бібліотеціgreet.cppшлях не потрібен за правиломINTERFACE, і вона не зібралася.PUBLIC. Шлях діє в обох місцях.
#include), потрібен PUBLIC. Якщо він потрібен лише для .cpp самої бібліотеки, потрібен PRIVATE. INTERFACE застосовують для бібліотек без власного коду (header-only) та для налаштувань, які бібліотека лише передає.Транзитивність
Видимість діє ланцюжком. Якщо app лінкується з greet через PRIVATE, а greet лінкується з fmt через PUBLIC, то app теж отримає fmt. Якби greet лінкувала fmt через PRIVATE, то fmt залишився б прихованою деталлю реалізації, і app про нього б не знала.
Header-only бібліотеки
Бібліотека без .cpp описується як INTERFACE:
add_library(MathLib INTERFACE)
target_include_directories(MathLib INTERFACE include/)
target_compile_features(MathLib INTERFACE cxx_std_17)
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE MathLib)
Усі налаштування для такої цілі позначаються INTERFACE, бо в самої цілі немає коду, який би їх використовував. target_compile_features(... cxx_std_17) вимагає стандарт C++17 від усіх, хто підключить бібліотеку.
Псевдоніми цілей
Рекомендується створювати псевдонім із простором імен у стилі Проєкт::Ціль:
add_library(MyProject_MyUtils STATIC mylib/math_utils.cpp)
add_library(MyProject::MyUtils ALIAS MyProject_MyUtils)
target_link_libraries(MyApp PRIVATE MyProject::MyUtils)
Переваги: запис однаковий для власних і зовнішніх бібліотек (fmt::fmt, Boost::filesystem), а CMake розрізняє ціль і звичайну бібліотеку: якщо ви помилитесь в імені цілі з ::, CMake видасть помилку замість того, щоб мовчки шукати файл бібліотеки з такою назвою.
Питання для самоперевірки
PUBLIC та INTERFACE налаштування. Коли ви лінкуєте програму з такою ціллю, CMake передає їх програмі автоматично. Саме тому в main.cpp не довелося окремо викликати target_include_directories..lib із функціями, і лінкер програми повідомить про нерозв'язані символи. Вирішення: позначити API макросом dllexport через GenerateExportHeader або тимчасово ввімкнути CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS.Так. Слова-ключі діють на всі аргументи після них, поки не зустрінеться наступне слово:
target_link_libraries(MyLib
PUBLIC fmt::fmt
PRIVATE Threads::Threads
)
Резюме
📚 Типи
STATIC вбудовується в програму, SHARED завантажується під час запуску, INTERFACE описує бібліотеки лише із заголовків.🔗 target_link_libraries
PUBLIC та INTERFACE налаштування бібліотеки: шляхи, макроси, прапорці.👁️ Видимість
PRIVATE лише для бібліотеки, INTERFACE лише для користувачів, PUBLIC для обох. Помилка у виборі проявляється як «file not found».🎛️ BUILD_SHARED_LIBS
STATIC або SHARED. Змінюється командою -DBUILD_SHARED_LIBS=ON.find_package, add_subdirectory, FetchContent або pkg-config.Проєкт із кількома файлами: перелік джерел і file(GLOB)
Три способи вказати файли проєкту: явний перелік, змінні SOURCES і HEADERS, автоматичний пошук через file(GLOB). Чому GLOB ризикований і як працює CONFIGURE_DEPENDS.
Підключення зовнішніх бібліотек: find_package, add_subdirectory, FetchContent, pkg-config
П'ять способів підключити чужу бібліотеку до CMake-проєкту, режими Module і Config у find_package, автоматичне завантаження через FetchContent, ручне підключення і pkg-config, порівняльна таблиця.