Глава 5 / 51
Том 1 · Старт и ментальная модель

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

Каталоги C-проектов можно организовывать как угодно. Эта глава даёт проверенные в demo-single соглашения — это не обязательный стандарт, но они лучше всего сцеплены с семантикой изоляции поколений горячей перезагрузки.

Обзор

Позиционирование: отвечаем на вопрос «куда класть код». После главы у каждого вашего файла будет однозначное место; главы тома 3 о протоколах прямо используют эти соглашения. Читателям с большим C-инженерным стажем стоит сфокусироваться на 5.3 — «почему одна единица трансляции безопасна».

5.1 Введение

Первый вопрос нового приложения XServer — не «как написать обработчик», а «как разложить файлы». В C нет пакетного менеджера, который прочертит границы: пути include, видимость символов, разбиение единиц компиляции — всё на самодисциплине. Беспорядок в организации в сценарии горячей перезагрузки усиливается: перезагружается вся единица трансляции, и расположение любого файла влияет на компиляцию каждого поколения.

demo-single даёт лёгкие соглашения: каталог — это ответственность, одна единица трансляции, самодостаточные модули. На них держится полное приложение с 19 маршрутами, SQLite, шаблонами и контролируемыми страницами, и они выдержали нагрузку routebench с десятью тысячами маршрутов — соглашения хватает по масштабу.

5.2 Каталожные соглашения

Дерево каталогов demo-single проверено на реальной машине источник: release/demo-single/
myapp/
├── xs.exe          /* единственный бинарник (хост) */
├── xs.json         /* факты сборки: сервисы/порты/TLS/vhost */
├── main.c          /* вход приложения: include-оркестровка + жизненный цикл */
├── route.h         /* реестр маршрутов: URI × метод → обработчик */
├── includes/       /* dev_inc: дополнительный путь поиска заголовков скрипта */
├── librarys/       /* dev_lib: сторонние библиотеки (#pragma comment) */
├── modules/        /* переиспользуемые модули: http / db / template / resource / protocol */
├── route_http/     /* обработчики запросов: файл на ресурс */
├── page/           /* контролируемые страницы: вне wwwroot, без прямых ссылок */
├── template/       /* шаблоны xrtTemplate */
├── db/             /* файлы данных SQLite */
└── wwwroot/        /* корень статики (единственный каталог, доступный по URL) */
КаталогОтветственностьКритерий
modules/Переиспользуемые возможности вне бизнесаМожно скопировать в другое приложение как есть
route_http/Бизнес-обработчикиСсылается на modules, его никто не ссылается
page/ template/Материалы динамического контентаТолько через скрипт, недоступны по URL
wwwroot/СтатикаЕдинственный каталог, доступный по URL
db/Персистентные данныеПерезагрузка его не трогает, общий для поколений

Обратите внимание на контраст двух последних строк: wwwroot — «доступен по URL», db — «межпоколенческий» — соглашения кодируют одновременно границу доступа и границу жизненного цикла.

5.3 Оркестровка одной единицы трансляции

Всё приложение сливается в одну единицу трансляции: main.c подключает всё в порядке зависимостей. Оркестровка демо:

include-оркестровка main.c (фрагмент) источник: release/demo-single/main.c
/* 1. контракт SDK */
#include <xsbase.h>
/* 2. переиспользуемые модули (снизу вверх) */
#include "modules/resource.h"
#include "modules/http.h"
#include "modules/database.h"
#include "modules/template.h"
#include "modules/protocol.h"  /* каркас маршрутизации: последним, зависит от всех выше */
/* 3. бизнес-обработчики */
#include "route_http/basic.h"
#include "route_http/item.h"
/* 4. реестр маршрутов (ссылается на все обработчики) */
#include "route.h"

Почему одна единица трансляции безопасна

  • Стоимость компиляции удерживается: TCC быстр; полная компиляция входа демо в 2,6 КБ — одна строка лога script loaded (2.6 KB gen N); приложение routebench с десятью тысячами маршрутов компилируется так же мгновенно.
  • Простейший мир символов: нет меж-.c границ линковки; видимость static- и глобальных функций очевидна; нет скрытой ямы «в двух единицах компиляции по одноимённому глобалу».
  • Естественное сцепление с изоляцией поколений: смена поколения = перекомпиляция всей единицы; все статики обновляются комплектом, не бывает сборки «наполовину новое, наполовину старое».

Статики изолированы по поколениям

Это важнейшая семантика данных соглашений. Исходник модуля маршрутизации демо говорит прямо:

modules/protocol.h (комментарий дословно) источник: release/demo-single/modules/protocol.h
/* Горячая перезагрузка создаёт новый набор статических переменных;
 * старое поколение выполняет ServiceUnit только после ухода всех запросов. */

Иначе говоря: таблица маршрутов в route.h, дескриптор G_DB в database.h — всё это состояние поколения. После смены новое поколение стартует с чистой статической области (ServiceInit заново строит таблицу маршрутов, переоткрывает базу), а комплект старого уничтожается при обнулении ссылок. То, что должно пережить смену (счётчики соединений, кэши), передаётся явно через ServiceSwap (глава 28).

5.4 Паттерн переиспользования modules

Хороший модуль отвечает требованию «скопировал в другое приложение — и работает». Возьмём ReplyJSON из modules/http.h как пример границы:

modules/http.h · пример границы модуля источник: release/demo-single/modules/http.h
/* ReplyJSON лишь заимствует pObject; xrtValueRelease остаётся за вызывающим. */
static bool ReplyJSON(XS_HttpReq* pReq, uint16 iStatus, const xvalue* pObject)
{
    /* сериализация → ReplyRaw пишет заголовок и тело → владение оговорено в комментарии */
    ...
}

Что только что произошло — все три признака приличного модуля в пяти строках:

  • Зависимость только вниз: xsbase.h + xrt, ничего из route_http;
  • Комментарий владения: первая строка явно говорит «заимствует, не забирает» — вызывающему не нужно гадать про освобождение;
  • static-функции: в одной единице трансляции static = «приватно для модуля», без засорения пространства имён приложения.

5.5 Контракт

  • Зависимости: порядок include = порядок зависимостей (SDK → modules → route_http → route.h); между модулями тоже строго однонаправленно.
  • Именование: в одной единице трансляции все static-символы делят общее пространство имён — символы модуля несут префикс модуля (например RouteHTTP_, DB_, Template_).
  • Жизненный цикл: статики принадлежат поколению; файловые ресурсы (db/, page/, template/) общие для поколений и при смене не пересоздаются.

Частые ошибки

Два модуля объявили одноимённые static-функции
Симптом
Компиляция скрипта падает сразу: ошибка повторного определения.
Причина
В одной единице трансляции все .h разворачиваются в одну область видимости; одноимённые static-функции неизбежно конфликтуют — это не проблема линковщика, взрыв происходит на компиляции.
✗ Ошибка
/* modules/a.h */ static void Init(void);
/* modules/b.h */ static void Init(void);
/* main.c включает оба → ошибка компиляции */
✓ Правильно
static void A_Init(void);
static void B_Init(void);  /* префиксы модулей */
Межпоколенческие данные в статической переменной
Симптом
После горячей перезагрузки счётчики обнуляются, кэши исчезают — «я же не перезапускал».
Причина
Статики изолированы по поколениям: смена поколения = новый комплект. Это не баг, а семантика — но если вы ждали продолжения, вы попали.
✗ Ошибочное ожидание
static uint64 G_iTotal;  /* всего запросов
 * после перезагрузки = 0, «данные пропали» */
✓ Правильно
/* экспорт через ServiceSwap → возврат через xsSwapTake
 * (полная реализация в главе 28) */
Файл для публикации положили в page/
Симптом
Прямой переход по пути файла даёт 404 — «файл же лежит».
Причина
По URL доступен только wwwroot; page/ — контролируемый каталог, чтение только через LoadPage в скрипте (глава 19).
✗ Ошибка
/* страницу загрузок положили в page/download.html
 * и ждёте /download.html */
✓ Правильно
/* публичные ресурсы → wwwroot/
 * контролируемые → page/ + LoadPage в маршруте */

Упражнения

БазовыйМедосмотр каталогов

По критериям 5.2 проверьте приложение из упражнения главы 4: у каждого ли файла однозначное место? Нет ли смешения (шаблоны и статика в одном каталоге)?

Приёмка: таблица «файл → каталог → основание» без строк со смешением.
ПродвинутыйМодуль counter

Создайте modules/counter.h с Counter_Inc() / Counter_Get(): префикс модуля, зависимости только от стандартной библиотеки, полные комментарии владения. Вызовите из двух разных обработчиков.

Приёмка: counter.h, скопированный в пустой каталог (с зависимостями не шире include xsbase.h), компилируется; оба обработчика видят один счётчик.
ВызовПроверка изоляции поколений

Накапливайте счётчик запросов модулем counter; после рестарта убедитесь в обнулении; затем прочитайте объявление ServiceSwap демо (XS_ServiceSwapProc в sdk/xsbase.h) и набросайте межпоколенческую передачу счётчика (достаточно псевдокода).

Приёмка: вы можете объяснить, какое значение передавать, какое сбрасывать и кто освобождает объект, экспортированный Swap.

Итоги

ТезисВывод
Каталог = ответственностьmodules (переиспользование) / route_http (бизнес) / page·template (контроль) / wwwroot (публикация) / db (между поколениями)
Одна единица трансляцииmain.c подключает всё по порядку зависимостей; стоимость компиляции TCC удерживается
Изоляция поколенийСтатики принадлежат поколению; продолжение жизни — только через ServiceSwap
Признаки модуляЗависимость вниз, комментарии владения, префиксные static-символы
Граница доступаПо URL — только wwwroot; остальное через скрипт
Что дальше по xrt