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

Эта глава — один из фундаментов книги. Почти каждая падающая функция в XRT сообщает о неудаче одним и тем же способом: возвращает false или NULL, одновременно помещая структурированный объект xerror в слот ошибки текущего контекста выполнения. Вы уже видели вызов xrtGetError() в главе 3; эта глава разбирает модель полностью — из каких полей состоит ошибка, как цепочка причин выражает «что же случилось внизу», кому принадлежит объект ошибки, каков жизненный цикл слота ошибки потока, и какова инженерная дисциплина передачи между потоками и моментов оборачивания. Каждая следующая глава (сети, TLS, задачи) переиспользует эти правила: комбинация «неудача — по возвращаемому значению, подробности — в слоте ошибки, расслоение — цепочкой причин» будет попадаться снова и снова, поэтому главу стоит прочитать до конца.

4.1引入

Начните со сценария, знакомого каждому C-программисту: вы написали функцию загрузки конфигурации.

str load_config(const char* sPath);

Файла нет — возвращает NULL. Вызывающий получил NULL — и что дальше? «Файл отсутствует», «файл есть, но JSON некорректен» и «не хватило памяти» в возвращаемом значении выглядят одинаково. Обычная заплатка — напечатать строку в журнал, но журнал для людей: верхний код не может по нему ветвиться. Или определить кучу отрицательных кодов — но пространства кодов разных слоёв конфликтуют, а контексту вида «в строке N, столбце M файла ошибка» места нет.

Хуже становится с расслоением. Загрузчик конфигурации внутри читает файл и разбирает JSON: когда разбор падает, верхнему слою действительно нужно знать «загрузка конфигурации не удалась, потому что в строке 3 JSON синтаксическая ошибка» — должна сохраниться информация обоих слоёв, а не сжаться в голое parse failed.

Традиционные решения каждое хромо по-своему. Возврат int-кодов требует от каждого модуля таблицы кодов; при переходе между слоями либо теряется контекст, либо конфликтуют пространства кодов. Прямая печать в printf / stderr приваривает информацию к слою отображения: вызывающий не получает ни категории, ни возможности ветвления. Глобальная переменная в стиле errno требует платформенных особенностей в многопоточности и несёт один integer за раз. По существу эти решения отвечают только «случилась неудача» — никогда «какой слой, почему и что делать дальше».

Эти три проблемы — классификация, контекст, расслоение — и решает модель ошибок XRT. Никаких целочисленных кодов: неизменяемый объект несёт структурированную информацию и человекочитаемое сообщение вместе, а каждый контекст выполнения предоставляет слот «текущей ошибки», так что возврат false и подробности ошибки сопровождают друг друга автоматически.

4.2概念

Что внутри объекта ошибки

xerror — неизменяемый объект, который безопасно держать между потоками. После создания никто не может изменить его поля, поэтому «передавать его» всегда безопасно. Каждая ошибка состоит из:

  • Общий род (xerrkind): грубая классификация, стабильная между модулями, например XERR_TIMEOUT, XERR_NOT_FOUND. Он для потока управления — решения вроде «повторять или нет» и «деградировать или нет» смотрят только на род.
  • Домен: строка пространства имён модуля, например "xrt.json", "example.net". Один и тот же числовой код в разных доменах значит разное; домен исключает конфликты кодов.
  • Код ошибки внутри модуля: каждый домен нумерует свой с единицы, для точных решений на стороне модуля.
  • Системный код, имя операции, UTF-8 сообщение: мост к семантике errno платформы, пометка операции, в которой случился сбой, и описание для человека.
  • Опциональные данные и цепочка причин: машиночитаемые дополнения (скажем, позиция ошибки JSON) и ссылка на ошибку нижнего слоя.

Одна дисциплина проходит через всё: род и домен — для машинных решений, сообщение — только для отображения. Никогда не пишите код, разбирающий строки сообщений об ошибках: ветвитесь по роду, а детали читайте из структурированных полей.

Набор родов нарочно мал и стабилен; часто используемые:

ГруппаРодЗначение
Вход нарушает контрактXERR_ARGUMENT XERR_TYPE XERR_VALUE XERR_RANGEПустой параметр, несоответствие типа, недопустимое значение, выход за границы
Состояние не разрешаетXERR_STATEСостояние объекта или рантайма не поддерживает операцию
РесурсыXERR_MEMORY XERR_IO XERR_NOT_FOUND XERR_EXISTS XERR_PERMISSIONНехватка памяти и ошибки системных ресурсов
Повторить позжеXERR_AGAINОбъект ещё валиден; повторите после ожидания
Ожидание прерваноXERR_TIMEOUT XERR_CANCELLED XERR_CLOSEDТайм-аут, отмена, ресурс закрыт
Протокол и возможностиXERR_PROTOCOL XERR_UNSUPPORTED XERR_INTERNALОшибка протокола, отсутствие возможности, нарушение внутреннего инварианта

XERR_NONE означает лишь «нет ошибки» и не годится для создания объекта ошибки.

Цепочка причин: многослойная история неудачи

xrtErrorWrap «оборачивает» ошибку нижнего слоя в новую ошибку верхнего, образуя цепочку причин. Вернёмся к примеру с конфигурацией из начала: цепочка выглядит так:

1Нижний слойразбор JSON не удался: домен xrt.json записывает позицию строки/столбца
2Оборачиваниезагрузка конфигурации не удалась: домен example.config оборачивает ошибку разбора в XERR_IO
3РешениеxrtErrorIs видит XERR_IO наверху; по цепочке можно дойти до синтаксической ошибки
4ОтображениеxrtErrorMessage читает только верхнее сообщение; слой журнала может обойти всю цепочку

Решение по цепочке не требует ручного обхода: xrtErrorIs(错误, XERR_TIMEOUT) ищет по цепочке указанный род и возвращает заимствованный указатель на найденное (или NULL). «Эта неудача в итоге вызвана тайм-аутом?» — одна строка кода. xrtErrorFind ищет точно по домену и коду.

Момент оборачивания: ошибка получает семантику на границах

Цепочка причин не означает оборачивания на каждом уровне. Практичная дисциплина: оборачивать на границах модулей, внутри модуля передавать. Ошибка домена xrt.json, поднятая внутри модуля JSON, пересекает границу «загрузки конфигурации» и становится «загрузкой конфигурации не удалась» домена example.config — с этого момента у стратегии повторов и деградации верхнего слоя появляется явная точка решения: это XERR_IO (повтор имеет смысл) или XERR_ARGUMENT (повтор бессмысленен — сообщить об ошибке конфигурации)? А нижележащие вызовы между границами не нуждаются в лишней обёртке «вызов не удался» — иначе цепочка распухает от безынформативных дублей и настоящая семантика разжижается.

Пробный камень «оборачивать или нет»: если новый слой может добавить информацию для решения (новый домен, новый семантический род, новая стратегия восстановления) — оборачивайте; если он лишь пересказывает «внизу не удалось» — не оборачивайте, пусть исходная ошибка идёт наверх.

Разделение труда ошибок и журналирования

Объекты ошибок «переносят неудачу структурно»; журналирование «оставляет след выполнения» — не подменяйте одно другим. Распространённый антипаттерн — печатать сообщение об ошибке на каждом слое: одно и то же событие трижды появляется в журнале, и при разборе теряется структура. Лучшее разделение: ошибки тихо проходят по цепочке вызовов и обрабатываются в точках решения (повтор, деградация, окончательный выход неудачей); журнал пишет только в точках решения и на ключевых границах, неся полный род и цепочку причин. Logger главы 38 умеет записывать структурированные поля xerror в записи журнала; пока достаточно запомнить принцип разделения.

Слот ошибки потока: текущая ошибка

Каждый контекст выполнения владеет одним слотом текущей ошибки. Обычные потоки по умолчанию используют контекст потока; корутины и планировщики задач переключают собственные слоты, поэтому миграция корутины не загрязняет несущий поток, а задачи не перебивают друг друга. Семантика слота:

xrtGetError только заимствует и ничего не меняет. Три ключевых правила:

  • xrtSetError добавляет ссылку на переданный объект и заменяет текущую ошибку; передача NULL равнозначна очистке. При замене или очистке слот освобождает удерживаемую им ссылку.
  • xrtTakeError забирает текущую ссылку и опустошает слот — используйте его, чтобы сохранить ошибку надолго (очередь журнала, передача между потоками), и не забудьте xrtErrorFree по завершении.
  • Объекты, оставшиеся в слоте по умолчанию при выходе нативного потока, освобождаются автоматически; потокам расширений не нужно принудительно чистить ошибки ради защиты от утечек.

Есть ещё одно легко подводное умолчание: успешная операция не очищает старую ошибку неявно. В слоте может оставаться содержимое предыдущей неудачи — читать слот вы вправе только после того, как функция объявила неудачу возвращаемым значением.

Между потоками и долгое хранение

xerror неизменяем, поэтому сам объект безопасно держать между потоками; заботят только два вопроса — «кто освобождает» и «какому контексту принадлежит слот». Чтобы вернуть неудачу из рабочего потока в главный (например, приложить ошибку к результату задачи), правильная поза: xrtTakeError забирает владение, ошибка передаётся вместе с результатом, потребитель делает xrtErrorFree; хранить заимствованный указатель xrtGetError() — гонка: слот может быть заменён в любой момент. Слоты ошибок корутин и задач управляются планировщиком и мигрируют вместе с контекстом выполнения — их переносить не нужно; явный перенос требуется только между нативными потоками. Подсчёт ссылок ведут xrtErrorRef/xrtErrorFree по тем же правилам, что и общие подсчитываемые объекты главы 5.

Одношаговая запись типичных неудач

Большую часть времени не нужны ручные «создать, установить, освободить». xrtSetErrorInfo создаёт и передаёт владение за один шаг:

xrtSetErrorInfo(XERR_ARGUMENT, "app.config", 1, "path is empty");

Для динамических сообщений используйте xrtSetErrorFormat из модуля error_format (правила printf, с отказом от %n с побочным записывающим эффектом; при неудаче форматирования остаётся не требующая распределения XERR_MEMORY). Оба помощника создают неизменяемые объекты и передают их напрямую текущему контексту. Позицию исходника несёт xrtErrorBuildAt, читаемая затем через xrtErrorFile, xrtErrorLine, xrtErrorColumn.

4.3示例

Полная программа: создать, обернуть, запросить цепочку причин

Следующая программа взята из примера репозитория examples/core/error/main.c и полностью демонстрирует жизненный цикл ошибки при модели владения. Сначала код, затем разбор:

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






















 */

#include <stdio.h>

#include <xrt.h>



int main(void)
{
	




	 */
	xerror* pCause = xrtErrorCreate(XERR_TIMEOUT, "example.net", 1, "connect timeout");
	xerror* pError;

	if ( pCause == NULL ) {
		return 1;
	}

	




	 */
	pError = xrtErrorWrap(pCause, XERR_IO, "example.client", 2, "request failed");
	xrtErrorFree(pCause);
	if ( pError == NULL ) {
		return 2;
	}

	



	 */
	xrtSetError(pError);
	xrtErrorFree(pError);

	/* 借用读取:消息永不为 NULL,未设置错误时返回 "(no error)"。 */
	printf("error: %s\n", xrtErrorMessage(xrtGetError()));

	



	 */
	printf("timeout cause: %s\n",
		xrtErrorIs(xrtGetError(), XERR_TIMEOUT) != NULL ? "yes" : "no");

	/* 清空线程槽,避免错误泄漏到后续逻辑。 */
	xrtClearError();
	return 0;
}
$ gcc -O1 -I single impl.c examples/core/error/main.c -lws2_32 -liphlpapierror: request failedtimeout cause: yes

Что только что произошло. Программа делает четыре шага. Первый: xrtErrorCreate создаёт нижнюю ошибку (сетевой тайм-аут) из «род + домен + код + сообщение», возвращая владеющий указатель — кто создал, тот и освобождает. Второй: xrtErrorWrap оборачивает её в верхнюю XERR_IO; Wrap внутренне добавляет ссылку на причину, поэтому создатель тут же делает xrtErrorFree своей копии — доходить ли счёту до нуля, решает цепочка. Третий: xrtSetError помещает ошибку в слот потока — заметьте, SetError тоже добавляет ссылку, у слота своя копия; создатель снова освобождает свою, и в этот момент у объекта два ссылающихся (цепочка и слот). Четвёртый: xrtGetError заимствует для чтения: xrtErrorMessage никогда не возвращает NULL (при неустановленной ошибке вернёт (no error)), а xrtErrorIs идёт по цепочке и находит XERR_TIMEOUT. Наконец xrtClearError опустошает слот и освобождает его ссылку.

Полная программа: одношаговая конструкция в стиле printf

Вторая программа взята из examples/core/error_format/main.c и показывает удобную конструкцию динамических сообщений:

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



















 */

#include <stdio.h>

#include <xrt.h>



int main(void)
{
	







	 */
	xrtSetErrorFormat(
		XERR_NOT_FOUND,
		"example.config",
		1,
		"file does not exist: %s",
		"app.json"
	);

	/* 借用读取:消息已含动态上下文;未设置错误时返回 "(no error)"。 */
	printf("%s\n", xrtErrorMessage(xrtGetError()));

	/* 清空线程槽,避免错误泄漏到后续逻辑。 */
	xrtClearError();
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/core/error_format/main.c -lws2_32 -liphlpapifile does not exist: app.json

Отличие от xrtErrorCreate — источник сообщения: Create принимает готовую строку, Format принимает форматную строку printf с вариативными аргументами и внутренне переиспользует модуль форматирования строк — сообщение ошибки может нести контекст времени выполнения (здесь — имя файла), и вызывающему не нужно заранее собирать буфер.

Обратите внимание: во второй программе ни разу не появляется Free: xrtSetErrorFormat, как и xrtSetErrorInfo, после создания напрямую передаёт владение текущему контексту — создатель больше не держит ссылку, а финальный xrtClearError освобождает копию слота. На фоне трёх шагов первой программы «создать → установить → освободить свою копию» одношаговая форма экономит не только строки — она устраняет целый класс ошибок «забыл освободить свою копию» (ловушка 2 этой главы). В повседневном коде предпочитайте помощников, если только не помещаете ошибку в структуру или не переносите её между потоками.

4.4契约

  • Владение: Create/Wrap возвращают владеющие указатели, требующие ровно одного Free; Wrap добавляет ссылку на причину; SetError добавляет ссылку на переданный объект; GetError только заимствует.
  • Потоки: xerror неизменяем и безопасен между потоками; у каждого контекста выполнения свой слот ошибки; для переноса между потоками сначала TakeError.
  • Соглашение об ошибках: неудачу объявляют возвращаемые значения false/NULL, подробности даёт слот; успех слот не очищает; читать слот можно только после неудачного возврата.
  • Разделение отображения и решения: поток управления смотрит только на род (при необходимости домен+код); сообщение и позиция — только для отображения и журналов.
  • Расширенные данные: модулям, нуждающимся в структурированных дополнениях, следует определить стабильный формат Data, а не требовать от вызывающих разбора отображаемых сообщений; xrtErrorBuildAt несёт позицию исходника, читаемую через xrtErrorFile/xrtErrorLine/xrtErrorColumn.
  • Минимальные зависимости: базовые API ошибок не зависят от контейнеров, строк или рантайма printf; динамическое форматирование — отдельный модуль удобства error_format.

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

Ловушка 1: освобождение заимствованной ошибки:Ловушка 1: освобождение заимствованной ошибки

Симптомы: редкие падения или повреждения кучи, часто рядом с кодом «прочитал ошибку и прибрал».

Причина: xrtGetError возвращает заимствованный указатель; слот может заменить или очистить его (освободив ту ссылку) в любой момент; вызов xrtErrorFree на заимствованном указателе — двойное освобождение.

错误写法
xerror* pError = xrtGetError();
printf("%s\n", xrtErrorMessage(pError));
xrtErrorFree(pError);   /* освободили заимствование — двойное освобождение при очистке слота */
正确写法
/* Заимствование использовал сразу; для долгого хранения заберите владение */
printf("%s\n", xrtErrorMessage(xrtGetError()));
/* или: xerror* pKeep = xrtTakeError(); ... xrtErrorFree(pKeep); */
Ловушка 2: забыли освободить свою копию ссылки на причину после Wrap:Ловушка 2: забыли освободить свою копию ссылки на причину после Wrap

Симптомы: память медленно растёт; детекторы утечек показывают, что объекты xerror только прибывают.

Причина: после того как xrtErrorWrap добавил ссылку на причину, исходная ссылка создателя всё ещё существует, и освободить её должен создатель — иначе объект причины никогда не обнулится.

错误写法
xerror* pCause = xrtErrorCreate(XERR_TIMEOUT, "net", 1, "timeout");
xerror* pError = xrtErrorWrap(pCause, XERR_IO, "client", 2, "request failed");
/* нет xrtErrorFree(pCause): объект причины утекает */
正确写法
xerror* pCause = xrtErrorCreate(XERR_TIMEOUT, "net", 1, "timeout");
xerror* pError = xrtErrorWrap(pCause, XERR_IO, "client", 2, "request failed");
xrtErrorFree(pCause);
Ловушка 3: «в слоте есть ошибка» как сигнал неудачи:Ловушка 3: «в слоте есть ошибка» как сигнал неудачи

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

Причина: успешные операции не очищают старую ошибку неявно, и в слоте может оставаться объект предыдущей неудачи. Использовать «слот непуст» как критерий неудачи — значит принимать решения по устаревшему состоянию.

错误写法
call_something();              /* вернула true — успех */
if ( xrtGetError() != NULL ) { /* ложное срабатывание: это остаток прошлой неудачи */
	recover();
}
正确写法
if ( !call_something() ) {     /* сначала неудача, объявленная возвратом */
	report(xrtErrorMessage(xrtGetError()));
}

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

Базовое: создать и прочитать

Напишите программу: установите ошибку XERR_ARGUMENT через xrtSetErrorInfo, напечатайте сообщение для её рода, затем xrtClearError и убедитесь, что xrtErrorMessage(xrtGetError()) возвращает (no error).

Продвинутое: трёхуровневая цепочка причин

Постройте цепочку «服务启动失败 → 配置加载失败 → JSON 第 N 行语法错误» из трёх уровней: нижний — через xrtErrorCreate (не Wrap), средний и верхний — через xrtErrorWrap. Одним вызовом xrtErrorIs запросите род нижнего уровня и напечатайте результат поиска. Подсказка: после каждого Wrap сразу освобождайте свою копию ссылки на причину, избегая утечек.

Вызов: журнал неудач с позицией

Реализуйте макрос log_failure(const char* sFile, int iLine): на пути отказа вызовите xrtErrorBuildAt (автоматически несёт позицию исходника в точке вызова), затем напечатайте «файл:строка + сообщение + сообщение каждого уровня цепочки». Критерий приёмки: постройте двухуровневую цепочку причин и увидите в выводе сообщения обоих уровней и файл со строкой точки вызова; позиция, прочитанная через xrtErrorFile/xrtErrorLine, совпадает с местом разворачивания макроса. Завершив, вернитесь к сценарию загрузки конфигурации из начала главы и перепишите его на возвращение структурированных ошибок — получится скелет обработки неудач, готовый для реального проекта.

Итоги

ТемаКраткая справка
СозданиеxrtErrorCreate (владеющее) / xrtSetErrorInfo (одношаговая передача) / xrtSetErrorFormat (сообщение printf)
РасслоениеxrtErrorWrap оборачивает причину и добавляет ссылку; xrtErrorIs ищет по цепочке по роду; xrtErrorFind — по домену и коду
Семантика слотаSetError добавляет ссылку и заменяет; TakeError забирает владение; ClearError опустошает и освобождает; GetError только заимствует
Момент решенияСлот читается только после неудачного возврата; успех не очищает
Одношаговая конструкцияxrtSetErrorInfo / xrtSetErrorFormat создают и сразу передают — окна для утечки нет
Между потокамиОбъекты неизменяемы и межпоточно безопасны; перенос через TakeError, Free у потребителя
Дисциплина оборачиванияОборачивать на границах модулей, внутри передавать; нет новой семантики — нет обёртки
Позиция исходникаПишет xrtErrorBuildAt, читают xrtErrorFile/xrtErrorLine/xrtErrorColumn
ВладениеСоздатель делает ровно один Free; заимствования не освобождаются; перед переносом TakeError
ОтображениеxrtErrorMessage никогда не NULL; род — потоку управления, сообщение — человеку

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

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