Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Неисправимые ошибки и panic!

Иногда в коде случается что-то плохое, и с этим ничего не поделать. На такие случаи в Rust есть макрос panic!. На практике вызвать панику можно двумя способами: сделать что-то, отчего код запаникует (например, обратиться за конец массива), или явно вызвать макрос panic!. В обоих случаях в программе возникает паника. По умолчанию при панике программа выводит сообщение об ошибке, раскручивает и очищает стек и завершается. С помощью переменной окружения можно также попросить Rust при панике показывать стек вызовов — так проще найти её источник.

Раскрутка стека или аварийное завершение при панике

По умолчанию при панике программа начинает раскрутку стека (unwinding): Rust проходит по стеку в обратном направлении и очищает данные каждой функции, которая ему встречается. Но такой обход и очистка — большая работа. Поэтому Rust позволяет выбрать другой вариант — немедленное аварийное завершение (abort), при котором программа заканчивается без всякой очистки.

Память, которую занимала программа, тогда придётся освобождать операционной системе. Если в вашем проекте нужно, чтобы бинарник был как можно меньше, можно переключиться с раскрутки на аварийное завершение при панике, добавив 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` profile [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/panic`

thread 'main' (6018279) 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! вызывается не в нашем коде, а в коде, который вызывается из нашего, — например, в библиотеке. Тогда в сообщении будет файл и строка из этого чужого кода, а не строка нашего кода, которая в итоге привела к панике.

Чтобы найти часть нашего кода, которая вызывает проблему, можно воспользоваться трассировкой (backtrace) функций, из которых пришёл вызов panic!. Чтобы понять, как с ней работать, рассмотрим другой пример: на этот раз panic! вызывается не нашим кодом напрямую, а библиотекой — из-за бага в нашем коде. В листинге 9-1 код пытается обратиться к элементу вектора по индексу за пределами допустимых.

Файл: src/main.rs
fn main() {
    let v = vec![1, 2, 3];

    v[99];
}
Листинг 9-1: Попытка обратиться к элементу за концом вектора, которая приведёт к вызову panic!

Здесь мы пытаемся получить сотый элемент вектора (с индексом 99, ведь нумерация начинается с нуля), а в векторе всего три элемента. В такой ситуации 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' (6017887) 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.
Листинг 9-2: Трассировка, которую выводит вызов panic!, если задана переменная окружения RUST_BACKTRACE

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

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

Мы ещё вернёмся к panic! и обсудим, когда стоит, а когда не стоит обрабатывать ошибки с его помощью, в разделе «Паниковать или не паниковать» этой главы. А дальше посмотрим, как восстанавливаться после ошибок с помощью Result.