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

В главе 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 … uint64int8_t … uint64_tПолное семейство фиксированной ширины, прямо из <stdint.h>
ptrvoid*Универсальный указатель; параметр пользовательских данных в обратных вызовах всегда такой
str / cstrchar* / const char*Строки с нулевым завершением; str обычно означает «вы владеете, по завершении xrtFree»
bytes / cbytesunsigned char* и const-версияУказатели на двоичные данные
xtimeint64Абсолютное время 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 поиска возвращают его при промахе. Дисциплина времени жизни представлений:

1Источник живлитералы, буферы и управляемые строки — законные источники представлений
2В период заимствованияисточник должен оставаться живым, а его содержимое неизменным
3Использовал и выбросилсамо представление освобождать не нужно, и его нельзя хранить дольше источника

Границы ресурсов: общая линия обороны для недоверенного ввода

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; она демонстрирует сразу версии на этапе компиляции и во время выполнения, а также значения по умолчанию для границ ресурсов:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/core/version_limits/main.c -lws2_32 -liphlpapiversion=2.0.0-devlimits: depth=128 entries=100000 input=268435456

Что только что произошло. xrtVersion() возвращает строку версии во время выполнения, совпадающую с результатом, собранным из макросов компиляции XRT_VERSION_MAJOR/MINOR/PATCH — в однозаголовочном сценарии оба из одного источника; при интеграции со связываемой библиотекой несовпадение означает, что заголовок и библиотека из разных версий. Границы ресурсов по умолчанию (глубина 128, сто тысяч записей, 256 МБ ввода) — конкретные числа трёх шлюзов из предыдущего раздела. Семантика трёх шлюзов одинакова для всех парсеров: превышение — отказ со структурированной ошибкой (глава 4), остаток ввода не потребляется — «либо полный разбор, либо отказ с явной причиной», без промежуточных состояний. Это же значит, что защитный код пишется очень просто: передать границы, проверить возвращаемое значение, прочитать слот ошибки — три шага и всё.

Полная программа: управление разделяемым объектом через подсчёт ссылок

Вторая программа взята из examples/core/reference/main.c; она создаёт объект со встроенным счётчиком через xrtMalloc и проходит полный жизненный цикл «создание → разделение → обнуление и разрушение»:

examples/core/reference/main.c源码 ↗



















 */

#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/core/reference/main.c -lws2_32 -liphlpapivalue: 42

Что только что произошло. Объект встраивает 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.

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

Ловушка 1: обращение с представлением как со строкой с нулевым завершением:Ловушка 1: обращение с представлением как со строкой с нулевым завершением

Симптомы: мусор в конце вывода или обрезка в странных местах; изредка чтения за границей с падениями.

Причина: 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);   /* длина в паре с данными, двоичная безопасность */
Ловушка 2: забытый последний Release (или лишний Release):Ловушка 2: забытый последний Release (или лишний Release)

Симптомы: в первом случае память только растёт (детектор утечек главы 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 меньше на пути ошибки — та же утечка; на один больше — то же повисание.

Ловушка 3: хранение представления в структуре, переживающей источник:Ловушка 3: хранение представления в структуре, переживающей источник

Симптомы: работает нормально какое-то время, затем изредка мусор или падения с трассировками, указывающими на совершенно посторонние модули; условия воспроизведения плавают (зависит от того, когда буфер-источник переиспользуется или освобождается).

Причина: представление — только заимствование: как только время жизни источника заканчивается (переиспользование буфера, освобождение управляемой строки, возврат стекового кадра), представление повисает. Хранить его в глобальной таблице, долгоживущем объекте или очереди другого потока — посадить бомбу с часовым механизмом.

错误写法
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 записей)

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