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; }
توجه داشته باشید که ما در این کد از کلیدواژه 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; }
به یاد داشته باشید که میتوانیم اشارهگر (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); } }
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]); }
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);
}
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); }
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) }; }
ما مالک حافظه در این مکان دلخواه نیستیم و هیچ تضمینی وجود ندارد که برشی که این کد ایجاد میکند حاوی مقادیر معتبر i32
باشد. تلاش برای استفاده از values
بهعنوان اینکه یک برش معتبر است منجر به رفتار تعریفنشده میشود.
Using extern
Functions to Call External Code
گاهی اوقات، کد Rust شما ممکن است نیاز به تعامل با کدی که به زبان دیگری نوشته شده دارد. برای این منظور، راست کلیدواژه extern
را ارائه میدهد که امکان ایجاد و استفاده از یک رابط تابع خارجی (FFI) را فراهم میکند. یک FFI راهی است برای یک زبان برنامهنویسی برای تعریف توابع و امکان فراخوانی آن توابع توسط یک زبان برنامهنویسی دیگر (خارجی).
فهرست 20-8 نشان میدهد که چگونه یک یکپارچهسازی با تابع abs
از کتابخانه استاندارد C تنظیم کنیم. توابعی که درون بلوکهای extern
اعلام میشوند معمولاً از کد راست ناامن برای فراخوانی استفاده میشوند، بنابراین باید با unsafe
نیز علامتگذاری شوند. دلیل این است که زبانهای دیگر قوانین و تضمینهای راست را اعمال نمیکنند، و راست نمیتواند آنها را بررسی کند، بنابراین مسئولیت بر عهده برنامهنویس است که ایمنی را تضمین کند.
unsafe extern "C" { fn abs(input: i32) -> i32; } fn main() { unsafe { println!("Absolute value of -3 according to C: {}", abs(-3)); } }
extern
تعریفشده در زبان دیگردرون بلوک unsafe extern "C"
، ما نامها و امضاهای توابع خارجی از یک زبان دیگر که میخواهیم فراخوانی کنیم را فهرست میکنیم. بخش "C"
مشخص میکند که کدام رابط دودویی برنامه (ABI) توسط تابع خارجی استفاده میشود: ABI تعریف میکند که چگونه تابع در سطح اسمبلی فراخوانی شود. ABI "C"
رایجترین است و از ABI زبان برنامهنویسی C پیروی میکند.
این تابع خاص هیچ ملاحظات ایمنی حافظهای ندارد. در واقع، ما میدانیم که هر فراخوانی به abs
همیشه برای هر i32
ایمن خواهد بود، بنابراین میتوانیم از کلیدواژه safe
استفاده کنیم تا بگوییم که این تابع خاص حتی با وجود اینکه در یک بلوک unsafe extern
است، ایمن است. هنگامی که این تغییر را اعمال کنیم، فراخوانی آن دیگر نیاز به یک بلوک unsafe
ندارد، همانطور که در فهرست 20-9 نشان داده شده است.
unsafe extern "C" { safe fn abs(input: i32) -> i32; } fn main() { println!("Absolute value of -3 according to C: {}", abs(-3)); }
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 بهعنوان مقدار را نشان میدهد.
static HELLO_WORLD: &str = "Hello, world!"; fn main() { println!("name is: {HELLO_WORLD}"); }
متغیرهای static مشابه ثابتها هستند، که در بخش “Constants” در فصل 3 در مورد آنها صحبت کردیم. نام متغیرهای static طبق قرارداد بهصورت SCREAMING_SNAKE_CASE
نوشته میشود. متغیرهای static فقط میتوانند ارجاعهایی با lifetime 'static
ذخیره کنند، به این معنا که کامپایلر راست میتواند lifetime را مشخص کند و نیازی نیست که آن را صراحتاً حاشیهنویسی کنیم. دسترسی به یک متغیر static غیرقابل تغییر ایمن است.
یک تفاوت ظریف بین ثابتها و متغیرهای static غیرقابل تغییر این است که مقادیر در یک متغیر static دارای یک آدرس ثابت در حافظه هستند. استفاده از مقدار همیشه به همان داده دسترسی خواهد داشت. از سوی دیگر، ثابتها مجاز هستند دادههای خود را هر زمان که استفاده میشوند تکرار کنند. تفاوت دیگر این است که متغیرهای static میتوانند قابل تغییر باشند. دسترسی و تغییر متغیرهای static قابل تغییر ناامن است. فهرست 20-11 نشان میدهد که چگونه یک متغیر static قابل تغییر به نام COUNTER
را اعلام، دسترسی و تغییر دهیم.
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)); } }
همانند متغیرهای معمولی، ما با استفاده از کلمه کلیدی 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() {}
با استفاده از 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 را بخوانید.