xs3 · xrt 2.0 · контрактный заголовок xsbase.h

Руководство разработчика

Полный путь от сборки до горячей перезагрузки. Все поля конфигурации, сигнатуры контракта и примеры кода на этой странице сверены с исходниками основной ветки (config.h / xsbase.h / demo-single).

Быстрый старт

Три действия: собрать хост → написать xs.json и исходники C-приложения → запустить.

1. Сборка хоста

Нужны GCC (переопределяется через --cc) и Python 3. Позиционные аргументы выбирают варианты расширений; Windows / Linux — единый процесс:

build.bat                /* минимальный вариант */
build.bat sqlite xtp     /* выбрать по имени: SQLite и XTP2 */
build.bat all            /* все десять опциональных библиотек одним ключом */
build.sh all             /* эквивалент для Linux */
xs --version             /* версия и список вкомпилированных расширений */

Артефакт атомарно заменяется только после успешной линковки — неудачная сборка всегда оставляет последний рабочий xs. Результат — один исполняемый файл со встроенными компилятором TCC и полным набором заголовков SDK.

2. Минимальная конфигурация и приложение

/* xs.json — реальная конфигурация из demo-single */
{
    "services": [
        {
            "enabled": true,
            "class": "http",
            "name": "demo",
            "ip": "127.0.0.1",
            "port": 9080,
            "host_default": {
                "enabled": true,
                "name": "app",
                "path": "wwwroot",
                "devlang": "c",
                "devfile": "main.c"
            }
        }
    ]
}
/* main.c — минимальный работающий скрипт */
#include <xsbase.h>

static void ServiceInit(XS_HostInfo* pHost)
{
    printf("[%s] service init\n", pHost->Name);
}

static XS_RequestResult RequestProc(XS_HttpReq* pReq)
{
    /* необработанные запросы падают в статический слой (wwwroot), остальное 404 */
    return XS_FALLBACK;
}

3. Запуск

./xs           /* по умолчанию читает xs.json из каталога приложения */
./xs my.json   /* или указать конфиг явно */

Лог старта: [xs] config loaded → engine started → script loaded → по одному bound на порт → [xs] running. Откройте http://127.0.0.1:9080/ — получите статические страницы из wwwroot.

Структура приложения

Приложение XServer = exe хоста + один каталог исходников. Структуру определяете вы; ниже — проверенная в demo-single организация:

myapp/
├── xs.exe          /* единственный бинарник (5,2 МБ, компилятор и SDK внутри) */
├── xs.json         /* сервисы · порты · TLS · vhost */
├── main.c          /* вход приложения: include-оркестровка + ServiceInit/Unit */
├── route.h         /* реестр регистрации маршрутов */
├── modules/        /* переиспользуемые модули (http/db/template/resource…) */
├── route_http/     /* обработчики запросов */
├── page/           /* контролируемые страницы (вне wwwroot, без прямых ссылок) */
├── template/       /* шаблоны xrtTemplate */
├── db/             /* файлы данных SQLite */
└── wwwroot/        /* корень статики */

Все исходники сливаются в одну единицу трансляции (main.c подключает всё) и компилируются встроенным TCC в машинный код. Статические переменные изолированы по поколениям скрипта: перезагрузка создаёт новый набор статиков, а ServiceUnit старого поколения выполняется только после ухода последнего запроса — это и есть семантическая основа горячей перезагрузки.

Дополнительные заголовки скриптов кладите в каталог dev_inc, сторонние библиотеки — в dev_lib (объявляются в xs.json), TCC ищет их автоматически; #pragma comment(lib, ...) ссылается на них напрямую.

Справочник конфигурации (xs.json)

Загрузка конфигурации по принципу fail-fast: любое недопустимое поле прерывает старт с ошибкой, называющей конкретное поле, — процесс никогда не работает «с дефектом».

Поля корневого уровня

ПолеТип / ограничениеОписание
engine.workersuint, 0–1024Число рабочих потоков движка, 0 = значение xrt по умолчанию. Неизменяемо при горячей перезагрузке; изменения отклоняются
servicesмассив, обязателенПеречень сервисов; отсутствие = ошибка старта

Поля уровня сервиса (services[i])

ПолеТип / ограничениеОписание
enabledbool, по умолчанию trueВключён ли сервис
classбелый списокhttp / ws / tcp / udp / custom
nameобязательно, глобально уникальноКлюч для xsServerFind
ipпо умолчанию 0.0.0.0Адрес прослушивания
port1–65535, обязательно кроме customПорт открытого текста
tlsboolВключить TLS-порт (для udp запрещено)
port_tlsпо умолчанию 443Старт отклоняется при конфликте с портом открытого текста на том же IP
ip_tlsпо умолчанию совпадает с ipАдрес прослушивания TLS
backloguint32Передаётся напрямую в конфигурацию слушателя xrt
recv_limit≤SIZE_MAX; udp ≤65535Предел чтения потока
path / devlang / devfile / dev_inc / dev_lib—Скриптовые значения по умолчанию уровня сервиса, сливаются в DefaultHost

Поля уровня хоста (host_default / hosts[i])

ПолеТип / ограничениеОписание
enabledboolВключён ли хост
nameуникально внутри server, непустоеИмя хоста
hostдомены через точку с запятойОбязательно и непусто при включённых vhost для http/ws; маршрутизация по заголовку Host / SNI
pathпутьКорень статики / скриптов
devlangstatic / c / свойstatic = чистая статика; c = скрипт TCC
devfileпутьВходной файл скрипта
tls_ca / tls_cert / tls_keyпути PEMСертификаты уровня хоста → источник данных выбора по SNI; горячая замена
dev_inc / dev_libкаталоги через точку с запятойДополнительные пути поиска include / библиотек для скриптов (относительно appPath)

Ручки приложения (поля Custom)

Предустановленной схемы нет — ключи зарезервированы для чтения приложением (через server->Custom / xsConfigRoot()). Соглашения хоста и демо:

ПолеУровеньОграничение / умолчание
header_limitserverПредел заголовков запроса
path_limitserverПредел длины пути, по умолчанию 2048
body_limitserverПредел тела запроса (413 при превышении)
idle_timeoutserverТайм-аут простоя (http/ws/tcp); сервер закрывает по истечении
ws_message_limitserverПредел сообщений WS; превышение закрывает с кодом 1009
ws_protocolserverОбязательный подпротокол (валидируется при старте)
static.headershost≤8 полей; 8 зарезервированных заголовков запрещены (вкл. Transfer-Encoding)
static.indexhostМассив индексов каталога, перебор по порядку (по умолчанию index.html)
static.error_pageshostСтатусы 400–599 → относительный путь кастомной страницы ошибки
static.deny_dotfileshostПо умолчанию true: файлы с точкой в начале всегда отклоняются (вкл. обход путей)

Все файловые пути переводятся в абсолютные относительно каталога файла конфигурации — каталог конфигурации самодостаточен и переносится целиком.

Контракт скриптов

Хост ищет колбэки по имени символа в скомпилированном результате; все необязательны и объявляются по мере надобности. Это и есть всё соглашение между XServer и приложением — без базовых классов и макросов регистрации.

СимволПротоколыСигнатура (xsbase.h)
ServiceInitвсеvoid (XS_HostInfo*) инициализация после загрузки
ServiceUnitвсеvoid (XS_HostInfo*) деструкция при утилизации, не более одного раза за жизнь
ServiceSwapвсеbool (XS_HostInfo*, xvalue**) экспорт снимка состояния перед заменой
RequestProchttp/httpsXS_RequestResult (XS_HttpReq*) вход запроса
WsOpen / WsText / WsBinaryws/wss(XS_HostInfo*, xwsstream*[, представление сообщения])
WsPing / WsPong / WsClosews/wssто же; Close добавляет код состояния uint16 и представление причины
EventOpen / EventData / EventClosetcp/tcps(XS_HostInfo*, XS_StreamConn*[, xnetbuf*/результат])
EventDgramudp(XS_HostInfo*, xnetudp*, const xnetudpmessage*)

Трёхзначный возврат RequestProc

ВозвратСемантика
XS_OKПолный ответ отправлен; драйвер вычитывает остаток тела и продолжает keep-alive
XS_FALLBACKНе обработано: GET/HEAD падают в статический слой, остальное 404
XS_TAKEOVERПриложение перехватывает соединение: принимает/отправляет через pull/Future и в конце Close; терминальное состояние Close освобождает lease через XServer
Модель доверия: скрипты — скомпилированный нативный код в одном процессе и с теми же правами, что хост (без песочницы). devfile должен поступать от доверенного оператора — это осознанный дизайн «без ограничений», а не недосмотр.

Разработка HTTP

XServer не навязывает объектную модель: XS_HttpReq — структура-представление над набором указателей xrt; вы напрямую используете весь API xrt.

ПолеОписание
tcp / tls«Голые» указатели транспорта, ровно один не пуст — доступен весь потоковый/отправочный API xrt
headРазобранный заголовок запроса (вкл. MethodCode); представления полей действительны только внутри колбэка
bodyАвтомат кадрирования: фиксированная длина / chunked / закрытие; инкрементальное чтение через xrtHttp1BodyRead
host / serverПринадлежность (результат маршрутизации vhost)

Построение ответов

Ответ приложение строит само — xrtHttp1ResponseWrite пишет заголовок, xrtHttp1ChunkWrite дробит на куски, xrtNetStreamSend / xrtTlsStreamSend отправляют. Ниже — реальная реализация демо (фрагмент):

/* modules/http.h — полный ответ: Content-Length + Content-Type генерируются
 * вместе; HEAD сохраняет Content-Length, но не отправляет тело */
static bool ReplyRaw(XS_HttpReq* pReq, uint16 iStatus,
    const char* sContentType, const void* pBody, size_t iBodySize)
{
    char arrHead[1024], arrLength[32];
    xhttpfield arrField[12];
    size_t iFieldCount = 0, iHeadSize = 0;

    snprintf(arrLength, sizeof(arrLength), "%llu",
        (unsigned long long)iBodySize);
    arrField[iFieldCount].Name = XRT_STR_LITERAL("Content-Length");
    arrField[iFieldCount].Value = xrtStrView(arrLength);
    iFieldCount++;

    if ( !xrtHttp1ResponseWrite(XHTTP_VERSION_1_1, iStatus,
        xrtHttpStatusText(iStatus), arrField, iFieldCount,
        arrHead, sizeof(arrHead), &iHeadSize) ) return false;
    if ( !ConnSend(pReq, arrHead, iHeadSize) ) return false;
    return pReq->head->MethodCode == XHTTP_METHOD_HEAD ||
        ConnSend(pReq, pBody, iBodySize);
}

Маршрутизация (шаблон уровня приложения)

Встроенного роутера нет — на каждый host один RequestProc, диспетчеризацию URL строит приложение. Проверенный в demo-single шаблон: статическая таблица (xrtMap) + динамические pattern (xrtPatternCompileMany) + слоты методов:

AddStaticRouteHTTP("/api/v1/items", XHTTP_METHOD_GET,  Handle_API_List);
AddDynamicRouteHTTP("/api/v1/items/{id}",
    XHTTP_METHOD_PUT | XHTTP_METHOD_PATCH, Handle_API_Item_Update);
/* после регистрации — однократная компиляция; стоимость сопоставления
   не зависит от числа маршрутов */
(void)RouteHTTP_Compile();
  • Маски методов свободно комбинируются; XHTTP_METHOD_CRUD / XHTTP_METHOD_ANY — агрегированные маски
  • Порядок: статическое точное совпадение → динамический pattern → слот метода; промах возвращает XS_FALLBACK в wwwroot
  • Замер на 10 000 маршрутов: 42,0 мкс @ 10 → 59,1 мкс @ 10 000 (детали бенчмарка)

vhost и TLS

Маршрутизация по трём источникам: заголовок Host / absolute-form / authority-form; HTTP/1.1 требует Host, несовпавшее уходит хосту по умолчанию. TLS выбирает сертификаты хоста по SNI; серверная cookie привязывает выбор из ClientHello к тому же поколению, что и Accept. Сертификаты заменяются на ходу (атомарная подмена таблицы под блокировкой).

Разработка WebSocket

XServer проверяет рукопожатие (400 при провале) и делает xrtWsStreamAttach; маскирование, пересборка фрагментов, управляющие кадры и permessage-deflate целиком живут в слое xrt — вы пишете только бизнес-колбэки:

static void WsOpen(XS_HostInfo* pHost, xwsstream* pWs)
{
    (void)xrtWsStreamText(pWs, xrtStrView("connected"));
}

static void WsText(XS_HostInfo* pHost, xwsstream* pWs, xstrview tText)
{
    (void)xrtWsStreamText(pWs, tText);   /* echo */
}
  • Отправка: xrtWsStreamText / xrtWsStreamBinary (плюс варианты с передачей владения Ref / Take)
  • Пределы: ws_message_limit закрывает с кодом 1009; ws_protocol может требовать подпротокол
  • Хост без экспортированных WsText/WsBinary отвечает 503; неудачная маршрутизация vhost — 404; превышение заголовков — 431
  • Сканирование простоя использует межпотоково-безопасный Abort; аккумуляторы свыше 256 КБ освобождаются после конца сообщения

Потоковые сервисы TCP

class = tcp (переключатель tls уровня сервиса решает tcp или tcps). XS_StreamConn — структура-представление: ровно один из tcp / tls не пуст; используйте отправочный API ненулевой стороны (под tcps запрещено писать в tcp в обход tls).

static void EventOpen(XS_HostInfo* pHost, XS_StreamConn* pConn)
{
    /* приветственный баннер при установке соединения */
    if ( pConn->tcp != NULL )
        (void)xrtNetStreamSend(pConn->tcp, "hello\n", 6);
}

static void EventData(XS_HostInfo* pHost, XS_StreamConn* pConn, xnetbuf* pBuffer)
{
    /* pBuffer — цепочка буферов без копирования, заимствована на колбэк;
     * размер через xrtNetBufSize(pBuffer); данные потребляются API буферов xrt */
}

static void EventClose(XS_HostInfo* pHost, XS_StreamConn* pConn,
    xnetresult iResult, const xerror* pError)
{
    /* жизненным циклом соединения владеет xs; приложение не вызывает Destroy */
}
  • Жизненный цикл: перехват Accept → реестр → деструкция на Close — всем управляет и всем владеет XServer
  • idle_timeout закрывает соединения по простою; recv_limit меньше блока чтения по умолчанию автоматически сжимает блок
  • Сертификаты хостов строят таблицу SNI; каждому соединению сертификат по SNI

Сервисы UDP

class = udp, минимальный драйвер: bind → прозрачный EventDgram. Каждая дейтаграмма берёт lease текущего поколения через слот слушателя; состояния соединений нет.

static void EventDgram(XS_HostInfo* pHost, xnetudp* pUdp,
    const xnetudpmessage* pMsg)
{
    /* pMsg->Remote адрес отправителя · pMsg->Data/pMsg->Size нагрузка
     * pMsg->Flags несёт флаг XNET_UDP_MESSAGE_TRUNCATED */
    (void)xrtNetUdpSendTo(pUdp, &pMsg->Remote,
        "ack", 3);   /* ответ на адрес отправителя */
}

recv_limit ограничен 65535; отправка целиком на совести скрипта через xrtNetUdpSendTo, на любой адрес назначения.

Горячая перезагрузка

Перезагрузка — это задача координации целевого состояния, а не синхронная команда: Submit возвращает стабильный ID, итог опрашивается. И перезагрузка host, и server публикуются как полное поколение server.

API запуска

ФункцияОписание
xsReloadHostSubmit(pHost)Перезагрузить один host (публикация целым поколением его server)
xsReloadServerSubmit(name)Перезагрузить весь server по имени
xsReloadAllSubmit()Пакетная транзакция: все кандидаты прекампилируются, любая неудача откатывает в обратном порядке
xsReloadQuery(id, &result)Опрос итога и отпечатка Revision

Терминальные состояния: ACCEPTED → PREPARING → SUCCEEDED / FAILED / SUPERSEDED / CANCELLED. Новое намерение на том же server вытесняет неисполненное старое (SUPERSEDED); предел pending — 64.

Передача состояния

/* Старое поколение: неразрушающий экспорт снимка (может продолжать обслуживать) */
static bool ServiceSwap(XS_HostInfo* pHost, xvalue** ppShared)
{
    xvalue* pState = xrtValueObject();
    xrtValueObjectSetNew(pState, XRT_STR_LITERAL("counter"),
        xrtValueInt64(G_iCounter));
    *ppShared = pState;
    return true;
}

/* Новое поколение: забрать внутри ServiceInit и бесшовно продолжить */
static void ServiceInit(XS_HostInfo* pHost)
{
    xvalue* pState = xsSwapTake(pHost);
    if ( pState != NULL ) {
        G_iCounter = xrtValueInt64(xrtValueObjectGet(pState, "counter"));
        xrtValueRelease(pState);
    }
}

Семантические гарантии

  • Сначала компиляция, потом замена: всё множество кандидатов server обязано скомпилироваться до побочных эффектов — любая неудача отклоняет партию целиком, старое поколение продолжает обслуживать (откат — семантическая гарантия)
  • Два поколения параллельно: старые соединения дорабатывают на старом коде; неизменённые эндпоинты передаются в слоте слушателя без мерцания
  • Утилизация при нуле ссылок: после обнуления lease GC-поток в правильном порядке разрушает скрипт / драйвер / конфигурацию — soak-тесты: ноль утечек, ноль падений
  • Таймеры умирают со своим поколением: xsTimerAfter привязан к поколению создания; замена отменяет его автоматически, в новое он не выстрелит
  • Неизменяемое: изменения engine.workers отклоняются; класс custom без онлайн-перезагрузки (у «сырых» ресурсов нет терминального lease)

Публикация одним файлом (сайтовый VFS)

xs.json и весь сайт упаковываются в один LZMA-архив, дописываемый в конец бинарника xs, — получается развёртываемый одним файлом exe. Запуск без аргументов автоматически находит пакет приложения и читает конфигурацию и файлы из него.

команды pack
xs.exe pack <каталог-сайта> -o app.exe   # в каталоге должен лежать xs.json; база = сам запущенный xs
xs.exe pack --list app.exe           # список элементов (путь/метод/размер/CRC)
xs.exe pack --extract app.exe -d out/  # распаковка для разбора
xs.exe pack --strip app.exe -o orig.exe # восстановить исходный бинарник (побайтово)

Приоритет диска

Положите одноимённый файл рядом с exe — он переопределяет версию из пакета (горячее исправление CSS / продление сертификатов без перепаковки); удалите — содержимое пакета вернётся. Пути сертификатов идут через единый выход чтения (сначала диск, потом пакет), поэтому после записи файла его копия в пакете естественно отменяется.

Формат архива

  • Сжатие: LZMA по элементам (без выигрыша — STORE); демо-сайт 1,7 МБ → 542 КБ
  • Обнаружение: чтение 24-байтового концевого маркера, нулевая цена; четыре уровня CRC (маркер/заголовок/индекс/элемент)
  • Память: индекс загружается один раз при старте и неизменяем (потокобезопасно); содержимое распаковывается лениво при первом обращении и остаётся в кэше
  • --no-vfs игнорирует пакет приложения и принуждает режим каталогов (отладка)

Форма настольного приложения (class: "app")

Объявите "class": "app" у сервисного узла — получите оконную форму: локальный HTTP-сервис + окно WebView2 в одном процессе — окно загрузки при старте, автоматическое открытие интерфейса по готовности сервиса, корректная остановка при закрытии окна. Конфигурация http-стороны (host / static / TLS / горячая перезагрузка) значит то же самое; добавляется лишь дескриптор window (9 полей, см. справочник полей).

Когда выбирать форму app

  • Доставка инструментального продукта конечным пользователям (панели управления, локальные дашборды, рабочие места агентов), где пользователь не должен «открывать браузер и вводить адрес»
  • Один и тот же продукт должен работать и как серверное развёртывание — Linux / варианты без webview автоматически уходят в headless, конфигурация не меняется
  • Нужен невидимый локальный порт: класс app по умолчанию ip 127.0.0.1 (без диалога брандмауэра) + port: 0 автоназначение ОС (фактический порт — в XS_ServerInfo.PortBound)

xsw.exe

Релизный пакет для Windows содержит xsw.exe — вариант той же сборки с GUI-подсистемой (без консольного окна). Используйте его как базу упаковки для формы app: xs.exe pack сайт/ -o app.exe даёт продукт «по двойному щелчку»; с close: "stop" консоли нет ни на одном шаге.

Чек-лист выпуска

  • window.devtools выключен (умолчание) — в релизных пакетах без F12
  • install_fallback по аудитории: ask для обычных пользователей (запрос при отсутствии Runtime) / silent без присмотра / headless явно без окна
  • Внешние обратные вызовы или адреса в закладках → фиксированный порт (прилипание port:0 живёт только с процессом)
  • Сценарии close: "hide": подтвердите путь остановки (сигналы; в v1 без трея)
  • Та же xs.json без изменений развёртывается на серверы: среды без оконного бэкенда деградируют в headless и печатают адрес доступа в лог

Матрица деградации по платформам/вариантам и детали жизненного цикла — на странице справки webview; сквозной разбор — в главе 50 учебника.

Автопродление сертификатов (узел acme)

Требует расширения xacme. Корневой узел acme в xs.json включает встроенный демон ACME — жизненный цикл сертификата из «ручного продления» превращается в необслуживаемый: ежедневная проверка по расписанию, автоматическое продление DNS-01 при остатке менее 30 дней, горячая замена без разрыва соединений. Постатейные ограничения — в справочнике полей xs.json.

Правила продления

ПунктПоведение
Время проверкиcheck_time "03:00" — фиксированное ежедневное время (или check_interval_hours 12 — интервальный режим, взаимоисключающие); первая проверка через 5 минут после старта
Запуск продленияостаток жизни < 30 дней (renew_before_days, 1–89) или нет сертификата в store
Замыканиепри действующем сертификате ни один CA не опрашивается — чисто локальное решение
Горячая заменапродукт продления атомарно пишется на диск (раскладка acme.sh) → xsTlsRefresh для каждого server, ссылающегося на группу

Цепочка отказоустойчивости CA

Упорядоченный массив directories[]: пробуем по порядку, побеждает первый успешный; сбой CA (сеть / лимиты / протокол) падает на следующий. Действующий локальный сертификат замыкает проверку; продление всегда retry с головы цепочки — автоматический возврат к основному CA после восстановления. Сосуществование нескольких CA без переделок: store xacme хранит аккаунты по каталогам на CA, один ключ аккаунта открывает аккаунты в нескольких CA.

EAB и импорт аккаунта

  • CA с EAB (например litessl): eab_kid_file / eab_hmac_file парой через ссылки (креденциалы никогда не попадают в xs.json)
  • Импорт существующего аккаунта acme.sh без EAB: предзаполнить ключ аккаунта (SEC1 или PKCS#8) в acme-store/accounts/<первые 16 hex sha256(url)>/account.pem — демон идёт по пути «в store есть аккаунт», EAB не нужен вовсе

Шпаргалка эксплуатации

  • Принуденная переподписка: удалить acme-store/certs/<основной домен>/ и перезапустить (решение читает store, а не каталог tls хоста)
  • Поток демона владеет своим сетевым движком (Workers=1) и снимает конфигурацию при старте — изменения acme требуют перезапуска
  • Референс служебного развёртывания: tools/xs.service в репозитории (systemd, 5-секундное восстановление после сбоя + AmbientCapabilities вместо setcap)

Развёртывание и эксплуатация

Форма поставки

Один файл xs / xs.exe (~5,2 МБ, встроенные через LZMA TCC + SDK + системные библиотеки) плюс каталог исходников приложения. Ноль установки на целевой машине: ни рантайма, ни DLL, ни зависимостей от заголовков.

Корректная остановка

Ctrl+C / SIGTERM запускает пять фаз: прекратить приём и утилизировать скрипты → дождаться терминальных состояний сети и остановить движок → утилизировать custom → вычистить деструкции на reaper → освободить конфигурацию. ServiceUnit на этом пути гарантированно выполняется не более одного раза.

Тестовые базлайны

  • func_test — 40 сценариев с утверждениями: 21 случай fail-fast конфигурации, 8 векторов обхода путей, штормы перезагрузки
  • reload_matrix — четыре протокола × TLS, 7 эндпоинтов: передача на тех же эндпоинтах и изоляция поколений
  • drill — шестичасовой марафон: slowloris / бомбы пайплайнов / бомбы перезагрузок битых скриптов + замеры RSS
  • routebench — сравнение задержек на одной машине с Go / PHP / FastAPI (результаты)

Платформы

Windows и Linux (система сборки ветвится автоматически); macOS поддержан на уровне xrt бэкендом kqueue. Национальные пути на Windows преобразуются в UTF-8 на входе.