خطاهای غیرقابل بازیابی با panic!

گاهی اوقات اتفاقات بدی در کد شما رخ می‌دهد و هیچ کاری نمی‌توانید در مورد آن انجام دهید. در این موارد، Rust ماکروی panic! را ارائه می‌دهد. دو راه برای ایجاد یک خطا با panic! وجود دارد: با انجام عملی که باعث ایجاد خطا می‌شود (مانند دسترسی به یک اندیس (index)خارج از محدوده در یک آرایه) یا با صراحت فراخوانی ماکروی panic!. در هر دو حالت، ما یک خطا در برنامه خود ایجاد می‌کنیم. به طور پیش‌فرض، این خطاها یک پیام خطا چاپ می‌کنند، استک را unwind می‌کنند، داده‌ها را پاکسازی می‌کنند و برنامه را متوقف می‌کنند. با استفاده از یک متغیر محیطی، می‌توانید Rust را مجبور کنید هنگام وقوع یک panic، استک فراخوانی را نمایش دهد تا ردیابی منبع خطا آسان‌تر شود.

Unwinding the Stack یا متوقف کردن در پاسخ به یک Panic

به طور پیش‌فرض، هنگامی که یک خطا رخ می‌دهد، برنامه شروع به unwinding می‌کند، که به معنی این است که Rust استک را به سمت بالا پیمایش می‌کند و داده‌ها را از هر تابعی که با آن برخورد می‌کند پاکسازی می‌کند. با این حال، پیمایش به بالا و پاکسازی کار زیادی است. بنابراین، Rust به شما اجازه می‌دهد گزینه جایگزین abort کردن فوری را انتخاب کنید، که برنامه را بدون پاکسازی متوقف می‌کند.

حافظه‌ای که برنامه استفاده می‌کرد نیاز به پاکسازی توسط سیستم عامل خواهد داشت. اگر در پروژه خود نیاز دارید تا فایل باینری حاصل را تا حد ممکن کوچک کنید، می‌توانید از unwind به abort در زمان خطا تغییر دهید با اضافه کردن panic = 'abort' به بخش‌های مناسب [profile] در فایل Cargo.toml خود. برای مثال، اگر می‌خواهید در حالت release در زمان وقوع خطا متوقف شوید، این مورد را اضافه کنید:

[profile.release]
panic = 'abort'

بیایید فراخوانی panic! را در یک برنامه ساده امتحان کنیم:

Filename: 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' panicked at src/main.rs:2:5:
crash and burn
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

فراخوانی panic! پیام خطای موجود در دو خط آخر را ایجاد می‌کند. خط اول پیام خطای panic! ما و مکانی در کد منبع ما که این خطا رخ داده است را نشان می‌دهد: src/main.rs:2:5 نشان می‌دهد که این خط دوم، پنجمین کاراکتر در فایل src/main.rs ما است.

در این مورد، خط نشان داده شده بخشی از کد ما است، و اگر به آن خط برویم، فراخوانی ماکروی panic! را می‌بینیم. در موارد دیگر، فراخوانی panic! ممکن است در کدی باشد که کد ما آن را فراخوانی می‌کند، و نام فایل و شماره خط گزارش شده توسط پیام خطا کدی از دیگران را نشان می‌دهد که در آن ماکروی panic! فراخوانی شده است، نه خطی از کد ما که در نهایت منجر به فراخوانی panic! شد.

ما می‌توانیم از backtrace توابعی که فراخوانی panic! از آن‌ها آمده است استفاده کنیم تا بخش کد ما که باعث مشکل شده است را پیدا کنیم. برای درک نحوه استفاده از backtrace یک panic!، بیایید یک مثال دیگر ببینیم و مشاهده کنیم زمانی که یک فراخوانی panic! از یک کتابخانه به دلیل یک باگ در کد ما رخ می‌دهد، به جای اینکه کد ما مستقیماً ماکرو را فراخوانی کند، چگونه است. لیست ۹-۱ کدی دارد که تلاش می‌کند به یک اندیس (index)در یک بردار که خارج از محدوده اندیس‌های معتبر است دسترسی پیدا کند.

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

    v[99];
}
Listing 9-1: تلاش برای دسترسی به عنصری که خارج از انتهای بردار است، که منجر به فراخوانی panic! خواهد شد

در اینجا، ما سعی داریم به عنصر صدم بردار خود دسترسی پیدا کنیم (که در اندیس (index)۹۹ است زیرا اندیس‌گذاری از صفر شروع می‌شود)، اما بردار فقط سه عنصر دارد. در این وضعیت، Rust با یک خطا متوقف می‌شود. استفاده از [] قرار است یک عنصر را بازگرداند، اما اگر یک اندیس (index)نامعتبر را ارائه دهید، هیچ عنصری وجود ندارد که Rust بتواند به درستی بازگرداند.

در زبان C، تلاش برای خواندن فراتر از انتهای یک ساختار داده رفتاری نامشخص دارد. ممکن است هر چیزی که در مکان حافظه‌ای که با آن عنصر در ساختار داده مطابقت دارد باشد را دریافت کنید، حتی اگر آن حافظه متعلق به آن ساختار نباشد. این به عنوان buffer overread شناخته می‌شود و می‌تواند به آسیب‌پذیری‌های امنیتی منجر شود اگر یک مهاجم بتواند اندیس (index)را به گونه‌ای دستکاری کند که داده‌هایی را بخواند که نباید به آن‌ها دسترسی داشته باشد و پس از ساختار داده ذخیره شده‌اند.

برای محافظت از برنامه شما در برابر این نوع آسیب‌پذیری، اگر تلاش کنید یک عنصر را در یک اندیسی که وجود ندارد بخوانید، 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

این خطا به خط ۴ فایل main.rs ما اشاره می‌کند، جایی که سعی داریم به اندیس (index)99 بردار v دسترسی پیدا کنیم.

خط note: به ما می‌گوید که می‌توانیم متغیر محیطی RUST_BACKTRACE را تنظیم کنیم تا یک backtrace دقیقاً از آنچه باعث خطا شده است دریافت کنیم. یک backtrace لیستی از تمام توابعی است که تا این نقطه فراخوانی شده‌اند. backtraceها در Rust مانند زبان‌های دیگر کار می‌کنند: کلید خواندن backtrace این است که از بالا شروع کرده و تا زمانی که فایل‌هایی که شما نوشته‌اید را ببینید، بخوانید. این همان جایی است که مشکل از آنجا منشأ گرفته است. خطوط بالاتر از آن نقطه کدی است که کد شما فراخوانی کرده است؛ خطوط پایین‌تر کدی است که کد شما را فراخوانی کرده است. این خطوط قبل و بعد ممکن است شامل کد هسته Rust، کد کتابخانه استاندارد، یا کرایت‌هایی که استفاده می‌کنید باشند. بیایید با تنظیم متغیر محیطی RUST_BACKTRACE به هر مقداری به غیر از 0 یک backtrace دریافت کنیم. لیست ۹-۲ خروجی مشابه چیزی را که خواهید دید نشان می‌دهد.

$ 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.
Listing 9-2: backtrace تولید شده توسط فراخوانی به panic! که وقتی متغیر محیطی RUST_BACKTRACE تنظیم شده است نمایش داده می‌شود

این مقدار زیادی خروجی است! خروجی دقیق ممکن است بسته به سیستم عامل و نسخه Rust شما متفاوت باشد. برای دریافت backtraceها با این اطلاعات، باید نمادهای اشکال‌زدایی (debug symbols) فعال باشند. نمادهای اشکال‌زدایی به طور پیش‌فرض هنگام استفاده از cargo build یا cargo run بدون فلگ --release فعال هستند، همانطور که در اینجا انجام دادیم.

در خروجی لیست ۹-۲، خط ۶ از backtrace به خطی در پروژه ما اشاره می‌کند که باعث مشکل شده است: خط ۴ فایل src/main.rs. اگر نمی‌خواهیم برنامه ما دچار خطا شود، باید بررسی خود را از مکانی که توسط اولین خطی که اشاره به فایلی که نوشته‌ایم دارد، آغاز کنیم. در لیست ۹-۱، جایی که به عمد کدی نوشته‌ایم که باعث خطا شود، راه حل رفع این خطا این است که درخواست یک عنصر فراتر از محدوده اندیس‌های بردار نکنیم. زمانی که کد شما در آینده دچار خطا می‌شود، باید بفهمید که کد با چه مقادیری چه عملی انجام می‌دهد که باعث خطا می‌شود و کد چه کاری باید انجام دهد.

ما در بخش “To panic! or Not to panic! که بعداً در این فصل آمده است، دوباره به موضوع panic! و زمانی که باید و نباید از panic! برای مدیریت شرایط خطا استفاده کنیم بازخواهیم گشت. اکنون، به بررسی نحوه بازیابی از یک خطا با استفاده از Result می‌پردازیم.