Невосстанавливаемые ошибки с panic!
Иногда в вашем коде происходят плохие вещи, и вы ничего не можете с этим поделать. В таких случаях Rust предоставляет макрос panic!. На практике есть два способа вызвать панику: выполнить действие, которое приводит к панике (например, доступ к элементу массива за его пределами), или явно вызвать макрос panic!. В обоих случаях мы вызываем панику в нашей программе. По умолчанию такие паники выводят сообщение об ошибке, выполняют разворачивание стека (unwinding), очищают стек и завершают программу. С помощью переменной окружения вы также можете заставить Rust отображать трассировку стека при панике, чтобы упростить поиск источника проблемы.
Разворачивание стека или аварийное завершение в ответ на панику
По умолчанию, когда происходит паника, программа начинает разворачивать стек (unwinding), что означает, что Rust проходит вверх по стеку и очищает данные из каждой встреченной функции. Однако такой процесс требует много работы. Поэтому Rust позволяет выбрать альтернативу — немедленное аварийное завершение (aborting), которое завершает программу без очистки.
Память, которую использовала программа, затем будет очищена операционной системой. Если в вашем проекте необходимо сделать итоговый бинарный файл как можно меньше, вы можете переключиться с разворачивания на аварийное завершение при панике, добавив panic = 'abort' в соответствующие секции [profile] в вашем файле Cargo.toml. Например, если вы хотите аварийно завершать программу при панике в режиме выпуска (release), добавьте это:
[profile.release]
panic = 'abort'
Давайте попробуем вызвать panic! в простой программе:
fn main() { panic!("crash and burn"); }
При запуске программы вы увидите что-то вроде этого:
$ 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:
crash and burn
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Вызов panic! вызывает сообщение об ошибке, содержащееся в последних двух строках. Первая строка показывает наше сообщение о панике и место в исходном коде, где произошла паника: src/main.rs:2:5 указывает, что это вторая строка, пятый символ нашего файла src/main.rs.
В этом случае указанная строка является частью нашего кода, и если мы перейдем к этой строке, мы увидим вызов макроса panic!. В других случаях вызов panic! может находиться в коде, который вызывает наш код, и имя файла и номер строки, указанные в сообщении об ошибке, будут относиться к чужому коду, где вызван макрос panic!, а не к строке нашего кода, которая в конечном итоге привела к этому вызову.
Мы можем использовать трассировку стека (backtrace) функций, из которых произошел вызов panic!, чтобы выяснить, какая часть нашего_code вызывает проблему. Чтобы понять, как использовать трассировку стека при panic!, давайте рассмотрим другой пример и посмотрим, как это выглядит, когда вызов panic! исходит из крейта из-за ошибки в нашем коде, а не из нашего прямого вызова макроса. В листинге 9-1 есть код, который пытается получить доступ к элементу вектора по индексу, выходящему за пределы допустимых индексов.
fn main() { let v = vec![1, 2, 3]; v[99]; }
panic!Здесь мы пытаемся получить доступ к 100-му элементу нашего вектора (который имеет индекс 99, так как индексация начинается с нуля), но вектор содержит только три элемента. В такой ситуации Rust вызовет панику. Использование [] предполагает возврат элемента, но если передать недопустимый индекс, не существует элемента, который Rust мог бы вернуть и который был бы корректным.
В C попытка чтения за пределами структуры данных приводит к неопределённому поведению (undefined behavior). Вы можете получить любые данные, находящиеся в памяти, которые соответствуют этому элементу в структуре, даже если память не принадлежит этой структуре. Это называется переполнением буфера при чтении (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/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/std/src/panicking.rs:692:5
1: core::panicking::panic_fmt
at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:75:14
2: core::panicking::panic_bounds_check
at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:273:5
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:274:10
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:16:9
5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:3361:9
6: panic::main
at ./src/main.rs:4:6
7: core::ops::function::FnOnce::call_once
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
panic!, отображаемая при установке переменной окружения RUST_BACKTRACEЭто очень много вывода! Точный вывод, который вы увидите, может отличаться в зависимости от вашей операционной системы и версии Rust. Чтобы получать трассировки стека с такой информацией, должны быть включены символы отладки (debug symbols). Символы отладки включены по умолчанию при использовании cargo build или cargo run без флага --release, как мы и сделали.
В выводе в листинге 9-2 строка 6 трассировки стека указывает на строку в нашем проекте, которая вызывает проблему: строку 4 файла src/main.rs. Если мы не хотим, чтобы наша программа паниковала, мы должны начать расследование в месте, на которое указывает первая строка, упоминающая файл, который мы написали. В листинге 9-1, где мы намеренно написали код, который вызовет панику, способ исправить панику — не запрашивать элемент за пределами диапазона индексов вектора. Когда ваш код паникует в будущем, вам нужно будет выяснить, какое действие выполняет код с какими значениями, что вызывает панику, и что код должен делать вместо этого.
Мы вернемся к panic! и к тому, когда мы должны и не должны использовать panic! для обработки условий ошибок, в разделе «panic! или не panic!» позже в этой главе. Далее мы посмотрим, как восстанавливаться после ошибки с помощью Result.