مقدمه

به فصل «قابلیت‌های پیشرفته‌تر Rust» خوش آمدید. تاکنون تمام کدهایی که نوشته‌ایم، توسط تضمین‌های ایمنی حافظه کامپایلر Rust در زمان کامپایل بررسی شده‌اند. اما Rust یک زبان دیگر در دل خود پنهان کرده که این تضمین‌ها را اعمال نمی‌کند: «راست ناامن» یا Unsafe Rust. این قابلیت به ما اجازه می‌دهد تا برخی از محدودیت‌های کامپایلر را دور بزنیم و کنترل سطح پایین‌تری بر روی سیستم داشته باشیم.

استفاده از کد ناامن به معنی غیرفعال کردن borrow checker یا دیگر مکانیزم‌های ایمنی Rust نیست. بلکه به شما اجازه می‌دهد تا به پنج قابلیت اضافی دسترسی پیدا کنید که کامپایلر نمی‌تواند ایمنی آنها را تضمین کند. این قابلیت‌ها به «ابرقدرت‌های ناامن» (unsafe superpowers) معروفند و باید با احتیاط فراوان استفاده شوند.

ابرقدرت‌های ناامن

برای استفاده از این قابلیت‌ها، باید کد خود را در یک بلوک unsafe قرار دهیم. این بلوک به کامپایلر می‌گوید: "به من اعتماد کن، من می‌دانم چه کار می‌کنم و مسئولیت ایمنی این بخش از کد را بر عهده می‌گیرم." این پنج ابرقدرت عبارتند از:

  • Dereference کردن یک اشاره‌گر خام (raw pointer)
  • فراخوانی یک تابع یا متد ناامن
  • دسترسی یا تغییر یک متغیر استاتیک تغییرپذیر
  • پیاده‌سازی یک trait ناامن
  • دسترسی به فیلدهای یک union

در این درس، ما بر روی اولین و رایج‌ترین مورد، یعنی کار با اشاره‌گرهای خام، تمرکز خواهیم کرد.

کار با اشاره‌گرهای خام (Raw Pointers)

اشاره‌گرهای خام، که به دو صورت *const T (تغییرناپذیر) و *mut T (تغییرپذیر) وجود دارند، شبیه به رفرنس‌ها هستند اما با این تفاوت که قوانین وام‌گیری روی آنها اعمال نمی‌شود.

  • آنها مجازند که چندین رفرنس تغییرپذیر و تغییرناپذیر به صورت همزمان به یک مکان داشته باشند.
  • تضمینی برای معتبر بودن آدرسی که به آن اشاره می‌کنند، وجود ندارد.
  • آنها مجازند که null باشند.
  • آنها هیچ مکانیزم ردیابی خودکاری (مانند شمارش ارجاع) را پیاده‌سازی نمی‌کنند.

شما می‌توانید یک اشاره‌گر خام را در کد ایمن ایجاد کنید، اما برای dereference کردن آن (یعنی دسترسی به داده‌ای که به آن اشاره می‌کند)، باید حتماً در یک بلوک unsafe باشید.

Copy Icon src/main.rs
fn main() {
    let mut num = 5;

    // Create raw pointers from references. This is safe.
    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;

    // Dereferencing raw pointers must be done in an unsafe block.
    unsafe {
        println!("r1 is: {}", *r1);
        println!("r2 is: {}", *r2);
    }
}

در این کد، ما از رفرنس‌های عادی، دو اشاره‌گر خام می‌سازیم. این کار ایمن است. اما برای خواندن مقدار آنها با عملگر `*`، کد را در یک بلوک unsafe قرار داده‌ایم. با این کار، ما به کامپایلر می‌گوییم که تضمین می‌کنیم این اشاره‌گرها در این لحظه معتبر هستند.

فراخوانی توابع ناامن

دومین ابرقدرت ناامن، فراخوانی توابع و متدهایی است که با کلمه کلیدی unsafe علامت‌گذاری شده‌اند. این توابع پیش‌شرط‌هایی دارند که کامپایلر نمی‌تواند آنها را بررسی کند و این وظیفه برنامه‌نویس است که قبل از فراخوانی، از برقراری آن پیش‌شرط‌ها اطمینان حاصل کند.

یک مثال رایج، فراخوانی توابع از زبان‌های دیگر (مانند C) از طریق Foreign Function Interface (FFI) است.

Copy Icon src/main.rs
extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C is {}", abs(-3));
    }
}

در اینجا، ما تابع abs را از کتابخانه استاندارد زبان C فراخوانی می‌کنیم. از آنجایی که کامپایلر Rust نمی‌تواند ایمنی کدهای خارجی را تضمین کند، ما باید فراخوانی آن را در یک بلوک unsafe قرار دهیم.

در این درس با مفهوم کد ناامن در Rust و ابرقدرت‌هایی که در اختیار ما قرار می‌دهد، آشنا شدیم. دیدیم که چگونه با استفاده از بلوک unsafe می‌توانیم کنترل سطح پایین‌تری بر روی سیستم داشته باشیم، اما در عین حال مسئولیت تضمین ایمنی آن کد را نیز بر عهده می‌گیریم. در درس بعدی، به بررسی «چند Trait پیشرفته» خواهیم پرداخت که به ما اجازه می‌دهند تا قابلیت‌های بسیار قدرتمندی را در نوع‌های خود پیاده‌سازی کنیم.