Ориентация главы
В главе 2 среда заработала; эта глава по-настоящему входит в include/xrt/core.h — самый низкоуровневый заголовок всего ядра. Его содержимое невелико, но каждый элемент используется модулями верхних уровней снова и снова: в любой вашей XRT-программе рано или поздно появится xstrview в сигнатуре; в любом API парсера, который вы читаете, вы столкнётесь с xrtresourcelimits; время жизни любого неизменяемого разделяемого объекта держится на xrtRefRetain / xrtRefRelease. Глава разбирает открытое содержимое core.h в порядке «типы → представления → границы → прогресс → подсчёт ссылок» и закладывает два мировоззрения, пронизывающих всю книгу: строки не завершаются нулём и разделяемые объекты не управляются аллокатором. Как учебная стратегия, главу не стоит зубрить по пунктам — каждый раздел будет многократно повторяться в последующих главах. Стоит своими руками запустить обе полные программы, держать в голове «таблицу заимствования и владения» и «диаграмму состояний подсчёта ссылок», а остальные детали оставить шпаргалке в конце главы и справочной странице core для справки по мере надобности.
3.1引入
Начните с двух фрагментов кода, которые писал каждый C-программист. Первый: функция должна вернуть часть текста — вернуть char* или сделать копию? Возврат указателя теряет информацию о длине и заставляет вызывающего угадывать, завершён ли он нулём; копия добавляет распределение памяти и ещё один путь отказа. Второй: объект конфигурации разделяется тремя местами — кто его освобождает? Ручная договорённость «кто закончил последним, тот и освобождает» не имеет языковой поддержки в C и рано или поздно вырождается в use-after-free или утечку. Третий случай коварнее: разбор недоверенного ввода — насколько глубокая вложенность считается злой умыслом? Насколько большой — атакой? Если каждый парсер придумывает свои правила, аудит приходится делать N раз.
Отраслевой ответ на эти три проблемы: тип среза строки, подсчёт ссылок и единые границы ресурсов. XRT даёт им простейшую C-форму в core.h — двухполевую структуру xstrview, два атомарных примитива xrtRefRetain/xrtRefRelease и структуру из трёх шлюзов xrtresourcelimits — и заставляет все API библиотеки единообразно потреблять эти три соглашения. Поняв эту главу, вы сможете прочитать «кто чем владеет и где линии обороны» в сигнатуре любого модуля.
3.2概念
Чтение общебиблиотечного стиля из core.h
core.h также учит методу «чтения любого заголовка XRT». Экспортируемые функции несут маркер XRT_API и комментарии-контракты — комментарии описывают контракты (кто владеет, когда действителен, в каком состоянии выходные параметры при отказе), а не пересказ типов параметров; при вопросах о поведении сначала читайте комментарий, потом справочную страницу. Разбивка заголовка — это слои модуля: сначала типы, в середине константы, в конце функции — в соответствии с раскладкой официальных справочных страниц.
Отметьте ещё две детали стиля. Первое — отказ без побочных эффектов: все функции с выходными указателями не трогают выходные объекты на пути отказа, поэтому очистку у вызывающего можно единообразно писать как «при отказе игнорируем выходы». Второе — нет скрытых распределений: результаты, требующие распределения (например, строка, возвращаемая xrtFormat), переносятся возвращаемым значением с явным владением — никогда «передайте двойной указатель, и библиотека тихо распределит за вас». Оба правила пронизывают всю библиотеку; их можно проверить на сигнатуре любого последующего модуля.
Псевдонимы типов: единая запись во всей библиотеке
XRT унифицирует запись типов набором коротких псевдонимов, все определены в core.h:
| Псевдоним | Эквивалент | Примечания |
|---|---|---|
int8 … uint64 | int8_t … uint64_t | Полное семейство фиксированной ширины, прямо из <stdint.h> |
ptr | void* | Универсальный указатель; параметр пользовательских данных в обратных вызовах всегда такой |
str / cstr | char* / const char* | Строки с нулевым завершением; str обычно означает «вы владеете, по завершении xrtFree» |
bytes / cbytes | unsigned char* и const-версия | Указатели на двоичные данные |
xtime | int64 | Абсолютное время UTC в миллисекундах от 1 января 1 года (разобрано в главе 41) |
xseek | перечисление | XSEEK_START / XSEEK_CURRENT / XSEEK_END — общая база перемещения для файлов и универсального IO |
Две детали, которые опытные C-разработчики могут упустить. Во-первых, в сигнатурах обратных вызовов используется ptr, а не буквальный void*: пользовательские данные имеют единый тип, при необходимости применяются явные приведения. Во-вторых, xtime хранит миллисекунды календарного UTC от 1 января 1 года; xrtTimer() возвращает секунды типа double, а разность двух показаний даёт прошедшее время. Часы, часовые пояса и сон разобраны в главе 41. Здесь достаточно запомнить: увидел xtime — думай миллисекунды.
Два сопутствующих «несуществования»: нет «трёхзначного булева» — истинность это обычный C bool; и нет иерархии строковых типов — текст либо завершённая нулём str, либо представление с длиной (сейчас рассмотрим).
Представления: xstrview и xbytesview
Это важнейшее соглашение о данных в XRT. Две структуры имеют одинаковую форму:
typedef struct xstrview { cstr Data; size_t Size; } xstrview; typedef struct xbytesview { cbytes Data; size_t Size; } xbytesview;
Комментарий заголовка формулирует контракт одним предложением: только заимствует память, не владеет данными и не требует нуля в конце. Следствие «может содержать нулевые байты» заслуживает отдельного разбора: в сетевых протоколах и двоичных форматах поля с префиксом длины и произвольным содержимым — норма: байтовые строки в заголовках HTTP и полезная нагрузка сертификатов в рукопожатиях TLS не гарантированы от нулевых байтов. Носить их в строках с нулевым завершением — значит обрезать или экранировать, и то и другое ловушки; носить в представлениях — длина есть длина, содержимое есть содержимое. В томах 7 и 8 вы увидите, что входы и выходы всех протокольных API выражены представлениями — вот почему. Три следствия пронизывают всю книгу:
- Нулевое завершение неприменимо — нельзя передать
Dataнапрямую вprintfс%s; нужно pairing сSizeчерез%.*s; - Время жизни следует за источником — представление не продлевает жизнь нижележащей памяти; после освобождения источника представление повисает;
- Может содержать нулевые байты — безопасно для двоичных данных; входы и выходы API парсеров почти исключительно это.
Построение представления из строкового литерала C — макрос XRT_STR_LITERAL("...") (sizeof(s)-1 автоматически отбрасывает нулевой завершитель); во время выполнения просто заполните два поля из буфера, а XRT_STR_INIT используйте для статической инициализации. Контраст «заимствование против владения» стоит закрепить таблицей:
| Форма | Владелец | Обязанность освобождения | Типичный источник |
|---|---|---|---|
xstrview / xbytesview | Никто | Освобождать нечего | Литералы, срез буфера, выдан API |
str / bytes | Вызывающий | xrtFree ровно один раз | Создающие API вроде xrtFormat |
| Объекты под управлением библиотеки | Библиотека и вызывающие делят по счёту ссылок | Кто обнулил, тот и разрушает | Глава 4 xerror, сертификаты тома 8 |
XRT_NPOS — сигнальное значение «позиции/длины не существует» ((size_t)-1); API поиска возвращают его при промахе. Дисциплина времени жизни представлений:
Границы ресурсов: общая линия обороны для недоверенного ввода
xrtresourcelimits — общий анти-DoS язык всех парсеров, три шлюза: максимальная глубина вложенности iMaxDepth, максимальное число записей iMaxEntries, максимальный суммарный размер ввода iMaxInputBytes. xrtResourceLimitsInit заполняет значения по умолчанию; при необходимости ужесточите и передавайте структуру в API парсеров вроде JSON/XLON/HTTP. Значение по умолчанию «глубина 128, сто тысяч записей» означает: злонамеренно сконструированный текст с десятитысячу уровней вложенности будет отвергнут на 128-м уровне, а не проест стек насквозь.
Как выбрать свои числа? Достаточно трёх вопросов: насколько глубоко вкладывается легитимный ввод (файлы конфигурации обычно однозначные; вложенность протокола — по спецификации), сколько записей максимум (деловой пик с запасом), каков потолок ввода одного запроса (исходя из бюджета памяти). Заполните три ответа в структуру — и граница становится деловой спецификацией в коде: на ревью это документация вашей линии обороны, а не россыпь проверок if. В этом и второй смысл «общего языка»: одна и та же структура при переходе от JSON к XLON, от файла к сетевому потоку сохраняет семантику обороны, и аудит делается один раз.
Обратный вызов прогресса и подсчёт ссылок
Длинные задачи могут принимать обратный вызов прогресса. xrtprogress описывает текущий прогресс (обработанные байты, текущая фаза и другие поля); xrtprogressproc — сигнатура обратного вызова: принимает структуру прогресса и пользовательские данные, возврат false запрашивает отмену; xrtProgressReport — точка отчёта на стороне библиотеки: разработчик вызывает её в ключевых местах цикла, выполняя проверки на null и сборку аргументов один раз. Не делайте тяжёлой работы внутри обратного вызова — он выполняется на обрабатывающем пути задачи; чем быстрее он возвращается, тем оперативнее выполняется отмена. Том 4 применяет его при разборе больших файлов, том 12 вернётся к нему в контексте производительности.
Подсчёт ссылок — язык времени жизни разделяемых объектов. XRT оформил его как два атомарных примитива, а не «умный указатель»:
Правила: поле счётчика должно быть volatile int32, встроенным первым полем объекта; функция создания возвращает со счётом 1 (голос создателя); вызывающий, чей xrtRefRelease вернул 0, отвечает за разрушение. Все неизменяемые разделяемые объекты библиотеки (регулярные выражения, шаблоны, сертификаты X.509, сессии TLS, …) построены на этой паре примитивов.
3.3示例
Полная программа: версии и границы ресурсов
Следующая программа взята из примера репозитория examples/core/version_limits/main.c; она демонстрирует сразу версии на этапе компиляции и во время выполнения, а также значения по умолчанию для границ ресурсов:
*/ #include <stdio.h> #include <xrt.h> int main(void) { xrtresourcelimits Limits; printf("version=%s\n", xrtVersion()); xrtResourceLimitsInit(&Limits); printf("limits: depth=%u entries=%llu input=%llu\n", (unsigned)Limits.iMaxDepth, (unsigned long long)Limits.iMaxEntries, (unsigned long long)Limits.iMaxInputBytes); return 0; }
Что только что произошло. xrtVersion() возвращает строку версии во время выполнения, совпадающую с результатом, собранным из макросов компиляции XRT_VERSION_MAJOR/MINOR/PATCH — в однозаголовочном сценарии оба из одного источника; при интеграции со связываемой библиотекой несовпадение означает, что заголовок и библиотека из разных версий. Границы ресурсов по умолчанию (глубина 128, сто тысяч записей, 256 МБ ввода) — конкретные числа трёх шлюзов из предыдущего раздела. Семантика трёх шлюзов одинакова для всех парсеров: превышение — отказ со структурированной ошибкой (глава 4), остаток ввода не потребляется — «либо полный разбор, либо отказ с явной причиной», без промежуточных состояний. Это же значит, что защитный код пишется очень просто: передать границы, проверить возвращаемое значение, прочитать слот ошибки — три шага и всё.
Полная программа: управление разделяемым объектом через подсчёт ссылок
Вторая программа взята из examples/core/reference/main.c; она создаёт объект со встроенным счётчиком через xrtMalloc и проходит полный жизненный цикл «создание → разделение → обнуление и разрушение»:
*/ #include <stdio.h> #include <xrt.h> */ typedef struct example_object { volatile int32 RefCount; int Value; } example_object; */ static example_object* exampleCreate(int iValue) { example_object* pObject = (example_object*)xrtMalloc(sizeof(example_object)); if ( pObject != NULL ) { pObject->RefCount = 1; pObject->Value = iValue; } return pObject; } */ static example_object* exampleRef(example_object* pObject) { if ( (pObject == NULL) || (xrtRefRetain(&pObject->RefCount) < 0) ) { return NULL; } return pObject; } */ static void exampleFree(example_object* pObject) { if ( (pObject != NULL) && (xrtRefRelease(&pObject->RefCount) == 0) ) { xrtFree(pObject); } } int main(void) { /* 第一个持有者:计数 1。 */ example_object* pFirst = exampleCreate(42); /* 第二个持有者共享同一对象:Retain 后计数 2,两个指针等价。 */ example_object* pSecond = exampleRef(pFirst); if ( pSecond == NULL ) { exampleFree(pFirst); return 1; } /* 通过第二个持有者读取:与第一个看到的是同一份对象。 */ printf("value: %d\n", pSecond->Value); */ exampleFree(pSecond); exampleFree(pFirst); return 0; }
Что только что произошло. Объект встраивает volatile int32 RefCount первым полем — жёсткое требование компоновки xrtRefRetain/xrtRefRelease, которые работают с этим полем через указатель. Создание — это владение (счёт 1); второй держатель вызывает xrtRefRetain до принятия; каждый держатель вызывает xrtRefRelease при отказе, и вызов, вернувший 0, выполняет разрушение (здесь — xrtFree). volatile — не украшение: счёт увеличивается и уменьшается несколькими потоками конкурентно; уберите его, и компилятор может кэшировать чтения и записи в регистрах — счёт больше не будет надёжным.
Два проектных решения стоит обдумать. Первое: примитивы возвращают новое значение счёта, а не булево — вызывающий может напрямую проверять защиту от переполнения (Retain возвращает -1) и момент обнуления (Release возвращает 0), без отдельного API «запросить счёт». Второе: разрушение принадлежит «вызову, обнулившему счёт», а не самому объекту — значит, один и тот же каркас счёта может нести любое поведение разрушения: освобождение памяти, закрытие дескрипторов, рекурсивное освобождение дочерних объектов. xerror главы 4 и объекты сертификатов тома 8 — конкретизации этого паттерна; дойдя до них, вернитесь и сравните — вы увидите один скелет, обрастающий разной плотью.
3.4契约
- Три «не» представления: не владеет, не завершает нулём, не продлевает время жизни; печать через
%.*sсSize; представление нельзя хранить в структуре, переживающей источник. - Границы ресурсов: любая задача разбора недоверенного ввода должна получать явные границы; значения по умолчанию — только отправная точка, ужесточайте под задачу; превышение — отказ всего разбора без потребления остатка ввода.
- Подсчёт ссылок: счёт встроен первым полем
volatile int32; создание возвращает 1; Retain с -1 — защита от переполнения; кто обнулил Release, тот разрушает; разрушение зеркально созданию (xrtMallocпротивxrtFree). - Сигнальное значение: промахи поиска возвращают
XRT_NPOS; проверка через== XRT_NPOS, никогда сравнением знака или величины. - Отказ без побочных эффектов: пути отказа не пишут в выходные параметры, скрытых распределений нет; результаты, требующие распределения, переносятся возвращаемым значением с явным владением.
- Соглашение о времени:
xtime— абсолютная метка в миллисекундах; позиционирование единообразно по трём базамxseek.
Типичные ошибки
Симптомы: мусор в конце вывода или обрезка в странных местах; изредка чтения за границей с падениями.
Причина: Data представления xstrview гарантирует только Size валидных байтов; после них может не быть нулевого завершителя — или случайно оказаться; поведение зависит от источника и не должно использоваться.
xstrview View = XRT_STR_LITERAL("hello"); printf("%s\n", View.Data); /* чтение за границей: ноль после Size не обещан */
xstrview View = XRT_STR_LITERAL("hello"); printf("%.*s\n", (int)View.Size, View.Data); /* длина в паре с данными, двоичная безопасность */
Симптомы: в первом случае память только растёт (детектор утечек главы 6 поймает); во втором счёт обнуляется слишком рано, и объект разрушается, пока ещё используется (падения или порча данных, и трудно воспроизводимые).
Причина: конвенция подсчёта ссылок — «ровно один Release на держателя», и голос создателя тоже является держанием.
example_object* pKeep = exampleCreate(42); example_object* pShare = pKeep; xrtRefRetain(&pShare->RefCount); /* разделение установлено */ /* освобождена только одна доля — счёт замирает на 1, объект никогда не разрушится */ xrtRefRelease(&pKeep->RefCount);
example_object* pObject = exampleCreate(42); xrtRefRetain(&pObject->RefCount); /* второй голос: разделение установлено (два держателя) */ xrtRefRelease(&pObject->RefCount); /* первый держатель отказывается */ if ( xrtRefRelease(&pObject->RefCount) == 0 ) { /* второе освобождение обнуляет: этот вызов выполняет разрушение — Retain и Release строго в паре */ }
Примечание: правильная мысленная модель — «на каждом пути выполнения Retain и Release строго в паре», включая пути ошибок. На один Release меньше на пути ошибки — та же утечка; на один больше — то же повисание.
Симптомы: работает нормально какое-то время, затем изредка мусор или падения с трассировками, указывающими на совершенно посторонние модули; условия воспроизведения плавают (зависит от того, когда буфер-источник переиспользуется или освобождается).
Причина: представление — только заимствование: как только время жизни источника заканчивается (переиспользование буфера, освобождение управляемой строки, возврат стекового кадра), представление повисает. Хранить его в глобальной таблице, долгоживущем объекте или очереди другого потока — посадить бомбу с часовым механизмом.
xstrview gCached; /* глобальный кэш */ void load(const char* sJson) { str s = xrtFormat("%s", sJson); gCached = (xstrview){ s, strlen(s) }; /* представление указывает на управляемую строку */ xrtFree(s); /* источник освобождён; gCached повисает с этого момента */ }
str gOwned = NULL; /* чтобы жить долго — владейте копией сами */ void load(const char* sJson) { xrtFree(gOwned); gOwned = xrtFormat("%s", sJson); /* копия во владеющее хранилище */ } /* xrtFree(gOwned) ровно один раз перед выходом из программы */
Практические задания
Напишите программу, печатающую sizeof(int8/int32/int64/ptr) и значение XRT_NPOS (по образцу программы types из главы 2), и убедитесь, что они соответствуют ожиданиям вашей платформы. Шаг дальше: постройте представление через XRT_STR_LITERAL и напечатайте его в манере этой главы, впервые прочувствовав «длина отдельно от данных».
Постройте xstrview над "XRT-Program-Design" и — без каких-либо строковых функций — вручную в цикле найдите позицию первого -, напечатав две подстроки вокруг неё (через %.*s). Подсказка: тип позиции size_t, договоритесь возвращать XRT_NPOS при отсутствии; при проверке «не найдено» пишется == XRT_NPOS, а не < 0 — у size_t нет отрицательных чисел.
Добавьте многопоточный стресс объекту из examples/core/reference/main.c: 4 потока по миллиону Retain/Release каждый, затем главный поток выполняет финальный Release. Критерий приёмки: программа работает стабильно и печатает «value» успешно (объект не разрушен досрочно); уберите volatile и повторите для сравнения (зафиксируйте, появляются ли аномальные завершения, чтобы оценить влияние оптимизаций компилятора — только наблюдение, стабильное воспроизведение не требуется). После этого ответьте на вопрос: почему в стресс-тесте объект разрушается ровно один раз, а не ноль или два раза? Проработайте ответ правилом «Retain и Release строго в паре».
Итоги
| Тема | Краткая справка |
|---|---|
| Псевдонимы типов | int8..uint64 / ptr / str,cstr / bytes,cbytes / xtime (миллисекунды) / xseek |
| Контракт представления | только заимствует, без нуля, может содержать нули; печать %.*s; построение XRT_STR_LITERAL; не хранить дольше источника |
| Сигнальное значение | промахи поиска XRT_NPOS ((size_t)-1); проверка == XRT_NPOS; сравнение по знаку запрещено |
| Границы ресурсов | xrtResourceLimitsInit три шлюза: глубина/записи/байты; ужесточать под задачу; превышение — отказ всего разбора |
| Подсчёт ссылок | первое поле volatile int32; создание = 1; Retain/Release в паре; кто обнулил, тот разрушает |
| Обратный вызов прогресса | xrtprogressproc, возврат false запрашивает отмену; держать обратный вызов лёгким и быстрым |
| Версия | макросы компиляции и xrtVersion() из одного источника; несовпадение — заголовок и библиотека разных версий |
📖 Справочники API в этой главе:core (25 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки