CMake і vcpkg

Бібліотеки в CMake: STATIC, SHARED, INTERFACE та видимість PUBLIC/PRIVATE

Три типи бібліотек, створення власної бібліотеки, динамічні бібліотеки на Windows, вибір типу через BUILD_SHARED_LIBS, поширення налаштувань між цілями за допомогою PUBLIC, PRIVATE і INTERFACE.

Бібліотеки в 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.

cmake --build build
[ 60%] Built target MyUtils
[ 80%] Building CXX object CMakeFiles/MyProject.dir/main.cpp.o
[100%] Linking CXX executable MyProject
[100%] Built target MyProject
$ ./build/MyProject
=== Math Utils ===
5 + 3 = 8
10 * 2 = 20
2^10 = 1024
=== String Utils ===
Original: Hello World
Upper: HELLO WORLD
Lower: hello world
Split: apple banana cherry date

Розбір команд

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) покаже залежність програми від динамічної бібліотеки:

otool -L build/MyProject
$ otool -L build/MyProject
build/MyProject:
@rpath/libMyUtils.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 2200.27.0)

Особливості Windows

На Windows .dll має додаткові обмеження:

  1. Експорт символів. У Linux та macOS усі функції бібліотеки видимі за замовчуванням. У Windows DLL нічого не експортує, доки функції явно не позначені __declspec(dllexport). Без експорту лінкер програми не знайде жодної функції бібліотеки.
  2. Розташування .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

Результат на 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. Подивимось, що буде з двома іншими словами:

три варіанти target_include_directories
PRIVATE app/main.cpp:2:10: fatal error: 'greet/greet.h' file not found
PUBLIC [100%] Built target app → Hello, CMake!
INTERFACE greet/src/greet.cpp:1:10: fatal error: 'greet/greet.h' file not found
  • 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 видасть помилку замість того, щоб мовчки шукати файл бібліотеки з такою назвою.

Питання для самоперевірки

Резюме

📚 Типи

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.
Copyright © 2026