Неисправимые ошибки с panic!
Иногда в коде возникают критические ошибки, и от этого никуда не деться. Для этих случаев у Rust есть макрос panic!
. На практике существует два способа вызвать панику: путём выполнения действия, которое вызывает панику (например, обращение к массиву за пределами его размера) или путём явного вызова макроса panic!
. В обоих случаях мы вызываем панику в нашей программе. По умолчанию, паника выводит сообщение об ошибке, раскручивает и очищает стек вызовов и завершает работу. С помощью переменной окружения вы также можете заставить Rust отображать стек вызовов при возникновении паники, чтобы было легче отследить источник паники.
Раскрутить стек или прервать исполнение программы?
По умолчанию, когда происходит паника, программа начинает процесс раскрутки стека, означающий проход обратно по стеку вызовов и очистку данных для каждой обнаруженной функции. Тем не менее, этот обратный проход по стеку и очистка требуют много работы. Rust в качестве альтернативы предоставляет вам возможность немедленного прерывания исполнения, которое завершает работу программы без очистки.
Память, которую использовала программа, должна быть очищена операционной системой. Если в вашем проекте нужно сделать исполняемый файл маленьким настолько, насколько это возможно, вы можете прямо указать программе прерывать исполнение в случае паники, добавив
panic = 'abort'
в соответствующие разделы[profile]
вашего файла Cargo.toml. Например, если вы хотите прерывать панику в релизном режиме, добавьте это:[profile.release] panic = 'abort'
Давайте попробуем вызвать panic!
в простой программе:
fn main() { panic!("критическая ошибка"); }
При запуске программы вы увидите подобный вывод:
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
Running `target/debug/panic`
thread 'main' panicked at src/main.rs:2:5:
критическая ошибка
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Выполнение макроса panic!
вызывает сообщение об ошибке, представленное в двух последних строчках выше. Первая строчка содержит сообщение паники и место в исходном коде, где возникла паника: запись src/main.rs:2:5 указывает на второй строчки пятый символ внутри файла src/main.rs.
В нашем случае, сообщение об ошибке указывает на наш собственный код, и если мы перейдём к этой строке, мы увидим вызов макроса panic!
. В других случаях вызов panic!
мог бы произойти в стороннем коде, вызываемом нашим, и тогда имя файла и номер строки указывали бы именно туда, где конкретно вызывается макрос panic!
, а не на тот код, вызов которого привёл к вызову panic!
.
Мы можем использовать развёртку вызовов функций, приводящих к вызову panic!
, чтобы выяснить, какая часть нашего кода вызывает проблему. Чтобы понять, как использовать развёртку вызова panic!
, давайте рассмотрим другой пример и посмотрим, каково это, когда panic!
вызывается не напрямую через вызов макроса, а в библиотечном коде, содержащем баг. В Листинге 9-1 приведен некоторый код, который пытается получить доступ элементу вектора по индексу, лежащему за пределами вектора.
fn main() { let v = vec![1, 2, 3]; v[99]; }
Здесь мы пытаемся получить доступ к 100му элементу вектора (который находится по индексу 99, потому что индексирование начинается с нуля), но вектор имеет только 3 элемента. В этой ситуации, Rust будет вызывать панику. Использование []
должно возвращать элемент, но если вы передадите неверный индекс, Rust не сможет найти ничего, что можно было бы корректно вернуть.
В языке C, попытка прочесть за пределами конца структуры данных (в нашем случае, вектора) приведёт к неопределённому поведению. А именно, вы всё-таки получите значение, которое находится в том месте памяти компьютера, которое соответствовало бы этому элементу в структуре данных — даже несмотря на то, что это место к ней относиться никак не будет. Это называется чтением за границами буфера (buffer overread) — уязвимостью в виде возможности чтения злоумышленником данных, к которым у него не должно быть доступа, вызванной возможностью читать произвольные области памяти с помощью некорректных индексов.
Чтобы защищать вашу программу от такого рода уязвимостей при попытке прочитать элемент по некорректному индексу, Rust останавливает исполнение и отказывается продолжать работу программы. Посмотрим, как это выглядит:
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/panic`
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Эта ошибка указывает на строку 4 нашего main.rs, где мы пытаемся получить доступ по индексу 99
к элементу вектора v
.
Строка note:
сообщает нам, что мы можем установить переменную среды RUST_BACKTRACE
, чтобы получать развёртку вызовов, последовательность которых привела к ошибке. Развёртка вызовов (backtrace) — это последовательный список вложенных вызовов. Развёртка вызовов в Rust работает так же, как и в других языках: её нужно читать от начала и до того момента, когда упоминается ваш код. Именно отсюда начинается ваша собственная ошибка. Строки выше — это указания на код, вызываемый строчкой, где начались проблемы; строки ниже — это указания на код, вызывающий ваш проблемный код. Эти строки выше и ниже могут содержать код ядра Rust, код из стандартной библиотеки или из используемых вами крейтов. Давайте попробуем выполнить развёртку вызовов, установив для переменной окружения RUST_BACKTRACE
любое значение, кроме 0
. В Листинге 9-2 показан соответствующий пример.
$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
0: rust_begin_unwind
at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/std/src/panicking.rs:662:5
1: core::panicking::panic_fmt
at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/panicking.rs:74:14
2: core::panicking::panic_bounds_check
at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/panicking.rs:276:5
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/slice/index.rs:302:10
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/slice/index.rs:16:9
5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/alloc/src/vec/mod.rs:2920:9
6: panic::main
at ./src/main.rs:4:6
7: core::ops::function::FnOnce::call_once
at /rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
Сколько текста! Вывод, который вы увидите, может отличаться от представленного выше, в зависимости от вашей операционной системы и версии Rust. Для того, чтобы получить развёртку вызовов с этой информацией, должны быть включены настройки отладки. Настройки отладки включены по умолчанию при использовании cargo build
или cargo run
, если вы не указываете флаг --release
.
В развёртке вызовов из Листинга 9-2, строка 6: ...
указывает на строчку в нашем проекте, которая и вызывала проблему: строчка 4 файла src/main.rs. Если мы не хотим, чтобы наша программа паниковала, мы должны начать анализ с места, на которое указывает первая строка с упоминанием нашего собственного файла. В Листинге 9-1, где мы для демонстрации развёртки вызовов сознательно написали код, который паникует, способ исправления паники состоит в том, чтобы не запрашивать элемент за пределами диапазона значений индексов вектора. Когда вы встретитесь с паникой в своих будущих программах, вам нужно будет выяснить, что над чем конкретно делает код, и что он должен делать по задумке.
Мы вернёмся к обсуждению макроса panic!
и того, когда нам следует и не следует его использовать для обработки ошибок, в разделе "Когда следует использовать panic!
?" этой главы. Далее мы рассмотрим, как можно продолжить исполнение программы после ошибки, воспользовавшись типом Result
.