Ориентация главы

В главе 2 вы вкомпилировали XRT двумя строками макросов; эта глава разбирает их до конца: как прямой выбор XRT_MODULE_* разворачивается через замыкание зависимостей в точное множество возможностей, как XRT_EXCLUDE_* исключает инвазивную диагностику, сохраняя полную функциональность, и как зондируются макросы компиляции XRT_FEATURE_*. Затем версии: чем ведают макросы компиляции и рантайм-функция xrtVersion() и как заголовок с библиотекой сверяют совпадение версий. Всё это обычно невидимо, но когда что-то ломается (падение линковки, странные размеры, несоответствие заголовка и библиотеки) — виноваты всегда они; этой главой том 1 закрывается, и заодно даётся полный ответ на упражнение «сравнение трёх ступеней размера» из главы 2.

8.1引入

Одна и та же библиотека должна войти в два совершенно разных хоста: инструмент командной строки в несколько сотен килобайт, только читающий JSON-конфигурацию; и резидентный фоновый сервис, которому нужны сети, TLS и весь набор HTTP. В эпоху разрозненных зависимостей это два списка зависимостей и две конфигурации сборки; к тому же «маленький инструмент» обычно вынужден линковать весь транспортный слой, потому что гранулярность дерева зависимостей останавливается на уровне «библиотеки».

Гранулярность поставки XRT — модуль: за 91 публичным заголовком стоят сотни обрезаемых модулей, каждый объявляет свои исходники и зависимости. Вы объявляете нужные корневые модули в единице трансляции, а система сборки разворачивает замыкание — инструмент линкует свои несколько тысяч строк, сервис линкует всё; одна версия, одни заголовки. Имя механизма — прямой выбор модулей: объявлять только «что нужно», никогда «что не нужно».

В сравнении с «обратным исключением» (скомпилировать всё, потом вырезать) у прямого выбора есть решающее достоинство: новые зависимости не меняют молча вашу сборку. В обратной системе какой-нибудь модуль сегодня завёл зависимость от транспортного слоя: ваш «список исключений» не изменился, а артефакт раздался. В прямой системе ваши объявленные корни не изменились, и как бы замыкание ни менялось, это затрагивает только поддерево самого корня. Отсюда предсказуемость поставки — потому книга с главы 2 учит только прямой форме записи.

8.2概念

Замыкание зависимостей: от корневых модулей к множеству возможностей

1Объявить корневые модулиdefine XRT_MODULE_REGEX (пишете только нужное)
2Развернуть замыканиеfeatures.h по modules.json рекурсивно приносит все зависимости
3Сгенерировать макросы возможностейкаждый затронутый модуль получает определение XRT_FEATURE_*
4Точная реализацияв компиляции и линковке участвуют исходники только выбранных модулей

Четыре ключевых пункта. Только корни: таблицу зависимостей ведёт config/modules.json; пользователям не нужно и не следует её копировать — в этом смысл «прямого выбора». Макросы до включения: модульные макросы обязаны быть определены до первого включения xrt.h или xrt/features.h в этой единице трансляции; позже — не действует. Реализация один раз: XRT_IMPLEMENTATION определяется в одной единице трансляции проекта (корень ловушки 1 из главы 2). Согласованность множества: при модульной линковке все единицы трансляции одного экземпляра XRT должны использовать согласованное множество модулей — две единицы, выбравшие каждая своё, дают неопределённое поведение связанного экземпляра.

modules.json как единый источник истины заслуживает ещё одного взгляда: исходники, внутренние заголовки, зависимости, тесты и примеры каждого модуля живут в манифесте, а features.h (основа разворачивания замыкания) и справочные страницы сайта генерируются из него же. «Домены модулей» из главы 2, списки материалов главы 6 и поведение обрезки этой главы — всё указывает на одни данные: документация, код и сборка не говорят врозь; так «единый источник истины» воплощается в инженерии.

Макросы исключения: полная функциональность минус диагностика

Когда XRT_MODULE_ALL включает всё, а вам нужно исключить лишь инвазивную диагностику вроде отладки памяти (она меняет глобальный путь выделения — глава 6), используйте макрос исключения, а не перечисляйте вручную сотни прямых макросов: XRT_EXCLUDE_MEMORY_DEBUG исключает и memory_debug, и его модули отчётности, но сохраняет memory_stats. Макросы исключения действуют только на неявный выбор XRT_MODULE_ALL — при прямом выборе вы их и не выбирали.

Мотив конструкции стоит развернуть: диагностические модули существуют ровно в двух состояниях — «опциональное дополнение» или «то, что явно выключить» — без середины. Семантика XRT_MODULE_ALL — «дай мне полную продакшн-функциональность», а полной продакшн-функциональности как раз не нужно постоянно живущая отладочная куча памяти. Так макросы исключения стали стандартной записью «полная функциональность минус диагностика»: релизные сборки CI исключают ими отладочные модули, локальные сборки разработки сохраняют всё. Поняв этот слой, вы прочитаете, почему модульные макросы в заголовках примеров репозитория выглядят именно так — все они экземпляры этих семантик.

Зондирование на этапе компиляции: XRT_FEATURE_*

После разворачивания замыкания каждый включённый модуль определяет свой макрос семейства XRT_FEATURE_. Публичные заголовки широко используют #if defined(...) для условных объявлений — значит, и ваш код может зондировать возможности при компиляции: заголовок вправе давать разные inline-реализации в зависимости от наличия возможности TLS, не полагаясь на передачу флагов системой сборки. Заголовок атомарных операций главы 9 — один из первых потребителей.

Двойная колея версий и формы поставки

У версии две колеи (общий источник демонстрировался в главе 3): компиляции — XRT_VERSION_MAJOR/MINOR/PATCH и XRT_VERSION_TEXT (целочисленные макросы проверяются #if, строковый — для отображения), и рантайм — xrtVersion() возвращает строку версии, влинкованную в программу. При интеграции одним заголовком обе обязательно совпадают; при интеграции связываемой библиотекой могут разойтись — когда заголовок новый, а библиотека старая, xrtVersion() есть единственная правда, и сверка при старте программы до всякой работы — стандартное действие.

Поставка идёт двумя дорогами: однозаголовочник (все примеры книги; single/xrt.h самодостаточно содержит реализацию) и исходники/библиотеки (config/modules.json объявляет принадлежность; упаковке посвящён том 12). API на обеих дорогах полностью одинаков; переключение меняет только скрипт сборки. Три практических соображения выбора: скорость старта — однозаголовочник скопировал и поехал, безусловно быстрее всех; инкрементальность сборки — в средних и крупных проектах однозаголовочник означает, что цена перекомпиляции единицы реализации растёт вместе с размером библиотеки, а форма исходников/библиотек возвращает нормальную инкрементальность; границы символов — при поставке динамической библиотеки контролируется таблица экспорта, и сценарии, где несколько компонентов в процессе хоста линкуют XRT каждый сам, тоже опираются на библиотечную форму для изоляции. Обычный путь эволюции именно таков: «прототип — однозаголовочником, инженеризация — библиотекой», и ни одна строка кода при смене дороги не меняется.

8.3示例

Первая программа: минимальное множество модулей и сравнение размеров

Трёхступенчатое сравнение главы 2 получает здесь полную версию — одна программа, две ступени модулей:

/* hello.c —— обслуживает обе ступени: модульные макросы внедряются -D из командной строки */
#include <xrt.h>
#include <stdio.h>

int main(void)
{
	printf("XRT %s\n", xrtVersion());
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXRT_IMPLEMENTATION -I single hello.c -lws2_32 -liphlpapi$ gcc -O1 -DXRT_IMPLEMENTATION -I single hello.c$ ls -l a.exe a2.exe-rwxr-xr-x 1 admin 106496 a.exe-rwxr-xr-x 1 admin 12288 a2.exe(体积因版本与工具链而异,比值才是重点)

Что только что произошло. Модульные макросы переехали в -D командной строки — в точности эквивалентно записи в исходнике, а выгода в том, что один исходник компилируется в несколько ступеней (стандартная поза матричных тестов обрезки в CI). Ступень -DXRT_MODULE_ALL втягивает все модули (включая сети/TLS/HTTP, потому и линкуются системные сетевые библиотеки); вторая ступень не пишет ни одного модульного макроса — фундамент core присутствует всегда и не обрезаем, xrtVersion принадлежит core, поэтому достаточно определить XRT_IMPLEMENTATION, и системные сетевые библиотеки не нужны. Различие в разы показывает: «обрезка» — это не экономия на нескольких функциях, а вход и выход целых поддеревьев зависимостей.

Вторая программа: зондирование возможностей при компиляции

/* probe.c —— зондируем, какие возможности развернуло замыкание */
#define XRT_MODULE_STRING
#define XRT_IMPLEMENTATION
#include <xrt.h>
#include <stdio.h>

int main(void)
{
	printf("version=%s\n", xrtVersion());
#if defined(XRT_FEATURE_STRING)
	printf("feature: string = yes\n");
#else
	printf("feature: string = no\n");
#endif
#if defined(XRT_FEATURE_TLS)
	printf("feature: tls = yes\n");
#else
	printf("feature: tls = no\n");
#endif
	return 0;
}
$ gcc -O1 -DXRT_MODULE_STRING -DXRT_IMPLEMENTATION -I single probe.c -o probe.exe$ ./probe.exeversion=2.0.0-devfeature: string = yesfeature: tls = no

Что только что произошло. Объявлен только XRT_MODULE_STRING; после разворачивания замыкания XRT_FEATURE_STRING определён, а XRT_FEATURE_TLS нет — результаты зондирования соответствуют выбору один к одному. Ценность паттерна — в заголовках и шаблонном коде: различия возможностей разветвляются на этапе компиляции, без рантайм-расходов и без дополнительных флагов системы сборки. Учтите: макросы зондирования обязаны проверяться в той же единице трансляции, что и xrt.h (сначала включить, затем проверять), а при передаче между заголовками сохраняйте стабильный порядок включения.

Связав оба примера, получаем полную картину модульной системы: -D командной строки решает «что выбрано» (поставка), зондирование XRT_FEATURE_ отражает «что выбранось» (возможности), а xrtVersion() докладывает «кто оказывает услугу» (версия). Трое замыкаются в одной единице трансляции — вам не нужно вести никакого списка «что включено в проект» вне скрипта сборки: макросы и зонды и есть список, а компилятор сверяет согласованность за вас.

8.4契约

  • Макросы до включения: модульные макросы и XRT_IMPLEMENTATION обязаны быть определены до первого включения xrt.h/features.h.
  • Реализация ровно один раз: одна единица реализации на проект; остальные единицы включают только объявления.
  • Согласованность множества: все единицы трансляции одного экземпляра XRT используют согласованное множество модулей.
  • Граница макросов исключения: XRT_EXCLUDE_* подавляет только неявный выбор XRT_MODULE_ALL и не затрагивает прямые объявления.
  • Сверка версий: при интеграции связываемой библиотекой при старте сравните xrtVersion() с макросами компиляции; расхождение — ошибка развёртывания.

Типичные ошибки

Ловушка 1: модульные макросы определены после включения:Ловушка 1: модульные макросы определены после включения

Симптомы: макрос явно определён, а поведение словно нет — отсутствие символов при линковке или невключённые возможности, без внятной ошибки.

Причина: разворачивание макросов происходит в точке включения; определения позже #include не действуют на уже завершённое включение.

错误写法
#include <xrt.h>          /* сначала включение: замыкание уже развернуто «без выбора» */
#define XRT_MODULE_STRING /* поздно — string в этой единице не включится */
正确写法
#define XRT_MODULE_STRING /* объявить сначала */
#define XRT_IMPLEMENTATION
#include <xrt.h>
Ловушка 2: две единицы трансляции выбирают каждая своё:Ловушка 2: две единицы трансляции выбирают каждая своё

Симптомы: причуды времени выполнения — объект ведёт себя нормально на одних путях вызова и падает на других; или отсутствие/дублирование символов при линковке.

Причина: единица A выбрала XRT_MODULE_NET_TCP, единица B только core — структуры, которые видит B при компиляции, расходятся с раскладкой A (различия условных объявлений); в одном экземпляре XRT уживились два мировоззрения.

错误写法
/* a.c */                /* b.c */
#define XRT_MODULE_ALL   #define XRT_MODULE_STRING
#include <xrt.h>         #include <xrt.h>
/* две единицы с несогласованными множествами: влинкованы в один экземпляр, раскладки разошлись */
正确写法
/* common.h —— единственная точка объявления модулей всего проекта */
#define XRT_MODULE_ALL
/* common.c: #define XRT_IMPLEMENTATION затем include (единственная единица реализации) */
/* a.c / b.c: включают common.h напрямую; множество всегда согласовано */

Практические задания

Базовое: тройной зонд

Скомпилируйте probe.c для трёх ступеней «без модульных макросов», «XRT_MODULE_STRING», «XRT_MODULE_ALL»; запишите результат зонда tls для каждой и объясните происхождение различий одним предложением.

Продвинутое: матрица обрезки

Скомпилируйте hello.c в пяти ступенях (без модулей / string / regex / string+regex / all); для каждой запишите, нужен ли -lws2_32, и размер бинарника, оформив таблицей. Подсказка: модульные макросы внедряются -D, выходные файлы различайте -o.

Вызов: выбор для маленького инструмента

CLI-инструмент, которому нужно лишь «читать JSON-конфигурацию + писать журнал»: подберите минимальное множество модулей, проверьте компиляцию, линковку и работу, зафиксируйте итоговый размер бинарника и его отношение к ступени XRT_MODULE_ALL. Критерий приёмки: у каждого макроса множества есть обоснование в одно предложение; отношение ниже четверти; функциональная проверка выводит корректно.

Итоги

ТемаКраткая справка
Прямой выборобъявлять только корневые модули (семейство XRT_MODULE_); замыкание зависимостей разворачивает автоматически; поставка предсказуема
Порядок макросоввсе макросы до первого включения xrt.h / features.h; поздние определения не действуют
РеализацияXRT_IMPLEMENTATION один раз на проект, в единственной единице реализации
ИсключениеXRT_EXCLUDE_MEMORY_DEBUG и др. подавляют только неявный полный выбор XRT_MODULE_ALL
Зондированиезамыкание определяет семейство XRT_FEATURE_; развилка на компиляции без рантайм-расходов
Версиямакросы компиляции проверяются #if; при библиотечной интеграции сначала сверка xrtVersion() при старте
Поставкаоднозаголовочник (single/) и исходники/библиотеки (modules.json) с одним API; прототип — заголовком, инженеризация — библиотекой
Тройка-D выбирает поставку, XRT_FEATURE_ зондирует возможности, xrtVersion() сверяет версию — замкнуто в одной единице

📖 Справочники API в этой главе:core (25 записей)memory_stats (5 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки