Ориентация главы
Эта глава — один из фундаментов книги. Почти каждая падающая функция в 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 «оборачивает» ошибку нижнего слоя в новую ошибку верхнего, образуя цепочку причин. Вернёмся к примеру с конфигурацией из начала: цепочка выглядит так:
Решение по цепочке не требует ручного обхода: 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 и полностью демонстрирует жизненный цикл ошибки при модели владения. Сначала код, затем разбор:
*/ #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; }
Что только что произошло. Программа делает четыре шага. Первый: xrtErrorCreate создаёт нижнюю ошибку (сетевой тайм-аут) из «род + домен + код + сообщение», возвращая владеющий указатель — кто создал, тот и освобождает. Второй: xrtErrorWrap оборачивает её в верхнюю XERR_IO; Wrap внутренне добавляет ссылку на причину, поэтому создатель тут же делает xrtErrorFree своей копии — доходить ли счёту до нуля, решает цепочка. Третий: xrtSetError помещает ошибку в слот потока — заметьте, SetError тоже добавляет ссылку, у слота своя копия; создатель снова освобождает свою, и в этот момент у объекта два ссылающихся (цепочка и слот). Четвёртый: xrtGetError заимствует для чтения: xrtErrorMessage никогда не возвращает NULL (при неустановленной ошибке вернёт (no error)), а xrtErrorIs идёт по цепочке и находит XERR_TIMEOUT. Наконец xrtClearError опустошает слот и освобождает его ссылку.
Полная программа: одношаговая конструкция в стиле printf
Вторая программа взята из 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; }
Отличие от 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.
Типичные ошибки
Симптомы: редкие падения или повреждения кучи, часто рядом с кодом «прочитал ошибку и прибрал».
Причина: xrtGetError возвращает заимствованный указатель; слот может заменить или очистить его (освободив ту ссылку) в любой момент; вызов xrtErrorFree на заимствованном указателе — двойное освобождение.
xerror* pError = xrtGetError(); printf("%s\n", xrtErrorMessage(pError)); xrtErrorFree(pError); /* освободили заимствование — двойное освобождение при очистке слота */
/* Заимствование использовал сразу; для долгого хранения заберите владение */ printf("%s\n", xrtErrorMessage(xrtGetError())); /* или: xerror* pKeep = xrtTakeError(); ... xrtErrorFree(pKeep); */
Симптомы: память медленно растёт; детекторы утечек показывают, что объекты 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);
Симптомы: функция явно успешна, а верхний слой сообщает старую ошибку; логика обработки ошибок работает через раз.
Причина: успешные операции не очищают старую ошибку неявно, и в слоте может оставаться объект предыдущей неудачи. Использовать «слот непуст» как критерий неудачи — значит принимать решения по устаревшему состоянию.
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 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки