# Глава 11.2. Подключение библиотек и модулей
В этой главе мы обсудим, как собирать проекты, использующие модули и библиотеки. Для этого нужно разобраться в правилах видимости символов для компилятора, вариантах линковки и в том, что из себя представляет ABI.
## Видимость символа для линкера
У термина «линковка» два значения. Первое вы уже знаете: это завершающий этап сборки программы, на котором из объектных файлов создаётся исполняемый файл или библиотека. Но есть и второе значение. Линковка (linkage) — это _свойство_ символа, определяющее его видимость для линкера.
Символ может быть вовсе не доступен для линкера, то есть отсутствовать в таблице символов. В таком случае говорят, что у него **нет линковки** (no linkage). Например, её нет у локальных переменных, полей классов, параметров функций.
Символ может быть доступен линкеру только внутри своей единицы трансляции. Это значит, что у него **внутренняя линковка** (internal linkage). Например, внутренней линковкой [обладают](https://timsong-cpp.github.io/cppwp/n4950/basic#link-3) константные глобальные переменные.
```cpp
#include <print>
const std::size_t buffer_size = 2048; // внутренняя линковка
int main()
{
std::println("{}", buffer_size);
}
```
А у не константных глобальных переменных **внешняя линковка** (external linkage). Символ со внешней линковкой доступен из любой единицы трансляции. Во всей программе у него должно быть только одно определение, иначе нарушится ODR. По умолчанию внешняя линковка есть у функций, перечислений и классов.
И, наконец, с появлением в C++20 модулей появилась [модульная линковка](https://en.cppreference.com/w/cpp/language/storage_duration.html#Module_linkage) (module linkage). Символы с модульной линковкой доступны из любой единицы трансляции, принадлежащей модулю. Подробнее мы рассмотрим этот вид линковки в главе про модули.
В некоторых случаях может потребоваться заменить внутреннюю линковку на внешнюю. Например, если в проекте есть глобальная константа, определённая в `cpp`-файле одной единице трансляции и использующаяся в других единицах трансляции. Сделать её доступной, просто подключив хедер, не получится, ведь она определена в `cpp`-файле.
Чтобы задать такой переменной внешнюю линковку, при её определении используется ключевое слово [extern](https://en.cppreference.com/w/c/language/storage_class_specifiers.html):
```cpp
// net.cpp
#include <print>
extern const std::size_t buffer_size = 2048; // внешняя линковка
// ...
```
В `cpp`-файле, внутри которого планируется использовать эту переменную, она объявляется также с участием спецификатора `extern`. Это означает, что переменная определена в другой единице трансляции. Если линкер не найдёт определения такой переменной, сборка проекта завершится с ошибкой:
```cpp
// main.cpp
#include <print>
// Только объявление. Определение в другом файле:
extern const std::size_t buffer_size;
int main()
{
std::println("{}", buffer_size);
}
```
```
2048
```
Итак, ключевое слово `extern` используется, чтобы заменить внутреннюю линковку на внешнюю. Порой возникает обратная задача: замена внешней линковки на внутреннюю. У не константных глобальных переменных внешняя линковка. Чтобы это изменить, глобальная переменная объявляется статической:
```cpp
static std::size_t bits = 16;
```
Как видите, спецификатор `static` для локальных и глобальных переменных имеет совершенно [разный смысл.](/courses/cpp/chapters/cpp_chapter_0091/#block-static)
Есть и второй способ замены внешней линковки на внутреннюю: размещение сущности внутри анонимного пространства имён (unnamed namespace). У такого пространства имён нет имени, и для доступа к объявленным внутри него сущностям не требуется использовать оператор разрешения области видимости `::`.
```cpp
import std;
namespace // анонимное пространство имён
{
void f() // внутренняя линковка вместо внешней
{
std::println("Function in unnamed namespace");
}
}
int main()
{
f();
}
```
```
Function in unnamed namespace
```
## Статическая и динамическая линковка {#block-static}
В зависимости от вида подключаемых библиотек линковаться с ними можно статически или динамически.
Статическая линковка означает полное включение библиотеки в результирующий бинарный файл. При статической линковке на момент сборки есть вся необходимая информация, чтобы разрешить межфайловые зависимости. Динамическая линковка подразумевает подгрузку используемой библиотеки каждый раз при запуске программы. Поэтому даже если сборка программы прошла успешно, необходимо, чтобы на целевой машине была установлена нужная библиотека. Все библиотеки проверяются на совместимость по ABI.
## C++ ABI
[ABI](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D1%8B%D0%B9_%D0%B8%D0%BD%D1%82%D0%B5%D1%80%D1%84%D0%B5%D0%B9%D1%81_%D0%BF%D1%80%D0%B8%D0%BB%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D0%B9) (application binary interface) — интерфейс между бинарными компонентами. Например, между библиотекой и подключающей её программой.
В мире C и C++ ABI фиксирует подробности реализации языка. Например, стандарт C++ описывает синтаксис функций, но не указывает, как в функцию передаются параметры — в регистрах процессора, по стеку или комбинированно. Этим заведует ABI. Стандарт C++ определяет, что такое классы и как их использовать. ABI определяет, как представлены поля класса в памяти компьютера: их расположение, порядок, [выравнивание.](https://ru.wikipedia.org/wiki/%D0%92%D1%8B%D1%80%D0%B0%D0%B2%D0%BD%D0%B8%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5_%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85)
Стандарт языка разрабатывается комитетом по стандартизации, а ABI — вендорами компиляторов.
Совместимость по ABI важна и для статических, и для динамических библиотек. Если два компилятора на одной и той же платформе будут следовать разным ABI, то собранный этими компиляторами код не удастся слинковать. Кстати, между GCC и Clang нет полной совместимости по ABI. Код, собранный разными версиями одного и того же компилятора, тоже может быть не совместим по ABI.
## Библиотеки {#block-libraries}
Библиотеки C++ бывают трёх видов:
- Статические (archive, архив).
- Динамические (shared library, разделяемая библиотека).
- Header-only (состоящие только из заголовочных файлов).
У любой библиотеки есть хедеры. Они содержат интерфейс библиотеки — объявления функций и других сущностей. По месту их использования требуется подключать соответствующие им хедеры.
**Статические** библиотеки имеют расширение `.a` под *nix и `.lib` под Windows. На этапе линковки они становятся частью бинарника, который их использует. Это увеличивает размер программы, но не создаёт внешних зависимостей. Такие библиотеки иногда называют архивами, потому что они представляют из себя несколько объектных файлов, скомпонованных вместе.
**Динамические** библиотеки имеют расширение `.so` (shared object) под *nix и `.dll` (dynamic link library) под Windows. Они не становятся частью программы, а подгружаются во время исполнения. Несколько исполняемых файлов совместно могут использовать один и тот же файл библиотеки. Поэтому динамические библиотеки и называют разделяемыми. Такой подход экономит место, но требует установки на целевую систему библиотеки нужной версии. {#block-dynamic-libs}
**Header-only** библиотеки состоят _только_ из заголовочных файлов. Их удобно подключать к проекту, ведь дополнительной линковки с такой библиотекой не требуется. Код header-only библиотеки копируется препроцессором в файлы реализации проекта по месту директивы `#include`.
## Рантайм C++
В языках семейства C есть **рантайм** — набор библиотек, реализующих часть описанных в стандарте языка возможностей, его [модель исполнения](https://en.wikipedia.org/wiki/Execution_model) и функции для корректного запуска программы. Рантайм C++ помимо прочего реализует механизм обработки исключений, операторы `new` и `delete` для выделения и освобождения памяти, а также нешаблонный код стандартной библиотеки.
Не путайте концепцию рантайма в C и C++ с рантаймом в [управляемых языках,](https://ru.wikipedia.org/wiki/%D0%91%D0%B8%D0%B1%D0%BB%D0%B8%D0%BE%D1%82%D0%B5%D0%BA%D0%B0_%D1%81%D1%80%D0%B5%D0%B4%D1%8B_%D0%B2%D1%8B%D0%BF%D0%BE%D0%BB%D0%BD%D0%B5%D0%BD%D0%B8%D1%8F) таких как Java, C# и python. В них рантайм — это полноценная среда выполнения программы, укомплектованная сборщиком мусора и виртуальной машиной.
Программы на C++ линкуются не только с рантаймом C++, но и с рантаймом C. Зачем? Дело в том, что рантайм C++ опирается на рантайм C. Например, операторы `new` и `delete` в C++ зачастую реализованы через сишные функции `malloc` и `free`.
С рантаймом можно линковаться динамически и статически. По умолчанию линковка динамическая. С рантаймом можно не линковаться вовсе. Тогда в программе будет доступно минимальное подмножество языка. Это имеет смысл при разработке [встраиваемых систем](https://ru.wikipedia.org/wiki/%D0%92%D1%81%D1%82%D1%80%D0%B0%D0%B8%D0%B2%D0%B0%D0%B5%D0%BC%D0%B0%D1%8F_%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0) (embedded systems), драйверов и низкоуровневых программ, запускаемых на устройствах без ОС.
Библиотека с рантаймом C++ от GCC называется [libstdc++](https://gcc.gnu.org/onlinedocs/libstdc++/index.html), от Clang — [libc++](https://libcxx.llvm.org/), а от Microsoft — [STL](https://github.com/microsoft/STL). Также эти библиотеки содержат реализацию стандартной библиотеки.
Библиотека glibc (GNU C Library) реализует рантайм C. При линковке рантайма, как и при линковке любой другой библиотеки, встаёт необходимость совместимости по ABI. Если в программе статически линкуются две библиотеки, использующие несовместимые между собой версии рантайма, то компиляция завершится с ошибкой.
Помимо рантайма языка есть ещё **рантайм компилятора.** Он содержит определения функций, неявно используемые компилятором для поддержки операций, которых нет на целевой машине. Например, операции над 64-битными числами на 32-битных архитектурах.
Реализация рантайма зависит от компилятора, его версии, от целевой системы и архитектуры процессора. Допустим, программа собрана компилятором GCC с опциями по умолчанию. Можно ли её запустить на такой же платформе, но с рантаймом от Clang? Нет, У рантайма GCC и Clang нет полной совместимости по ABI.
Посмотрите, какие библиотеки подгружает бинарник `main`. Для этого примените к нему утилиту `ldd` (list dynamic dependencies):
```shell
ldd main
```
```
linux-vdso.so.1 (0x00007fcb4735d000)
libc++.so.1 => /lib/x86_64-linux-gnu/libc++.so.1 (0x00007fcb47210000)
libc++abi.so.1 => /lib/x86_64-linux-gnu/libc++abi.so.1 (0x00007fcb471d2000)
libunwind.so.1 => /lib/x86_64-linux-gnu/libunwind.so.1 (0x00007fcb471c4000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fcb46fe3000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fcb46f03000)
/lib64/ld-linux-x86-64.so.2 (0x00007fcb4735f000)
```
Простой бинарник, выводящий в консоль "Hello compiler", зависит от нескольких динамических библиотек! Причем их количество, названия и пути к ним _будут отличаться_ на разных системах. В данном случае нас больше всего интересуют библиотеки, названия которых похожи на следующие:
- [ld.so](https://www.opennet.ru/man.shtml?topic=ld.so&category=8&russian=0). Это часть рантайма C. Ищет и подгружает используемые в программе динамические библиотеки, подготавливает программу к запуску. Также здесь содержится код, инициализирующий глобальные переменные и вызывающий функцию `main()` — точку входа в программу.
- [libm.so](https://packages.debian.org/search?searchon=contents&keywords=libm.so&mode=path&suite=stable&arch=any) — ещё одна часть рантайма C, которая отвечает за реализацию математических функций из хедера `math.h`. Она была вынесена в отдельный файл по историческим причинам.
- [libc.so](https://www.man7.org/linux/man-pages/man7/libc.7.html) — реализация стандартной библиотеки C.
- [libc++](https://packages.debian.org/ru/sid/libstdc++6) — Реализация рантайма и стандартной библиотеки C++ в Clang.
## Сборка проекта с модулями
Артефакты сборки модулей зависят от компилятора. Но в целом работа с модулями выглядит следующим образом: {#block-bmi}
1. Для модуля компилируется его BMI (Built Module Interface) — бинарный файл с [интерфейсом модуля.](/courses/cpp/chapters/cpp_chapter_0103/#block-interface) Он содержит экспортируемые объявления. Одно из распространённых расширений для BMI-файлов — это `.pcm` (Precompiled Module).
2. При сборке проекта повсюду, где импортируется модуль, компилятор задействует этот BMI. Это гораздо эффективнее, чем подключение хедеров препроцессором.
### Модуль стандартной библиотеки
Чтобы импортировать модуль `std` в своих проектах, для начала нужно получить его BMI. Делается это единожды.
Так выглядит компиляция интерфейса модуля `std.cppm` с сохранением результата в файл с BMI `std.pcm`. Обратите внимание на флаг `--precompile`:
```bash
clang \
-std=c++23 \
-O3 \
-nostdinc++ \
-isystem /usr/lib/llvm-20/include/c++/v1/ \
-Wno-reserved-module-identifier \
--precompile \
-o /usr/local/lib/std.pcm \
/usr/lib/llvm-20/share/libc++/v1/std.cppm
```
Флаг `-Wno-reserved-module-identifier` нужен, чтобы компилятор не показывал предупреждение о том, что имя модуля совпадает со стандартным: `warning: 'std' is a reserved name for a module`.
Теперь в файле нашего проекта `main.cpp` мы можем заменить директиву препроцессора `#include <print>` на выражение `import std;`:
```cpp
// main.cpp
import std;
int main()
{
std::println("Hello compiler");
}
```
Чтобы собрать файл, импортирующий модуль, нужно передать компилятору опцию `-fmodule-file`. Она указывает путь к BMI и имеет формат `-fmodule-file=<module-name>=<BMI-path>`. После первого знака `=` указывается имя модуля, после второго — путь:
```bash
clang \
-std=c++23 \
-O3 \
-fmodule-file=std=/usr/local/lib/std.pcm \
-lc++ \
-isystem /usr/lib/llvm-20/include/c++/v1/ \
-nostdinc++ \
-o main \
main.cpp
```
### Пользовательские модули
Создайте модуль `hello_compiler.cppm`, функция из которого вызывается в `main.cpp`. Содержимое файлов [приведено](/courses/cpp/chapters/cpp_chapter_0103/#block-project-modules) в прошлой главе.
```
├── hello_compiler.cppm
└── main.cpp
```
При сборке такого проекта вначале нужно получить BMI модуля `hello_compiler`. Так как внутри него присутствует импорт модуля `std`, нужно указать путь к `std.pcm`:
```bash
clang \
-std=c++23 \
-O3 \
-fmodule-file=std=/usr/local/lib/std.pcm \
--precompile \
-o hello_compiler.pcm \
hello_compiler.cppm
```
Теперь соберём бинарник `main` с указанием пути до двух BMI: стандартной библиотеки и нашего модуля.
```bash
clang \
-std=c++23 \
-O3 \
-fmodule-file=std=/usr/local/lib/std.pcm \
-fmodule-file=hello_compiler=hello_compiler.pcm \
-lc++ \
-o main \
main.cpp hello_compiler.pcm
```
-----
## Резюме
- От того, какая у символа линковка, зависит его видимость для линкера.
- Есть четыре варианта линковки символа:
- отсутствие линковки (no linkage),
- внутренняя линковка (internal linkage),
- внешняя линковка (external linkage),
- модульная линковка (module linkgage).
- Линковаться с библиотеками можно статически или динамически.
- ABI (Application Binary Interface) — интерфейс между бинарными компонентами, фиксирующий детали реализации языка.
- В мире C и C++ рантайм — это набор библиотек, реализующих часть описанных в стандарте языка возможностей, его модель исполнения и функции для запуска программы.
- BMI (Built Module Interface) — бинарный файл с интерфейсом модуля, содержащий все экспортируемые объявления.
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!