Неустранимые ошибки с макросом panic!

Иногда в коде происходят плохие вещи, и вы ничего не можете с этим поделать. В этих случаях у Rust есть макрос panic!. Когда выполняется макрос panic!, ваша программа напечатает сообщение об ошибке, раскрутит и очистит стек вызовов, а затем завершится. Это чаще всего происходит, когда был обнаружен какой-то дефект и программисту не ясно, как его обработать.

Раскручивать стек или прерывать выполнение программы в ответ на панику?

По умолчанию, когда происходит паника, программа начинает процесс раскрутки стека, означающий в Rust проход обратно по стеку вызовов и очистку данных для каждой обнаруженной функции. Но данный проход по стеку в обратном порядке и очистка генерируют много работы. Альтернативой является немедленное прерывание выполнения, которое завершает программу без очистки. Память, которую использовала программа, должна быть очищена операционной системой. Если в вашем проекте нужно сделать маленьким исполняемый файл, насколько это возможно, вы можете переключиться с варианта раскрутки стека на вариант прерывания, добавьте panic = 'abort' в раздел [profile] вашего Cargo.toml файла. Например, если вы хотите прерывать выполнение программы по панике в релизной версии программы добавьте следующее:

[profile.release]
panic = 'abort'

Давайте попробуем вызвать panic! в простой программе:

Файл: src/main.rs

fn main() {
    panic!("crash and burn");
}

При запуске программы, вы увидите что-то вроде этого:

$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished dev [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/panic`
thread 'main' panicked at 'crash and burn', 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 приведён код, который пытается получить доступ к элементу по индексу в векторе.

Файл: src/main.rs

fn main() {
    let v = vec![1, 2, 3];

    v[99];
}

Листинг 9-1. Попытка доступа к элементу за пределами вектора, которая вызовет panic!

Здесь мы пытаемся получить доступ к 100-му элементу вектора (который находится по индексу 99, потому что индексирование начинается с нуля), но вектор имеет только 3 элемента. В этой ситуации, Rust будет вызывать панику. Использование [] должно возвращать элемент, но вы передаёте неверный индекс: не существует элемента, который Rust мог бы вернуть.

В языке C, например, попытка прочесть за пределами конца структуры данных (в нашем случае векторе) приведёт к неопределённому поведению, undefined behavior, UB. Вы всё равно получите значение, которое находится в том месте памяти компьютера, которое соответствовало бы этому элементу в векторе, несмотря на то, что память по тому адресу совсем не принадлежит вектору (всё просто: C рассчитал бы место хранения элемента с индексом 99 и считал бы то, что там хранится, упс). Это называется чтением за пределом буфера, buffer overread, и может привести к уязвимостям безопасности. Если злоумышленник может манипулировать индексом таким образом, то у него появляется возможность читать данные, которые он не должен иметь возможности читать.

Чтобы защитить вашу программу от такого рода уязвимостей при попытке прочитать элемент с индексом, которого не существует, Rust остановит выполнение и откажется продолжить работу программы. Давайте попробуем так сделать и посмотрим на поведение Rust:

$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished dev [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Данная ошибка указывает на файл, который не является нашим, файл libcore/slice/mod.rs. Это файл с исходными кодами реализации slice, как вы возможно заметили, он расположен в исходных кодах Rust. Когда мы используем [] для вектора v запускается код находящийся в libcore/slice/mod.rs и это является тем местом, где на самом деле происходит вызов panic!.

Следующая строка говорит, что мы можем установить переменную среды RUST_BACKTRACE, чтобы получить обратную трассировку того, что именно стало причиной ошибки. Обратная трассировка создаёт список всех функций, которые были вызваны до какой-то определённой точки выполнения программы. Обратная трассировка в Rust работает так же, как и в других языках. Поэтому предлагаем вам читать данные обратной трассировки как и везде - сверху вниз, пока не увидите информацию о файлах написанных вами. Это место, где возникла проблема. Другие строки трассировки, которые находятся над строками с упоминанием наших файлов, - это код, который вызывается нашим кодом; строки ниже являются кодом, который вызывает наш код. Эти строки могут включать основной код Rust, код стандартной библиотеки или используемые крейты. Давайте попробуем получить обратную трассировку с помощью установки переменной среды RUST_BACKTRACE в любое значение, кроме 0. Листинг 9-2 показывает вывод, подобный тому, что вы увидите.

$  RUST_BACKTRACE=1 cargo run
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
stack backtrace:
   0: rust_begin_unwind
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/std/src/panicking.rs:483
   1: core::panicking::panic_fmt
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/panicking.rs:85
   2: core::panicking::panic_bounds_check
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/panicking.rs:62
   3: <usize as core::slice::index::SliceIndex<[T]>>::index
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/slice/index.rs:255
   4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/slice/index.rs:15
   5: <alloc::vec::Vec<T> as core::ops::index::Index<I>>::index
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/alloc/src/vec.rs:1982
   6: panic::main
             at ./src/main.rs:4
   7: core::ops::function::FnOnce::call_once
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ops/function.rs:227
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

Листинг 9-2. Обратная трассировка, сгенерированная вызовом panic!, когда установлена переменная окружения RUST_BACKTRACE

Тут много вывода! Вывод, который вы увидите, может отличаться от представленного, в зависимости от вашей операционной системы и версии Rust. Для того, чтобы получить обратную трассировку с этой информацией, должны быть включены символы отладки, debug symbols. Символы отладки включены по умолчанию при использовании cargo build или cargo run без флага --release, как у нас в примере.

В выводе обратной трассировки, в листинге 9-2, строка 12 указывает на строку в нашем проекте, который вызывал проблему: строка 4 из файла src/main.rs. Если мы не хотим возникновения паники в программе, место на которое указывает первая строка трассировки, упоминающая название нашего файла, - это то место, где мы должны начать расследование. В листинге 9-1, где мы для демонстрации обратной трассировки сознательно написали код, который паникует, способ исправления паники состоит в том, чтобы не запрашивать элемент с индексом 99 из вектора, который содержит только 3 элемента. Когда ваш код запаникует в будущем, вам нужно будет выяснить, какое выполняющееся кодом действие, с какими значениями вызывает панику и что этот код должен делать вместо этого.

Мы вернёмся к обсуждению макроса panic!, и того когда нам следует и не следует использовать panic! для обработки ошибок в разделе "panic! или НЕ panic!" этой главы. Далее мы рассмотрим, как восстановить выполнение программы после исправляемых ошибок, использующих тип Result.