Как работает паника в Rust


Что именно происходит, когда вы вызываете panic!()?
Недавно я потратил много времени на изучение частей стандартной библиотеки, связанных с этим и оказалось, что ответ довольно сложный!


Мне не удалось найти документы, объясняющие общую картину паники в Rust, так что это стоит записать.


(Бесстыдная статья: причина, по которой я заинтересовался этой темой, заключается в том что @Aaron1011 реализовал поддержку раскручивания стека в Miri.


Я хотел увидеть это в Miri с незапамятных времён, и у меня никогда не было времени, чтобы реализовать это самому, поэтому было действительно здорово видеть, как кто-то просто отправляет PR для поддержки этого на ровном месте.


После большого количества раундов проверки кода, он был влит совсем недавно.


Всё ещё есть некоторые грубые края, но основы определены точно.)


Целью этой статьи является документирование структуры высокого уровня и соответствующих интерфейсов, которые вступают в игру на стороне Rust.


Фактический механизм разматывания стека — это совершенно другой вопрос (о котором я не уполномочен говорить).


Примечание: эта статья описывает панику с этого коммита.


Многие из описанных здесь интерфейсов являются нестабильными внутренними деталями libstd и могут измениться в любое время.


Высокоуровневая структура


Пытаясь понять, как работает паника, читая код в libstd, можно легко потеряться в лабиринте.
Есть несколько уровней косвенности, которые соединяются только компоновщиком,
есть атрибут #[panic_handler] и «обработчик паники времени выполнения» (контролируемое стратегией паники, которая устанавливается через -C panic) и «ловушки паники», и оказывается, что паника в контексте #[no_std] требует совершенно другого пути к коду… очень многое происходит.


Что ещё хуже, RFC, описывающий ловушки паники, называет их «обработчик паники», но этот термин с тех пор был переопределён.


Я думаю, что лучшее место для начала — это интерфейсы, управляющие двумя направлениями:


  • Обработчик паники времени выполнения используется libstd для управления тем, что происходит после того, как информация о панике была напечатана в stderr.
    Это определяется стратегией паники: либо мы прерываем (-C panic=abort), либо запускаем разматывание стека (-C panic=unwind).
    (Обработка паники во время выполнения также обеспечивает реализацию catch_unwind, но здесь мы не будем говорить об этом.)


  • Обработчик паники используется libcore для реализации (а) паники, вставляемой генерацией кода (такой как паника, вызванная арифметическим переполнением или индексацией массива/среза за пределами границ) и (b) core::panic! макрос (это макрос panic! в самой libcore и в #[no_std] контексте #[no_std]).



Оба эти интерфейса реализуются через extern блоки: listd/libcore, соответственно, просто импортируют некоторую функцию, которой они делегируют, и где-то совсем в другом месте в дереве крейтов эта функция реализуется.


Импорт разрешается только во время связывания; Глядя локально на код нельзя сказать, где живёт фактическая реализация соответствующего интерфейса.
Неудивительно, что по пути я несколько раз терялся.


В дальнейшем оба этих интерфейса будут очень полезны; когда вы запутались. Первое, что нужно проверить, это не перепутали ли вы обработчик паники и обработчик паники времени выполнения.
(И помните, что есть также перехватчики паники, мы доберёмся до них.)
Это происходит со мной всё время.


Более того, core::panic! и std::panic! не одинаковы; как мы увидим, они используют совершенно разные пути кода.


libcore и libstd каждый реализуют свой собственный способ вызвать панику:


  • core::panic! из libcore очень мал: он всего лишь немедленно делегирует панику обработчику.


  • libstd std::panic! («нормальный» макрос panic! в Rust) запускает полнофункциональный механизм паники, который обеспечивает управляемый пользователем перехват паники.
    Хук по умолчанию выведет сообщение о панике в stderr.
    После того, как функция перехвата закончена, libstd делегирует её обработчику паники времени выполнения.


    libstd также предоставляет обработчик паники, который вызывает тот же механизм, поэтому core::panic! также заканчивается здесь.



Давайте теперь посмотрим на эти части более подробно.


Обработка паники во время выполнения программы


Интерфейс для среды выполнения паники (представленный этим RFC) представляет собой функцию __rust_start_panic(payload: usize) -> u32 которая импортируется libstd и позже разрешается компоновщиком.


Аргумент usize здесь на самом деле является *mut &mut dyn core::panic::BoxMeUp — это то место, где *mut &mut dyn core::panic::BoxMeUp «полезные данные» паники (информация, доступная при её обнаружении).


BoxMeUp — нестабильная внутренняя деталь реализации, но глядя на этот типаж, мы видим, что всё что он действительно делает, это оборачивает dyn Any + Send, который является типом полезных данных паники, возвращаемой catch_unwind и thread::spawn.


BoxMeUp::box_me_up возвращает Box<dyn Any + Send>, но в виде необработанного указателя (поскольку Box недоступен в контексте, где определён этот типаж); BoxMeUp::get просто заимствует содержимое.


Две реализации этого интерфейса поставляются в libpanic_unwind: libpanic_unwind для -C panic=unwind (по умолчанию на большинстве платформ) и libpanic_abort для -C panic=abort.


Макрос std::panic!


Поверх интерфейса времени выполнения паники, libstd реализует механизм паники Rust по умолчанию во внутреннем модуле std::panicking.


rust_panic_with_hook


Ключевая функция, через которую проходит почти всё, — rust_panic_with_hook:


fn rust_panic_with_hook(
payload: &mut dyn BoxMeUp,
message: Option<&fmt::Arguments<'_>>,
file_line_col: &(&str, u32, u32),
) -> !

Эта функция принимает местоположение источника паники, необязательное не отформатированное сообщение (см. Документацию fmt::Arguments) и полезные данные.


Его основная задача — вызывать то, чем является текущий перехватчик паники.
Перехватчики паники имеют аргумент PanicInfo, поэтому нам нужно расположение источника паники, информация о формате для сообщения о панике и полезные данные.
Это очень хорошо соответствует аргументу rust_panic_with_hook!
file_line_col и message могут использоваться непосредственно для первых двух элементов; payload превращается в &(dyn Any + Send) через интерфейс BoxMeUp.


Интересно, что стандартный перехватчик паники полностью игнорирует message; то что вы видите является приведением полезных данных к типу &str или String (что бы ни работало).
Предположительно, вызывающий код должен убедиться, что форматирование message, если оно присутствует, даёт тот же результат.
(И те, которые мы обсуждаем ниже, гарантируют это.)


Наконец, rust_panic_with_hook отправляется в текущий обработчик паники времени выполнения.


На данный момент, только payload по — прежнему актуальна — и что важно: message (со временем жизни '_ указывает, что могут содержаться короткоживущие ссылки, но полезные данные паники будут распространяться вверх по стеку и следовательно должные быть со временем жизни 'static).


Ограничение 'static там довольно хорошо скрыто, но через некоторое время я понял, что Any подразумевает 'static (и помните, что dyn BoxMeUp просто используется для получения Box<dyn Any + Send>).


Точки входа в libstd


rust_panic_with_hook — это закрытая функция для std::panicking; модуль предоставляет три точки входа поверх этой центральной функции и одну, которая её обходит:


  • Реализация обработчика паники по умолчанию, поддерживающая (как мы увидим) панику из core::panic! и встроенную панику (от арифметического переполнения или индексации массива/среза).
    Получает в качестве входных данных PanicInfo, и оно должно превратить это в аргументы для rust_panic_with_hook.
    Любопытно, что хотя компоненты PanicInfo и аргументы rust_panic_with_hook довольно похожи, и кажется, что их можно просто переслать, это не так.
    Вместо этого libstd полностью игнорирует компонент payload из PanicInfo и устанавливает фактические полезные данные (переданные в rust_panic_with_hook) так, чтобы в них содержался отформатированный message.


    В частности, это означает, что обработчик паники времени выполнения не имеет значения для приложений no_std.
    Он вступает в игру только тогда, когда используется реализация обработчика паники в libstd.
    (Стратегия паники, выбранная через -C panic всё ещё имеет значение, поскольку она также влияет на генерацию кода.
    Например, с -C panic=abort код может стать проще, так как не нужно поддерживать раскручивание стека).


  • begin_panic_fmt, поддерживающая версию форматной сроки std::panic! (т.е. это используется, когда вы передаёте несколько аргументов макросу).
    В основном происходит просто упаковка аргументов строки формата в PanicInfoфиктивными полезными данными) и вызовом обработчики паники по умолчанию, который мы только что обсуждали.


  • begin_panic, поддерживающая версию std::panic! с одним аргументом std::panic!.
    Интересно, что при этом используется совсем другой путь кода, чем в двух других точках входа!
    В частности, это единственная точка входа, которая позволяет передавать произвольные полезные данные.
    Эти полезные данные просто преобразуется в Box<dyn Any + Send>, чтобы его можно было передать в rust_panic_with_hook, и всё.


    В частности, перехватчик паники, который смотрит на полеmessage из PanicData, не сможет увидеть сообщение в std::panic!("do panic"), но он может видеть сообщение в std::panic!("panic with data: {}", data) поскольку последний вместо этого проходит через begin_panic_fmt.
    Это кажется довольно удивительным. (Но также обратите внимание, что PanicData::message() ещё не стабильна.)


  • update_count_then_panic оказалась странной: эта точка входа поддерживает resume_unwind и фактически не вызывает перехват паники.
    Вместо этого немедленно отправляется в обработчик паники.
    Например, begin_panic, позволяет вызывающей стороне выбирать произвольные полезные данные.
    В отличие от begin_panic, вызывающая функция отвечает за упаковку и определение размера полезных данных; функция update_count_then_panic просто пересылает свои аргументы почти дословно в обработчик паники времени выполнения.



Обработчик паники


std::panic! механизм действительно полезен, но он требует размещения данных в куче через Box, что не всегда доступно.
Чтобы дать libcore способ вызывать панику, были введены обработчики паники.
Как мы уже видели, если libstd доступен, он обеспечивает реализацию этого интерфейса core::panic! панику в представлениях libstd.


Интерфейс для обработчика паники — это функция fn panic(info: &core::panic::PanicInfo) -> ! libcore импортирует, и это позже разрешается компоновщиком.
Тип PanicInfo такой же, как и для перехватчиков паники: он содержит местоположение источника паники, сообщение о панике и полезные данные (dyn Any + Send).
Сообщение о панике представляется в виде fmt::Arguments, то есть строки форматирования с аргументами, которая ещё не была отформатирована.


Макрос core::panic!


Помимо интерфейса обработчика паники, libcore предоставляет минимальный API паники.
core::panic! макрос создаёт fmt::Arguments который затем передаётся обработчику паники.
Здесь не происходит форматирование, так как это потребует выделения памяти в куче; Вот почему PanicInfo содержит «не интерпретированную» строку формата со своими аргументами.


Любопытно, что поле payload из PanicInfo передаётся обработчику паники, всегда устанавливается в фиктивное значение.
Это объясняет, почему обработчик паники libstd игнорирует полезные данные (и вместо этого создаёт новые полезные данные из message), но это заставляет меня задуматься, почему это поле является частью API обработчика паники.
Другим следствием этого является то, что core::panic!("message") и std::panic!("message") (варианты без какого-либо форматирования) на самом деле приводят к очень разным паникам: первый превращается в fmt::Arguments, передаётся через интерфейс обработчика паники, а затем libstd создаёт полезные данные String путём её форматирования.
Последний, однако напрямую использует &str в качестве полезных данных, и поле message остаётся None (как уже упоминалось).


Некоторые элементы API паники в libcore являются элементами языка, потому что компилятор вставляет вызовы этих функций во время генерации кода:


  • Элемент языка panic вызывается, когда компилятору нужно вызвать панику, которая не требует какого-либо форматирования (например, арифметического переполнения); это та же самая функция, которая также поддерживает core::panic! одним аргументом core::panic!.
  • Элемент языка panic_bounds_check вызывается при неудачной проверке границ массива/среза.Он вызывает тот же метод, что и core::panic! с форматированием.

Выводы


Мы прошли через 4 уровня API, 2 из которых были перенаправлены через импортированные вызовы функций и разрешены компоновщиком.
Вот это путешествие!
Но мы достигли конца.
Я надеюсь, что вы не паниковали по пути. ;)


Я упомянул некоторые вещи как удивительные.
Оказывается, все они связаны с тем фактом, что перехватчики паники и обработчики паники разделяют структуру PanicInfo в своём интерфейсе, который содержит как необязательное ещё не отформатированное message и payload со стёртым типом:


  • Перехватчик паники всегда может найти уже отформатированное сообщение в payload, поэтому message кажется бессмысленным для перехватчиков.Фактически, message может отсутствовать, даже если payload содержит сообщение (например, для std::panic!("message")).
  • Обработчик паники никогда не будет на самом деле получать payload, так что поле кажется бессмысленным для обработчиков.

Читая RFC по описанию обработчика паники, кажется, что план был для core::panic! также поддерживать произвольные полезные данные, но до сих пор это не материализовалось.
Тем не менее, даже с этим будущим расширением, я думаю, у нас есть инвариант, что когда message имеет значение Some, тогда либо payload == &NoPayload (поэтому полезные данные избыточны), либо payload является форматированным сообщением (поэтому сообщение избыточно).


Интересно, есть ли случай, когда оба поля будут полезны и если нет, то можем ли мы закодировать это, сделав их двумя вариантами enum?


Вероятно, есть веские причины против этого предложения и для нынешнего дизайна; было бы здорово получить их где-нибудь в формате документации. :)


Есть ещё много чего сказать, но на этом этапе я приглашаю вас перейти по ссылкам на исходный код, который я включил выше.


Имея в виду структуру высокого уровня, вы должны быть в состоянии следовать этому коду.
Если бы люди думали, что этот обзор стоил бы поместить куда-то навсегда, я был бы рад превратить эту статью в блог в какую-то документацию — хотя я не уверен, что это было бы хорошим местом.


И если вы обнаружите какие-либо ошибки в том, что я написал, пожалуйста, дайте мне знать!

Комментарии (1)


  1. freecoder_xx
    01.01.2020 00:49

    Вот я даже не знаю, что будет проще: пройти путь автора самому и разобраться в паниках из кода, или вкурить эту статью… (: