Как работает паника в 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
?
Вероятно, есть веские причины против этого предложения и для нынешнего дизайна; было бы здорово получить их где-нибудь в формате документации. :)
Есть ещё много чего сказать, но на этом этапе я приглашаю вас перейти по ссылкам на исходный код, который я включил выше.
Имея в виду структуру высокого уровня, вы должны быть в состоянии следовать этому коду.
Если бы люди думали, что этот обзор стоил бы поместить куда-то навсегда, я был бы рад превратить эту статью в блог в какую-то документацию — хотя я не уверен, что это было бы хорошим местом.
И если вы обнаружите какие-либо ошибки в том, что я написал, пожалуйста, дайте мне знать!
Автор: andreevlex