Unsafe Rust

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

وجود Unsafe Rust به این دلیل است که تحلیل ایستا ذاتاً محافظه‌کارانه است. وقتی کامپایلر سعی می‌کند تعیین کند که آیا کد تضمین‌ها را رعایت می‌کند یا نه، بهتر است برخی از برنامه‌های معتبر را رد کند تا اینکه برخی از برنامه‌های نامعتبر را بپذیرد. اگرچه ممکن است کد درست باشد، اما اگر کامپایلر راست اطلاعات کافی برای اطمینان نداشته باشد، کد را رد خواهد کرد. در این موارد، می‌توانید از کد ناامن برای گفتن به کامپایلر استفاده کنید: «به من اعتماد کن، من می‌دانم چه کار می‌کنم.» اما هشدار داده شود که شما از کد ناامن به مسئولیت خودتان استفاده می‌کنید: اگر از کد ناامن به‌طور نادرست استفاده کنید، مشکلاتی ممکن است به دلیل ناامنی حافظه ایجاد شوند، مانند dereferencing اشاره‌گر (Pointer) null.

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

Unsafe Superpowers

برای تغییر به Unsafe Rust، از کلیدواژه unsafe استفاده کنید و سپس یک بلوک جدید که کد ناامن را نگه می‌دارد شروع کنید. در Unsafe Rust می‌توانید پنج عمل را انجام دهید که در راست امن نمی‌توانید، و ما این‌ها را قدرت‌های فوق‌العاده ناامن می‌نامیم. این قدرت‌ها شامل توانایی‌های زیر هستند:

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

مهم است که بفهمید unsafe سیستم borrow checker یا سایر بررسی‌های ایمنی راست را خاموش نمی‌کند: اگر از یک reference در کد ناامن استفاده کنید، همچنان بررسی خواهد شد. کلیدواژه unsafe فقط به شما دسترسی به این پنج ویژگی می‌دهد که سپس توسط کامپایلر برای ایمنی حافظه بررسی نمی‌شوند. شما همچنان درجه‌ای از ایمنی را در داخل یک بلوک ناامن خواهید داشت.

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

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

برای ایزوله کردن کد ناامن تا حد ممکن، بهتر است کد ناامن را درون یک انتزاع امن قرار دهید و یک API امن ارائه دهید، که در ادامه فصل وقتی توابع و متدهای ناامن را بررسی می‌کنیم، در این مورد بحث خواهیم کرد. بخش‌هایی از کتابخانه استاندارد به‌عنوان انتزاعات امن روی کد ناامن که مورد بازبینی قرار گرفته‌اند پیاده‌سازی شده‌اند. محصور کردن کد ناامن در یک انتزاع امن از نشت استفاده‌های unsafe به تمام مکان‌هایی که شما یا کاربران‌تان ممکن است بخواهند از قابلیت‌هایی که با کد ناامن پیاده‌سازی شده‌اند استفاده کنند، جلوگیری می‌کند، زیرا استفاده از یک انتزاع امن، امن است.

بیایید به هر یک از پنج قدرت فوق‌العاده ناامن به‌نوبت نگاه کنیم. همچنین به برخی از انتزاعات که یک رابط امن برای کد ناامن فراهم می‌کنند نگاهی خواهیم انداخت.

Dereferencing a Raw Pointer

در فصل 4، در بخش “Dangling References”، اشاره کردیم که کامپایلر تضمین می‌کند که ارجاعات همیشه معتبر هستند. Unsafe Rust دو نوع جدید به نام اشاره‌گر (Pointer)های خام (raw pointers) دارد که مشابه ارجاعات هستند. مانند ارجاعات، اشاره‌گر (Pointer)های خام می‌توانند immutable یا mutable باشند و به‌ترتیب به‌شکل *const T و *mut T نوشته می‌شوند. ستاره (*) عملگر dereference نیست؛ بلکه بخشی از نام نوع است. در زمینه اشاره‌گر (Pointer)های خام، immutable به این معناست که اشاره‌گر (Pointer) نمی‌تواند پس از dereference مستقیماً مقداردهی شود.

در مقایسه با ارجاعات و اشاره‌گر های هوشمند (smart pointers)، اشاره‌گر (Pointer)های خام:

  • مجاز به نادیده گرفتن قوانین borrowing هستند، به این صورت که می‌توانند هم اشاره‌گر (Pointer)های immutable و هم اشاره‌گر (Pointer)های mutable به همان مکان داشته باشند.
  • تضمینی برای اشاره به حافظه معتبر ندارند.
  • می‌توانند null باشند.
  • هیچ پاکسازی خودکاری را پیاده‌سازی نمی‌کنند.

با صرف‌نظر از تضمین‌های اجباری راست، می‌توانید ایمنی تضمین‌شده را با عملکرد بهتر یا توانایی ارتباط با یک زبان یا سخت‌افزار دیگر که تضمین‌های راست در آن‌ها اعمال نمی‌شود، مبادله کنید.

فهرست 20-1 نشان می‌دهد که چگونه یک اشاره‌گر (Pointer) خام immutable و یک اشاره‌گر (Pointer) خام mutable ایجاد کنیم.

fn main() {
    let mut num = 5;

    let r1 = &raw const num;
    let r2 = &raw mut num;
}
Listing 20-1: ایجاد اشاره‌گر (Pointer)های خام با عملگرهای raw borrow

توجه داشته باشید که ما در این کد از کلیدواژه unsafe استفاده نکرده‌ایم. می‌توانیم اشاره‌گر (Pointer)های خام را در کد امن ایجاد کنیم؛ فقط نمی‌توانیم خارج از یک بلوک unsafe اشاره‌گر (Pointer)های خام را dereference کنیم، همان‌طور که در ادامه خواهید دید.

ما اشاره‌گر (Pointer)های خام را با استفاده از عملگرهای raw borrow ایجاد کرده‌ایم: &raw const num یک اشاره‌گر (Pointer) خام immutable از نوع *const i32 ایجاد می‌کند، و &raw mut num یک اشاره‌گر (Pointer) خام mutable از نوع *mut i32 ایجاد می‌کند. چون آن‌ها را مستقیماً از یک متغیر محلی ایجاد کرده‌ایم، می‌دانیم که این اشاره‌گر (Pointer)های خام خاص معتبر هستند، اما نمی‌توانیم این فرض را برای هر اشاره‌گر (Pointer) خامی داشته باشیم.

برای نشان دادن این موضوع، در ادامه یک اشاره‌گر (Pointer) خام ایجاد می‌کنیم که نمی‌توانیم به‌طور قطع از اعتبار آن مطمئن باشیم، با استفاده از as برای تبدیل یک مقدار به‌جای استفاده از عملگرهای raw reference. فهرست 20-2 نشان می‌دهد که چگونه یک اشاره‌گر (Pointer) خام به یک مکان دلخواه در حافظه ایجاد کنیم. تلاش برای استفاده از حافظه دلخواه تعریف‌نشده است: ممکن است داده‌ای در آن آدرس باشد یا نباشد، کامپایلر ممکن است کد را بهینه‌سازی کند تا هیچ دسترسی حافظه‌ای وجود نداشته باشد، یا برنامه ممکن است با یک خطای segmentation fault مواجه شود. معمولاً دلیل خوبی برای نوشتن کدی مانند این وجود ندارد، به‌ویژه در مواردی که می‌توانید از عملگر raw borrow استفاده کنید، اما این کار امکان‌پذیر است.

fn main() {
    let address = 0x012345usize;
    let r = address as *const i32;
}
Listing 20-2: ایجاد یک اشاره‌گر (Pointer) خام به یک آدرس حافظه دلخواه

به یاد داشته باشید که می‌توانیم اشاره‌گر (Pointer)های خام را در کد امن ایجاد کنیم، اما نمی‌توانیم اشاره‌گر (Pointer)های خام را dereference کنیم و داده‌ای که به آن اشاره شده را بخوانیم. در فهرست 20-3، ما از عملگر dereference (*) روی یک اشاره‌گر (Pointer) خام استفاده می‌کنیم که به یک بلوک unsafe نیاز دارد.

fn main() {
    let mut num = 5;

    let r1 = &raw const num;
    let r2 = &raw mut num;

    unsafe {
        println!("r1 is: {}", *r1);
        println!("r2 is: {}", *r2);
    }
}
Listing 20-3: Dereferencing اشاره‌گر (Pointer)های خام درون یک بلوک unsafe

ایجاد یک اشاره‌گر (Pointer) آسیبی نمی‌رساند؛ فقط وقتی سعی می‌کنیم به مقداری که به آن اشاره می‌کند دسترسی پیدا کنیم ممکن است با یک مقدار نامعتبر سر و کار داشته باشیم.

همچنین توجه داشته باشید که در فهرست 20-1 و 20-3، ما اشاره‌گر (Pointer)های خام *const i32 و *mut i32 ایجاد کردیم که هر دو به همان مکان حافظه که num در آن ذخیره شده بود اشاره می‌کردند. اگر به‌جای این کار، سعی می‌کردیم یک ارجاع immutable و یک ارجاع mutable به num ایجاد کنیم، کد کامپایل نمی‌شد، زیرا قوانین مالکیت راست اجازه نمی‌دهند که یک ارجاع mutable همزمان با هر ارجاع immutable دیگری وجود داشته باشد. با اشاره‌گر (Pointer)های خام، می‌توانیم یک اشاره‌گر (Pointer) mutable و یک اشاره‌گر (Pointer) immutable به همان مکان ایجاد کنیم و داده‌ها را از طریق اشاره‌گر (Pointer) mutable تغییر دهیم، که ممکن است یک data race ایجاد کند. مراقب باشید!

با وجود تمام این خطرات، چرا باید از اشاره‌گر (Pointer)های خام استفاده کنید؟ یکی از موارد استفاده اصلی هنگام تعامل با کد C است، همان‌طور که در بخش بعدی “Calling an Unsafe Function or Method.” خواهید دید. مورد دیگر زمانی است که انتزاعات امنی ایجاد می‌کنید که سیستم borrow checker آن را نمی‌فهمد. ابتدا توابع ناامن را معرفی می‌کنیم و سپس به یک مثال از یک انتزاع امن که از کد ناامن استفاده می‌کند، می‌پردازیم.

Calling an Unsafe Function or Method

دومین نوع عملیاتی که می‌توانید در یک بلوک ناامن انجام دهید، فراخوانی توابع ناامن است. توابع و متدهای ناامن دقیقاً شبیه توابع و متدهای عادی به نظر می‌رسند، اما قبل از بقیه تعریف یک unsafe اضافه دارند. کلیدواژه unsafe در این زمینه نشان می‌دهد که تابع دارای الزاماتی است که هنگام فراخوانی این تابع باید رعایت کنیم، زیرا راست نمی‌تواند تضمین کند که این الزامات را رعایت کرده‌ایم. با فراخوانی یک تابع ناامن در یک بلوک unsafe، ما می‌گوییم که مستندات این تابع را خوانده‌ایم و مسئولیت رعایت قراردادهای تابع را بر عهده می‌گیریم.

در اینجا یک تابع ناامن به نام dangerous آورده شده است که در بدنه خود کاری انجام نمی‌دهد:

fn main() {
    unsafe fn dangerous() {}

    unsafe {
        dangerous();
    }
}

ما باید تابع dangerous را در یک بلوک unsafe جداگانه فراخوانی کنیم. اگر سعی کنیم بدون بلوک unsafe تابع dangerous را فراخوانی کنیم، با خطا مواجه خواهیم شد:

$ cargo run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0133]: call to unsafe function `dangerous` is unsafe and requires unsafe function or block
 --> src/main.rs:4:5
  |
4 |     dangerous();
  |     ^^^^^^^^^^^ call to unsafe function
  |
  = note: consult the function's documentation for information on how to avoid undefined behavior

For more information about this error, try `rustc --explain E0133`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error

با استفاده از بلوک unsafe، ما به راست اعلام می‌کنیم که مستندات تابع را خوانده‌ایم، می‌دانیم چگونه به‌درستی از آن استفاده کنیم، و تأیید کرده‌ایم که قرارداد تابع را رعایت می‌کنیم.

برای انجام عملیات ناایمن (unsafe) در بدنه یک تابع ناایمن، همچنان باید از یک بلوک unsafe استفاده کنید، همان‌طور که در یک تابع معمولی این کار را می‌کنید، و اگر این کار را فراموش کنید، کامپایلر به شما هشدار خواهد داد. این امر به کوچک نگه داشتن بلوک‌های unsafe کمک می‌کند، زیرا ممکن است عملیات ناایمن در کل بدنه تابع مورد نیاز نباشد.

Creating a Safe Abstraction over Unsafe Code

فقط به این دلیل که یک تابع حاوی کد ناامن است به این معنا نیست که باید کل تابع را به‌عنوان ناامن علامت‌گذاری کنیم. در واقع، محصور کردن کد ناامن در یک تابع ایمن یک انتزاع رایج است. به‌عنوان مثال، بیایید تابع split_at_mut از کتابخانه استاندارد را بررسی کنیم که به کد ناامن نیاز دارد. ما بررسی خواهیم کرد که چگونه ممکن است آن را پیاده‌سازی کنیم. این متد ایمن روی برش‌های قابل تغییر (mutable slices) تعریف شده است: این تابع یک برش را می‌گیرد و آن را به دو قسمت تقسیم می‌کند با تقسیم کردن برش در ایندکسی که به‌عنوان آرگومان داده شده است. فهرست 20-4 نشان می‌دهد که چگونه از split_at_mut استفاده کنیم.

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

    let r = &mut v[..];

    let (a, b) = r.split_at_mut(3);

    assert_eq!(a, &mut [1, 2, 3]);
    assert_eq!(b, &mut [4, 5, 6]);
}
Listing 20-4: استفاده از تابع ایمن split_at_mut

ما نمی‌توانیم این تابع را فقط با استفاده از راست ایمن پیاده‌سازی کنیم. یک تلاش ممکن است چیزی شبیه به فهرست 20-5 باشد، که کامپایل نخواهد شد. برای سادگی، ما split_at_mut را به‌عنوان یک تابع پیاده‌سازی می‌کنیم نه یک متد، و فقط برای برش‌های i32 به‌جای یک نوع generic T.

fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();

    assert!(mid <= len);

    (&mut values[..mid], &mut values[mid..])
}

fn main() {
    let mut vector = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mut(&mut vector, 3);
}
Listing 20-5: تلاش برای پیاده‌سازی split_at_mut فقط با استفاده از راست ایمن

این تابع ابتدا طول کل برش را به دست می‌آورد. سپس تأیید می‌کند که ایندکسی که به‌عنوان پارامتر داده شده در محدوده برش قرار دارد، با بررسی اینکه آیا کمتر از یا برابر طول است. این تأیید به این معناست که اگر ایندکسی بزرگ‌تر از طول برای تقسیم برش داده شود، تابع قبل از تلاش برای استفاده از آن ایندکس دچار panic خواهد شد.

سپس دو برش قابل تغییر را در یک tuple بازمی‌گردانیم: یکی از ابتدای برش اصلی تا ایندکس mid و دیگری از mid تا انتهای برش.

وقتی سعی می‌کنیم کد در فهرست 20-5 را کامپایل کنیم، با خطا مواجه خواهیم شد.

$ cargo run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0499]: cannot borrow `*values` as mutable more than once at a time
 --> src/main.rs:6:31
  |
1 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
  |                         - let's call the lifetime of this reference `'1`
...
6 |     (&mut values[..mid], &mut values[mid..])
  |     --------------------------^^^^^^--------
  |     |     |                   |
  |     |     |                   second mutable borrow occurs here
  |     |     first mutable borrow occurs here
  |     returning this value requires that `*values` is borrowed for `'1`
  |
  = help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices

For more information about this error, try `rustc --explain E0499`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error

Rust’s borrow checker نمی‌تواند بفهمد که ما در حال قرض گرفتن قسمت‌های مختلفی از یک برش هستیم؛ تنها چیزی که می‌داند این است که ما دو بار از همان برش قرض گرفته‌ایم. قرض گرفتن قسمت‌های مختلف یک برش اصولاً اشکالی ندارد، زیرا این دو برش با یکدیگر هم‌پوشانی ندارند، اما Rust به‌اندازه کافی هوشمند نیست که این موضوع را بداند. وقتی می‌دانیم کد مشکلی ندارد، اما Rust نمی‌داند، زمان استفاده از کد ناامن فرا می‌رسد.

فهرست 20-6 نشان می‌دهد که چگونه از یک بلوک unsafe، یک اشاره‌گر (Pointer) خام، و چند فراخوانی به توابع ناامن برای اجرای تابع split_at_mut استفاده کنیم.

use std::slice;

fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();
    let ptr = values.as_mut_ptr();

    assert!(mid <= len);

    unsafe {
        (
            slice::from_raw_parts_mut(ptr, mid),
            slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

fn main() {
    let mut vector = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mut(&mut vector, 3);
}
Listing 20-6: استفاده از کد ناامن در پیاده‌سازی تابع split_at_mut

به یاد بیاورید از بخش “The Slice Type” در فصل 4 که برش‌ها یک اشاره‌گر (Pointer) به برخی داده‌ها و طول آن برش هستند. ما از متد len برای دریافت طول یک برش و از متد as_mut_ptr برای دسترسی به اشاره‌گر (Pointer) خام یک برش استفاده می‌کنیم. در این مورد، چون ما یک برش قابل تغییر به مقادیر i32 داریم، as_mut_ptr یک اشاره‌گر (Pointer) خام با نوع *mut i32 بازمی‌گرداند که آن را در متغیر ptr ذخیره کرده‌ایم.

ما تأیید می‌کنیم که ایندکس mid در محدوده برش است. سپس به کد ناامن می‌رسیم: تابع slice::from_raw_parts_mut یک اشاره‌گر (Pointer) خام و یک طول را می‌گیرد و یک برش ایجاد می‌کند. ما از این تابع برای ایجاد یک برش که از ptr شروع می‌شود و mid آیتم طول دارد استفاده می‌کنیم. سپس متد add را روی ptr با آرگومان mid فراخوانی می‌کنیم تا یک اشاره‌گر (Pointer) خام که از mid شروع می‌شود دریافت کنیم، و با استفاده از آن اشاره‌گر (Pointer) و تعداد آیتم‌های باقی‌مانده بعد از mid به‌عنوان طول، یک برش ایجاد می‌کنیم.

تابع slice::from_raw_parts_mut ناامن است زیرا یک اشاره‌گر (Pointer) خام می‌گیرد و باید اعتماد کند که این اشاره‌گر (Pointer) معتبر است. متد add روی اشاره‌گر (Pointer)های خام نیز ناامن است، زیرا باید اعتماد کند که موقعیت آفست نیز یک اشاره‌گر (Pointer) معتبر است. بنابراین، ما مجبور شدیم یک بلوک unsafe در اطراف فراخوانی‌های خود به slice::from_raw_parts_mut و add قرار دهیم تا بتوانیم آن‌ها را فراخوانی کنیم. با نگاه به کد و با افزودن تأییدیه‌ای که mid باید کمتر از یا برابر با len باشد، می‌توانیم بگوییم که تمام اشاره‌گر (Pointer)های خام استفاده‌شده در بلوک unsafe اشاره‌گر (Pointer)های معتبری به داده‌های درون برش خواهند بود. این یک استفاده قابل‌قبول و مناسب از unsafe است.

توجه داشته باشید که نیازی به علامت‌گذاری تابع split_at_mut به‌عنوان unsafe نداریم و می‌توانیم این تابع را از کد امن Rust فراخوانی کنیم. ما یک انتزاع امن برای کد ناامن با پیاده‌سازی تابعی که از کد ناامن به روش ایمن استفاده می‌کند ایجاد کرده‌ایم، زیرا فقط اشاره‌گر (Pointer)های معتبری از داده‌هایی که این تابع به آن‌ها دسترسی دارد ایجاد می‌کند.

در مقابل، استفاده از slice::from_raw_parts_mut در فهرست 20-7 احتمالاً هنگام استفاده از برش باعث کرش کردن می‌شود. این کد یک مکان حافظه دلخواه می‌گیرد و یک برش با طول 10,000 آیتم ایجاد می‌کند.

fn main() {
    use std::slice;

    let address = 0x01234usize;
    let r = address as *mut i32;

    let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
}
Listing 20-7: ایجاد یک برش از یک مکان حافظه دلخواه

ما مالک حافظه در این مکان دلخواه نیستیم و هیچ تضمینی وجود ندارد که برشی که این کد ایجاد می‌کند حاوی مقادیر معتبر i32 باشد. تلاش برای استفاده از values به‌عنوان اینکه یک برش معتبر است منجر به رفتار تعریف‌نشده می‌شود.

Using extern Functions to Call External Code

گاهی اوقات، کد Rust شما ممکن است نیاز به تعامل با کدی که به زبان دیگری نوشته شده دارد. برای این منظور، راست کلیدواژه extern را ارائه می‌دهد که امکان ایجاد و استفاده از یک رابط تابع خارجی (FFI) را فراهم می‌کند. یک FFI راهی است برای یک زبان برنامه‌نویسی برای تعریف توابع و امکان فراخوانی آن توابع توسط یک زبان برنامه‌نویسی دیگر (خارجی).

فهرست 20-8 نشان می‌دهد که چگونه یک یکپارچه‌سازی با تابع abs از کتابخانه استاندارد C تنظیم کنیم. توابعی که درون بلوک‌های extern اعلام می‌شوند معمولاً از کد راست ناامن برای فراخوانی استفاده می‌شوند، بنابراین باید با unsafe نیز علامت‌گذاری شوند. دلیل این است که زبان‌های دیگر قوانین و تضمین‌های راست را اعمال نمی‌کنند، و راست نمی‌تواند آن‌ها را بررسی کند، بنابراین مسئولیت بر عهده برنامه‌نویس است که ایمنی را تضمین کند.

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

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}
Listing 20-8: اعلام و فراخوانی یک تابع extern تعریف‌شده در زبان دیگر

درون بلوک unsafe extern "C"، ما نام‌ها و امضاهای توابع خارجی از یک زبان دیگر که می‌خواهیم فراخوانی کنیم را فهرست می‌کنیم. بخش "C" مشخص می‌کند که کدام رابط دودویی برنامه (ABI) توسط تابع خارجی استفاده می‌شود: ABI تعریف می‌کند که چگونه تابع در سطح اسمبلی فراخوانی شود. ABI "C" رایج‌ترین است و از ABI زبان برنامه‌نویسی C پیروی می‌کند.

این تابع خاص هیچ ملاحظات ایمنی حافظه‌ای ندارد. در واقع، ما می‌دانیم که هر فراخوانی به abs همیشه برای هر i32 ایمن خواهد بود، بنابراین می‌توانیم از کلیدواژه safe استفاده کنیم تا بگوییم که این تابع خاص حتی با وجود اینکه در یک بلوک unsafe extern است، ایمن است. هنگامی که این تغییر را اعمال کنیم، فراخوانی آن دیگر نیاز به یک بلوک unsafe ندارد، همان‌طور که در فهرست 20-9 نشان داده شده است.

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

fn main() {
    println!("Absolute value of -3 according to C: {}", abs(-3));
}
Listing 20-9: علامت‌گذاری صریح یک تابع به‌عنوان safe درون یک بلوک unsafe extern و فراخوانی ایمن آن

علامت‌گذاری یک تابع به‌عنوان safe ذاتاً آن را ایمن نمی‌کند! در عوض، این مانند یک وعده‌ای است که شما به راست می‌دهید که ایمن است. همچنان مسئولیت شماست که اطمینان حاصل کنید این وعده رعایت شود!

Calling Rust Functions from Other Languages

ما همچنین می‌توانیم از extern برای ایجاد یک رابط استفاده کنیم که به زبان‌های دیگر اجازه دهد توابع راست را فراخوانی کنند. به جای ایجاد یک بلوک extern کامل، ما کلیدواژه extern را اضافه می‌کنیم و ABI مورد استفاده را درست قبل از کلیدواژه fn برای تابع مربوطه مشخص می‌کنیم. همچنین باید یک حاشیه‌نویسی #[unsafe(no_mangle)] اضافه کنیم تا به کامپایلر راست بگوییم نام این تابع را تغییر ندهد. Mangling زمانی است که یک کامپایلر نامی را که به یک تابع داده‌ایم به نامی متفاوت تغییر می‌دهد که حاوی اطلاعات بیشتری برای سایر بخش‌های فرآیند کامپایل باشد اما کمتر قابل خواندن برای انسان باشد. هر کامپایلر زبان برنامه‌نویسی نام‌ها را کمی متفاوت mangling می‌کند، بنابراین برای اینکه یک تابع راست توسط زبان‌های دیگر قابل نام‌گذاری باشد، باید mangling نام کامپایلر راست را غیرفعال کنیم. این ناامن است زیرا ممکن است در میان کتابخانه‌ها تضاد نام رخ دهد بدون mangling داخلی، بنابراین مسئولیت ماست که اطمینان حاصل کنیم نامی که صادر کرده‌ایم برای صدور بدون mangling ایمن است.

در مثال زیر، ما تابع call_from_c را برای کد C در دسترس قرار می‌دهیم، پس از اینکه به یک کتابخانه مشترک کامپایل و از C لینک شد:

#![allow(unused)]
fn main() {
#[unsafe(no_mangle)]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}
}

این استفاده از extern نیازی به unsafe ندارد.

Accessing or Modifying a Mutable Static Variable

در این کتاب، هنوز در مورد متغیرهای جهانی صحبت نکرده‌ایم، که راست از آن‌ها پشتیبانی می‌کند اما ممکن است با قوانین مالکیت راست مشکل‌ساز شوند. اگر دو thread به یک متغیر جهانی قابل تغییر دسترسی داشته باشند، ممکن است یک data race ایجاد شود.

در راست، متغیرهای جهانی static نامیده می‌شوند. فهرست 20-10 یک مثال از اعلام و استفاده از یک متغیر static با یک string slice به‌عنوان مقدار را نشان می‌دهد.

Filename: src/main.rs
static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("name is: {HELLO_WORLD}");
}
Listing 20-10: تعریف و استفاده از یک متغیر static غیرقابل تغییر

متغیرهای static مشابه ثابت‌ها هستند، که در بخش “Constants” در فصل 3 در مورد آن‌ها صحبت کردیم. نام متغیرهای static طبق قرارداد به‌صورت SCREAMING_SNAKE_CASE نوشته می‌شود. متغیرهای static فقط می‌توانند ارجاع‌هایی با lifetime 'static ذخیره کنند، به این معنا که کامپایلر راست می‌تواند lifetime را مشخص کند و نیازی نیست که آن را صراحتاً حاشیه‌نویسی کنیم. دسترسی به یک متغیر static غیرقابل تغییر ایمن است.

یک تفاوت ظریف بین ثابت‌ها و متغیرهای static غیرقابل تغییر این است که مقادیر در یک متغیر static دارای یک آدرس ثابت در حافظه هستند. استفاده از مقدار همیشه به همان داده دسترسی خواهد داشت. از سوی دیگر، ثابت‌ها مجاز هستند داده‌های خود را هر زمان که استفاده می‌شوند تکرار کنند. تفاوت دیگر این است که متغیرهای static می‌توانند قابل تغییر باشند. دسترسی و تغییر متغیرهای static قابل تغییر ناامن است. فهرست 20-11 نشان می‌دهد که چگونه یک متغیر static قابل تغییر به نام COUNTER را اعلام، دسترسی و تغییر دهیم.

Filename: src/main.rs
static mut COUNTER: u32 = 0;

/// SAFETY: Calling this from more than a single thread at a time is undefined
/// behavior, so you *must* guarantee you only call it from a single thread at
/// a time.
unsafe fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    unsafe {
        // SAFETY: This is only called from a single thread in `main`.
        add_to_count(3);
        println!("COUNTER: {}", *(&raw const COUNTER));
    }
}
Listing 20-11: خواندن از یا نوشتن به یک متغیر static قابل تغییر ناامن است

همانند متغیرهای معمولی، ما با استفاده از کلمه کلیدی mut قابلیت تغییرپذیری را مشخص می‌کنیم. هر کدی که بخواهد از COUNTER بخواند یا در آن بنویسد، باید در یک بلوک unsafe باشد. کدی که در لیست ۲۰-۱۱ نشان داده شده است کامپایل می‌شود و مقدار COUNTER: 3 را همان‌طور که انتظار می‌رود چاپ می‌کند، زیرا این کد تک‌ریسمانی (single-threaded) است. دسترسی چندین ریسمان به COUNTER به احتمال زیاد منجر به رقابت داده‌ای (data race) می‌شود و این رفتار تعریف‌نشده (undefined behavior) خواهد بود. بنابراین، نیاز است کل تابع را به عنوان unsafe علامت‌گذاری کنیم و محدودیت ایمنی را مستند کنیم، تا هرکسی که تابع را فراخوانی می‌کند بداند چه کارهایی را می‌تواند با اطمینان انجام دهد و چه کارهایی را نمی‌تواند.

هر زمان که یک تابع ناامن می‌نویسیم، به صورت قراردادی کامنتی با SAFETY شروع می‌کنیم و توضیح می‌دهیم که فراخوانی تابع چه چیزی نیاز دارد تا ایمن باشد. به همین ترتیب، هر زمان که یک عملیات ناامن انجام می‌دهیم، نوشتن یک کامنت که با SAFETY شروع شود برای توضیح اینکه چگونه قوانین ایمنی رعایت می‌شوند، قراردادی است.

علاوه بر این، کامپایلر به شما اجازه نمی‌دهد که مراجع به یک متغیر استاتیک تغییرپذیر ایجاد کنید. تنها می‌توانید از طریق یک اشاره‌گر خام (raw pointer) که با یکی از عملگرهای قرض خام ایجاد می‌شود به آن دسترسی پیدا کنید. این شامل مواردی است که مرجع به صورت نامرئی ایجاد می‌شود، مانند زمانی که در println! در این لیست کد استفاده می‌شود. الزام اینکه مراجع به متغیرهای استاتیک تغییرپذیر فقط از طریق اشاره‌گرهای خام ایجاد شوند، به وضوح بیشتر نیازهای ایمنی در استفاده از آن‌ها کمک می‌کند.

با داده‌های تغییرپذیری که به صورت جهانی قابل دسترسی هستند، اطمینان از اینکه رقابت داده‌ای (data race) رخ نمی‌دهد دشوار است، به همین دلیل Rust متغیرهای استاتیک تغییرپذیر را ناایمن در نظر می‌گیرد. در صورت امکان، ترجیح داده می‌شود از تکنیک‌های همزمانی (concurrency techniques) و اشاره‌گرهای هوشمند ایمن برای ریسمان‌ها (thread-safe smart pointers) که در فصل ۱۶ مورد بحث قرار گرفتند استفاده کنید تا کامپایلر بررسی کند که دسترسی به داده‌ها از ریسمان‌های مختلف به صورت ایمن انجام می‌شود.

Implementing an Unsafe Trait

می‌توانیم از unsafe برای پیاده‌سازی یک trait ناامن استفاده کنیم. یک trait زمانی ناامن است که حداقل یکی از متدهای آن دارای یک قاعده (invariant) باشد که کامپایلر نمی‌تواند آن را تأیید کند. ما با افزودن کلیدواژه unsafe قبل از trait اعلام می‌کنیم که یک trait ناامن است و پیاده‌سازی آن trait را نیز به‌عنوان unsafe علامت‌گذاری می‌کنیم، همان‌طور که در فهرست 20-12 نشان داده شده است.

unsafe trait Foo {
    // methods go here
}

unsafe impl Foo for i32 {
    // method implementations go here
}

fn main() {}
Listing 20-12: تعریف و پیاده‌سازی یک trait ناامن

با استفاده از unsafe impl، ما قول می‌دهیم که قاعده‌هایی را که کامپایلر نمی‌تواند تأیید کند، رعایت کنیم.

به‌عنوان مثال، به marker traitهای Sync و Send که در بخش “Extensible Concurrency with the Sync and Send Traits” در فصل 16 بررسی کردیم، بازگردید: کامپایلر این traitها را به‌صورت خودکار پیاده‌سازی می‌کند اگر نوع‌های ما به‌طور کامل از نوع‌های Send و Sync تشکیل شده باشند. اگر نوعی پیاده‌سازی کنیم که حاوی نوعی است که Send یا Sync نیست، مانند اشاره‌گر (Pointer)های خام، و بخواهیم آن نوع را به‌عنوان Send یا Sync علامت‌گذاری کنیم، باید از unsafe استفاده کنیم. راست نمی‌تواند تأیید کند که نوع ما تضمین‌های لازم برای ارسال ایمن بین ریسمان‌ها یا دسترسی ایمن از ریسمان‌های متعدد را رعایت می‌کند؛ بنابراین، ما باید این بررسی‌ها را به‌صورت دستی انجام دهیم و این را با unsafe نشان دهیم.

Accessing Fields of a Union

آخرین عملی که تنها با unsafe کار می‌کند، دسترسی به فیلدهای یک union است. یک union شبیه به یک struct است، اما تنها یکی از فیلدهای اعلام‌شده در یک نمونه در هر زمان خاص استفاده می‌شود. unions عمدتاً برای تعامل با unions در کد C استفاده می‌شوند. دسترسی به فیلدهای union ناامن است زیرا راست نمی‌تواند نوع داده‌ای که در حال حاضر در نمونه union ذخیره شده را تضمین کند. می‌توانید اطلاعات بیشتری درباره unions در مرجع راست بیاموزید.

Using Miri to check unsafe code

هنگام نوشتن کد ناامن، ممکن است بخواهید بررسی کنید که چیزی که نوشته‌اید واقعاً ایمن و درست است. یکی از بهترین روش‌ها برای این کار استفاده از Miri، یک ابزار رسمی راست برای شناسایی رفتارهای تعریف‌نشده است. در حالی که borrow checker یک ابزار استاتیک است که در زمان کامپایل کار می‌کند، Miri یک ابزار داینامیک است که در زمان اجرا کار می‌کند. این ابزار کد شما را با اجرای برنامه یا مجموعه تست آن بررسی می‌کند و زمانی که قوانین مربوط به نحوه کار راست را نقض کنید، آن را تشخیص می‌دهد.

استفاده از Miri نیاز به یک نسخه nightly از راست دارد (که در ضمیمه ی: How Rust is Made and “Nightly Rust” بیشتر درباره آن صحبت کرده‌ایم). می‌توانید یک نسخه nightly از راست و ابزار Miri را با تایپ کردن rustup +nightly component add miri نصب کنید. این کار نسخه راست پروژه شما را تغییر نمی‌دهد؛ فقط ابزار را به سیستم شما اضافه می‌کند تا هر زمان که بخواهید از آن استفاده کنید. می‌توانید Miri را روی یک پروژه با تایپ کردن cargo +nightly miri run یا cargo +nightly miri test اجرا کنید.

برای مثالی از اینکه این ابزار چقدر می‌تواند مفید باشد، به خروجی اجرای آن روی فهرست 20-11 توجه کنید:

$ cargo +nightly miri run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
     Running `/Users/chris/.rustup/toolchains/nightly-aarch64-apple-darwin/bin/cargo-miri runner target/miri/aarch64-apple-darwin/debug/unsafe-example`
warning: creating a shared reference to mutable static is discouraged
  --> src/main.rs:14:33
   |
14 |         println!("COUNTER: {}", COUNTER);
   |                                 ^^^^^^^ shared reference to mutable static
   |
   = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/static-mut-references.html>
   = note: shared references to mutable statics are dangerous; it's undefined behavior if the static is mutated or if a mutable reference is created for it while the shared reference lives
   = note: `#[warn(static_mut_refs)]` on by default

COUNTER: 3

این ابزار به‌درستی متوجه می‌شود که ما به داده‌های قابل تغییر ارجاعات مشترک داده‌ایم و در این مورد هشدار می‌دهد. در این مورد، ابزار به ما نمی‌گوید که چگونه مشکل را برطرف کنیم، اما به ما اطلاع می‌دهد که ممکن است یک مشکل وجود داشته باشد و می‌توانیم به این فکر کنیم که چگونه مطمئن شویم که ایمن است. در موارد دیگر، ممکن است به ما بگوید که بخشی از کد قطعاً اشتباه است و توصیه‌هایی برای رفع آن ارائه دهد.

Miri همه چیزهایی را که ممکن است در هنگام نوشتن کد ناامن اشتباه باشد، شناسایی نمی‌کند. اولاً، چون این ابزار یک بررسی داینامیک است، فقط مشکلات کدی را که واقعاً اجرا می‌شود شناسایی می‌کند. این بدان معناست که باید از آن همراه با تکنیک‌های تست خوب استفاده کنید تا اطمینان بیشتری درباره کد ناامن خود داشته باشید. ثانیاً، این ابزار تمام راه‌های ممکن برای ناسالم بودن کد شما را پوشش نمی‌دهد. اگر Miri مشکلی را شناسایی کند، می‌دانید که یک باگ وجود دارد، اما فقط به این دلیل که Miri باگی را شناسایی نمی‌کند، به این معنا نیست که مشکلی وجود ندارد. با این حال، Miri می‌تواند بسیاری از مشکلات را شناسایی کند. آن را روی سایر مثال‌های کد ناامن در این فصل اجرا کنید و ببینید چه می‌گوید!

When to Use Unsafe Code

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

برای یک بررسی عمیق‌تر درباره نحوه کار مؤثر با راست ناامن، راهنمای رسمی راست در این موضوع، یعنی Rustonomicon را بخوانید.