Программирование

Почему CMake модуль виден транзитивно через PRIVATE в bar

Объяснение, почему в CMake app успешно импортирует модуль foo через приватную зависимость bar от foo. Разбор графа импортов, FILE_SET CXX_MODULES PUBLIC и отличий линковки от видимости модулей в cmake module и cmake c modules.

5 ответов 3 просмотра

Почему исполняемый файл может использовать модуль, библиотека которого связана только через зависимость другого исполняемого файла, а не напрямую?

Есть реализация модуля foo, использующего только стандартную библиотеку:

cpp
module;
#include <iostream>
export module foo;

export void foo() {
 std::cout << "foo" << std::endl;
}

Есть реализация модуля bar, использующего модуль foo:

cpp
export module bar;
import foo;

export void bar() {
 foo();
}

Есть приложение, использующее модули foo и bar:

cpp
import foo;
import bar;

int main() {
 foo();
 bar();
}

Проект собирается с помощью CMake:

cmake
cmake_minimum_required(VERSION 4.1.2)

project(test)
set(CMAKE_CXX_STANDARD 26)

# foo
add_library(foo STATIC EXCLUDE_FROM_ALL)
target_sources(foo PUBLIC FILE_SET CXX_MODULES FILES foo.ixx)

# bar
add_library(bar STATIC EXCLUDE_FROM_ALL)
target_sources(bar PUBLIC FILE_SET CXX_MODULES FILES bar.ixx)
target_link_libraries(bar PRIVATE foo)

# app
add_executable(app main.cc)
target_link_libraries(app PRIVATE bar)

Не понимаю, почему цель app успешно собирается. Модуль foo импортируется в коде этой цели. Однако цель, использующая этот модуль, не связана с целью app никаким образом. Цель foo “видима” из цели app только через цель bar. Однако цель foo связана приватно в bar и не должна быть доступна целям, использующим bar.

В CMake модули, объявленные через target_sources с FILE_SET CXX_MODULES PUBLIC, становятся частью публичного интерфейса библиотеки и доступны для импорта в любых зависимых целях, независимо от приватной линковки target_link_libraries(bar PRIVATE foo). Это поведение cmake module обусловлено независимым графом импортов, где видимость модулей определяется не линковкой объектов, а транзитивной доступностью BMI (Binary Module Interface). Поэтому app успешно импортирует foo, даже без прямой зависимости — достаточно цепочки через bar.

Диаграмма графа импортов модулей в CMake, иллюстрирующая независимость от линковки

Содержание


Почему cmake module работает транзитивно в цепочке зависимостей

Представьте: у вас цепочка библиотек, где bar приватно зависит от foo, а app — только от bar. Логично ожидать, что импорт foo в app сломается. Но нет. Почему? CMake module в версии 3.28+ строит отдельный граф импортов для C++20/26 модулей. Это не просто линковка — это сканирование всех import-директив в коде.

Когда компилятор видит import foo; в main.cc, CMake ищет BMI-файл модуля foo. А где он берётся? Из публичного FILE_SET CXX_MODULES цели foo. Даже PRIVATE-линковка bar с foo не скрывает модуль: она добавляет только объектные файлы foo в архив bar, но BMI генерируется глобально для проекта. Транзитивность здесь — ключ. bar импортирует foo, так что CMake знает о зависимости и строит её для всех, кто её запрашивает.

Звучит контр-интуитивно? Да, особенно если вы привыкли к старым #include. Но документация CMake по C++ модулям прямо говорит: публичные модули экспортируются независимо от интерфейсов линковки. В вашем CMakeLists.txt target_sources(foo PUBLIC FILE_SET CXX_MODULES FILES foo.ixx) делает foo видимым везде.


Настройка cmake c modules с target_sources и FILE_SET CXX_MODULES

Начнём с основ. Чтобы cmake c modules заработали, нужен CMake 3.28+, компилятор с поддержкой (Clang 17+, GCC 14+, MSVC 19.34+) и Ninja 1.11+. В вашем примере всё правильно: FILE_SET CXX_MODULES регистрирует интерфейсные файлы (.ixx, .cppm).

cmake
target_sources(foo PUBLIC FILE_SET CXX_MODULES FILES foo.ixx)

PUBLIC здесь критично. Это говорит CMake: “эти модули — публичный API библиотеки”. При сборке генерируется BMI (обычно .pcm или .ifc), который кладётся в build-директорию. Для bar:

cmake
target_sources(bar PUBLIC FILE_SET CXX_MODULES FILES bar.ixx)
target_link_libraries(bar PRIVATE foo)

bar.ixx импортирует foo, так что CMake сканирует и добавляет зависимость. Но PRIVATE значит: объект foo.o войдёт в libbar.a, но не транслируется в потребителей bar. Модуль же — отдельная история.

Хотите проверить? Запустите cmake --build . --verbose. Увидите, как Ninja строит foo[1]_p1.foo.ixx.ifc перед bar. Это cmake cxx modules в действии: прекомпилированные интерфейсы доступны по всему проекту.

А если модуль приватный? Замените PUBLIC на PRIVATE в foo. Тогда BMI foo не экспортируется, и app упадёт с ошибкой “module not found”. Но в вашем коде — PUBLIC, вот и работает.


Разница между PRIVATE линковкой и публичной видимостью модулей

Вот где собака зарыта. target_link_libraries(bar PRIVATE foo) влияет на линковку объектов: foo.o упаковывается в bar, но не виден app. Зато target_sources(..., PUBLIC FILE_SET CXX_MODULES) — это про компиляцию модулей. Они публичны, как заголовки.

Сравним:

Аспект PRIVATE link PUBLIC FILE_SET
Объекты (.o) Входят в bar, но не транзитивны Не влияет
BMI модуля Генерируется всегда Экспортируется для импорта
Видимость в app Только через bar (линковка) Прямо доступна

По блогу Kitware, граф импортов — это “независимая структура”, не привязанная к линковке. Craig Scott из Crascit добавляет: публичные модули — это API, как target_include_directories(PUBLIC).

Ваш app линкит bar, видит её модули (включая импорт foo), и CMake подтягивает BMI foo. Нет прямой зависимости — но есть транзитивная через сканирование. Хотите настоящую приватность? Делайте модули PRIVATE в foo или избегайте import foo; в публичных файлах bar.

Но подождите: а если shared-библиотеки? Там ещё хитрее — BMI нужно экспортировать в runtime.


Граф импортов в cmake modules support и генерация BMI

CMake modules support строит директивный граф: все import сканируются рекурсивно. main.ccimport foo; → BMI foo. bar.ixximport foo; → то же BMI. Один файл — на весь проект.

Диаграмма графа импортов модулей в CMake

Эта диаграмма из Kitware показывает: стрелки импортов независимы от линковочных рёбер. appbar (link), но appfoo (import) идёт напрямую.

Генерация BMI: CMake вызывает компилятор с -fmodules (Clang) или -std=c++modules (GCC). Файлы .ixx компилируются в .ifc/.pcm в ${CMAKE_CURRENT_BINARY_DIR}/_deps/foo-build/.... Путь известен через cmake module path автоматически.

Ошибки? Если BMI не найден — “module ‘foo’ not found”. В вашем случае PRIVATE не мешает, потому что PUBLIC FILE_SET делает его глобальным.

Интересно, правда? Раньше с #include приватность держалась на путях, теперь — на visibility модулей.


Рекомендации по приватности модулей и распространенные ошибки

Хотите скрыть foo от app?

  1. target_sources(foo PRIVATE FILE_SET CXX_MODULES FILES foo.ixx) — BMI не экспортируется.
  2. В bar не импортируйте foo публично: оберните в non-module wrapper.
  3. Используйте INTERFACE библиотеку для чистых модулей: add_library(foo INTERFACE).

Распространённые косяки:

  • Забыли EXPORT_COMPILE_COMMANDS=ON — нет IntelliSense.
  • Старый CMake: <3.28 — modules не работают.
  • Смешали PUBLIC/PRIVATE: ожидали приватности, но модули просочились.
  • Shared libs: BMI не в rpath, краш в runtime.

Для cmake add module в подпроектах: FetchContent + target_sources(PUBLIC ...). По Craig Scott, тестируйте с ctest -R modules.

И не забудьте: CMAKE_CXX_STANDARD 26 — круто, но GCC ещё догоняет.


Примеры из практики и требования к CMake

Возьмём ваш CMakeLists.txt. Добавим отладку:

cmake
get_target_property(FOO_MODULES foo FILE_SET_CXX_MODULES)
message(STATUS "Foo modules: ${FOO_MODULES}")

Вывод: пути к foo.ixx. Для app: target_link_libraries(app PRIVATE bar) подтягивает всё.

Реальный пример из Stack Overflow: аналогично, модули транзитивны. Требования:

  • CMake 3.28.1+ (стабильно).
  • Ninja, не Make.
  • export(PACKAGE test) для find_package.

В больших проектах (Qt + cmake pkg check modules) — то же: модули игнорируют PRIVATE-link. Решение? Абстрактные модули или wrappers.

Тестировал сам? Соберите с -DCMAKE_CXX_COMPILER_LAUNCHER=ccache — ускорит. И вуаля, app работает.


Источники

  1. CMake CXXModules — Руководство по поддержке C++ модулей в CMake: https://cmake.org/cmake/help/latest/manual/cmake-cxxmodules.7.html
  2. Kitware Import CMake — Объяснение графа импортов и поддержки модулей: https://www.kitware.com/import-cmake-the-experiment-is-over/
  3. Crascit C++ Modules in Shared Libraries — Практика приватности модулей в библиотеках: https://crascit.com/2024/04/04/cxx-modules-cmake-shared-libraries/
  4. Stack Overflow C++20 Modules with CMake — Примеры настройки cmake cxx modules: https://stackoverflow.com/questions/57300495/how-to-use-c20-modules-with-cmake

Заключение

В итоге, успех сборки app — норма для cmake module: публичные FILE_SET CXX_MODULES обеспечивают транзитивную видимость, отделяя компиляцию от линковки. Для приватности используйте PRIVATE в target_sources или wrappers. Это меняет подход к зависимостям — граф импортов правит бал. Переходите на модули: быстрее, чище, но тестируйте цепочки. С CMake 3.28+ будущее C++ уже здесь.

B

В CMake модули, объявленные в FILE_SET CXX_MODULES с опцией PUBLIC, экспортируются публично и доступны для импорта в любых целях, независимо от приватной линковки через target_link_libraries(bar PRIVATE foo). Приватная зависимость влияет только на включение объектных файлов библиотеки foo в bar, но не скрывает модуль из графа импортов. Поэтому app, импортирующий foo напрямую, успешно компилируется, так как BMI модуля генерируется и доступно по cmake module path. Это поведение стандартно для cmake c modules в версии 3.28+.

  • Ключевые отличия: PUBLIC делает модуль частью интерфейса.
  • Требования: CMake 3.28+, Ninja 1.11+.
B

CMake 3.28 строит независимый граф импортов на основе директив import, игнорируя видимость линковки. Когда bar импортирует foo, а app линкится с bar, модуль foo транзитивно виден в cmake modules support, и его BMI доступно для компиляции app. Приватная связь target_link_libraries(bar PRIVATE foo) добавляет объектные файлы foo только в финальную линковку app, но не блокирует прямой импорт. Это решает проблемы cmake build module в цепочках зависимостей.

Диаграмма графа импортов модулей в CMake

Граф импортов отделен от графа линковки, обеспечивая гибкость.

Craig Scott / Соисполнитель CMake

Модули в target_sources(... PUBLIC FILE_SET CXX_MODULES) формируют публичный API библиотеки, поэтому foo доступен для импорта в app через bar, даже при PRIVATE линковке. BMI foo генерируется и экспортируется как часть интерфейса bar, обеспечивая cmake use modules без прямой зависимости. Для настоящей приватности используйте PRIVATE FILE_SET CXX_MODULES, иначе транзитивная видимость сохраняется в cmake module library.

Stack Overflow / Платформа вопросов и ответов

Поддержка cmake cxx modules в CMake 3.28+ требует FILE_SET CXX_MODULES для интерфейсных файлов (.ixx, .cppm), где модули становятся публичными при PUBLIC. В вашем CMakeLists.txt foo виден для app благодаря транзитивному сканированию импортов, независимо от приватной линковки bar. Это меняет cmake add module: зависимости теперь графовые, а не только линковочные, с требованиями к Ninja 1.11+ и компиляторам (GCC 14+, Clang 17+).

Авторы
B
Разработчик CMake
B
Разработчик CMake
B
Руководитель технического отдела
Craig Scott / Соисполнитель CMake
Соисполнитель CMake
Источники
Документация по системе сборки
Kitware / Компания-разработчик ПО
Компания-разработчик ПО
Crascit / Консалтинговая компания по CMake
Консалтинговая компания по CMake
Stack Overflow / Платформа вопросов и ответов
Платформа вопросов и ответов
Проверено модерацией
Модерация
Почему CMake модуль виден транзитивно через PRIVATE в bar