دستور الملكية
Rust

كل شيء بنيتَه منذ الإقليم ٠٠ — Vec, String, Box, Rc, Mutex — يعيش خلف واجهة آمنة تمنع الكوارث الثلاث التي افتتحنا بها المنهج. لكن شخصًا ما كتب Vec نفسها. وذلك الشخص، في مكان ما داخل المكتبة القياسية، اضطر لكتابة كود لا يخضع للفحص الكامل — لأن بعض الحقائق البرمجية الصحيحة يعجز نظام الأنواع عن إثباتها، مهما كان ذكيًا. هذا الإقليم يهبط بك إلى ذلك المكان، ويطلب منك أن تكتب Vec بنفسك.


الدرس ١: unsafe لا تعني ما يظنّه أغلب الناس

الخرافة التي نصححها فورًا

توقّع شائع خاطئ: "unsafe تُطفئ borrow checker". جرّب هذا الآن، قبل أي شرح إضافي:

fn weird() {
    let s = String::from("hi");
    let s2 = s;
    unsafe {
        println!("{s}");   // توقع: هل يمر أم يُرفَض؟
    }
}

يُرفَض — بنفس رسالة "قيمة منقولة" من الإقليم ٠١، بلا أي تغيير. unsafe لم تلمس فحص النقل، ولا الاستعارة، ولا الأنواع، ولا الـ exhaustiveness. كل ما بنيتَه من الإقليم ٠٠ حتى ٠٨ يبقى فعّالًا بالكامل داخل كتلة unsafe. الكلمة لا تفتح إلا خمس قدرات محدَّدة بدقة، لا أكثر:

القدرةما تفتحه
١إلغاء إشارة مؤشر خام (*const T / *mut T)
٢نداء دالة أو طريقة unsafe fn
٣قراءة/تعديل متغيّر static mut
٤تطبيق trait معلَّم بـ unsafe
٥الوصول لحقول union

هذا كل شيء. لا سادس. أي كود آخر داخل الكتلة يبقى صريحًا خاضعًا للفحص الكامل. unsafe ليست "أطفئ الأمان" — إنها "انقل عبء الإثبات من المترجم إليّ، لهذه العملية المحدَّدة بالذات فقط."

لماذا يحتاج نظام سليم (sound) أصلًا لبوّابة كهذه؟

نظام الأنواع في Rust سليم لكن غير مكتمل (sound but not complete): كل ما يقبله المترجم آمن فعلًا (السلامة)، لكن ليس كل ما هو آمن فعلًا يقبله المترجم (النقص) — لأن المترجم يحلّل بقواعد محافِظة عامة، بينما أنت أحيانًا تملك معرفة أدقّ لا تستطيع صياغتها كقاعدة عامة. مثال حقيقي، يعيش فعليًا داخل مصدر std:

fn split_at_mut(s: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    (&mut s[..mid], &mut s[mid..])   // جرّبها — سترفضها
}

جرّبها فعلًا. المترجم يرفض: استعارتان حصريتان لـ s في آنٍ واحد — خرق ظاهري للبديهية الثانية. لكن أنت تعرف يقينًا أن s[..mid] وs[mid..] لا تتقاطعان في الذاكرة إطلاقًا — بديهي من تعريف التقسيم عند mid. المترجم لا "يرى" هذا؛ تحليله يعمل على مستوى الاستعارات النحوية (اسم واحد s، استعارتان منه) لا على مستوى القيم العددية الفعلية للفهارس. هذه بالضبط الفجوة التي unsafe تسدّها: تكتب الدالة بمؤشرات خام، تُثبت لنفسك (وللقارئ، عبر تعليق) أن التقاطع مستحيل رياضيًا، والمترجم يثق بك لهذا الموضع المحدود وحده.

اصطلاح مهني لا تتجاوزه: كل كتلة unsafe تستحق تعليق // SAFETY: فوقها يشرح لماذا الفعل آمن فعليًا رغم أن المترجم لا يستطيع إثباته — موجَّه للقارئ البشري (وأحيانًا لك أنت بعد ستة أشهر). هذا ليس شكليات؛ clippy نفسه يفرضه في إعدادات صارمة.


الدرس ٢: المؤشرات الخام — C، لكن بأسماء تصرخ "احذر"

let x = 5;
let r1 = &x as *const i32;        // إنشاء مؤشر خام: آمن! لا unsafe هنا
let mut y = 10;
let r2 = &mut y as *mut i32;      // آمن أيضًا

unsafe {
    println!("{}", *r1);          // الإلغاء فقط يحتاج unsafe
    *r2 += 1;
}

لاحظ التقسيم المتعمَّد: إنشاء مؤشر خام عملية آمنة (لا تفعل شيئًا خطرًا بعد — مجرد عنوان)؛ إلغاء إشارته هو اللحظة الفعلية التي قد تقرأ ذاكرة غير صالحة، فهي وحدها المحصورة خلف unsafe. هذا تصميم دقيق: الخطر محدَّد بأصغر نقطة ممكنة، لا مُعمَّم على كل ما "يلمس" المؤشر.

الفروقات عن &T/&mut T التي عشتَ بها منذ الإقليم ٠٢:

&T / &mut T*const T / *mut T
قد تكون فارغة (null)؟لا، أبدًانعم
قد تتدلّى (dangling)؟لا — المترجم يمنع هذانعم — مسؤوليتك بالكامل
قواعد الإعارة (البديهية ٢)مفروضة دائمًاغير مفروضة إطلاقًا — يمكن وجود عدة *mut T لنفس الموضع بلا شكوى
عمر ('a) متتبَّعنعملا

لاحظ: السماح بعدة *mut T لنفس الموضع ليس خرقًا للبديهية الثانية — لأن unsafe هي حرفيًا الإعلان: "من الآن، أنا من يحرس هذه البديهية يدويًا لهذه المنطقة، لا المترجم." نفس الانضباط الذي كان في رأسك طوال كتابة C — عاد الآن، لكنه محصور ومُسمّى ومكتوب صراحة، لا افتراضًا صامتًا يخيّم على كل سطر كما كان الحال هناك.

std::ptr::NonNull<T>: غلاف حول *mut T يَعِد بأنه لن يكون أبدًا null — تستعمله المكتبة القياسية داخليًا في Box, Vec, Rc لأن أغلب الكود بعد الإنشاء يفترض هذا أصلًا، فلمَ لا يُصرَّح به في النوع؟ ستستعمله بنفسك بعد قليل.


الدرس ٣: تحت Vec — المخصِّص (allocator) اليدوي

لبناء Vec بنفسك تحتاج ثلاث أدوات من std::alloc، وهي نفس ما تفعله malloc/free/realloc في C، بواجهة مطابقة للنوع:

use std::alloc::{alloc, dealloc, realloc, Layout, handle_alloc_error};

let layout = Layout::array::<i32>(10).unwrap();   // حجم + محاذاة لعشرة i32
let ptr = unsafe { alloc(layout) } as *mut i32;    // مطابق لـ malloc
if ptr.is_null() { handle_alloc_error(layout); }   // فشل التخصيص — لا رفاهية Result هنا
// ... استعمال ...
unsafe { dealloc(ptr as *mut u8, layout); }        // مطابق لـ free

Layout تحمل الحجم والمحاذاة (alignment) معًا — تفصيلة قد لم تحتَجها صراحة في C إن كان malloc يتكفّل بمحاذاة آمنة لكل الأنواع تلقائيًا؛ هنا تحسبها صراحة عبر Layout::array::<T>(n) أو Layout::new::<T>()، ويمكنك فحص std::mem::size_of::<T>() وstd::mem::align_of::<T>() لأي نوع مباشرة.

**لماذا لا تكتب *ptr = value مباشرة لتهيئة خانة جديدة؟** لأن *ptr = value عبر مؤشر Rust (حتى الخام) تعني دلاليًا: "أسقط (drop) ما كان موجودًا في هذا الموضع أولًا، ثم ضع الجديد." لكن الذاكرة القادمة من alloc غير مهيّأة — قراءتها كقيمة T صالحة لإسقاطها UB فورية. الحل: std::ptr:: write(ptr, value) تكتب القيمة بلا محاولة إسقاط ما كان هناك؛ وأختها std::ptr::read(ptr) تقرأ قيمة بلا نقلها منطقيًا (تترك نسخة بتّية خلفها — مسؤوليتك ألا تقرأها مرتين وتُسقطها مرتين، وإلا double-free ذاتُه من الإقليم ٠١، لكنك الآن من يرتكبه لا نظام مؤتمَت).

للإزاحة داخل مساحة مخصَّصة (بدل حساب عنوان يدويًا كما في C): ptr.add(i) — مكافئ ptr + i في C، لكن باسم يفرض عليك التفكير: "أهذا ضمن الحدود المخصَّصة فعليًا؟" — السؤال الذي unsafe يذكّرك به في كل نداء.


الألغاز التمهيدية

لغز ٠٩-أ: المشي الخام. أنشئ Vec<i32> عاديًا آمنًا، احصل على مؤشر خام إليه عبر .as_ptr()، واكتب دالة unsafe fn sum_raw(ptr: *const i32, len: usize) -> i64 تجمع كل العناصر حصرًا عبر ptr.add(i) وإلغاء إشارة يدوي — بلا فهرسة []، بلا .iter(). تعامل بشكل صحيح مع len == 0 (يجب ألا تُلغى أي إشارة إطلاقًا في هذه الحالة — لماذا بالضبط لا يجوز حتى إنشاء ptr.add(0) مؤشرًا "لعنصر" حين لا عناصر أصلًا؟ فكّر بحدود المصفوفة الفعلية).

لغز ٠٩-ب: التقسيم المستحيل. أعد بناء split_at_mut بنفسك من الصفر (لا تستعمل نسخة std)، باستعمال slice.as_mut_ptr() وptr.add(mid) وstd::slice::from_raw_parts_mut. اكتب فوق كتلة unsafe الخاصة بك تعليق // SAFETY: يشرح — بجملة دقيقة — لماذا النصفان لا يتقاطعان أبدًا. اختبرها: عدّل كل نصف من خارج الدالة (بعد استدعائها) وتأكد أن التعديلات مستقلة كما تتوقع.


المعلم الأكبر: MyVec<T> من الصفر

المعلم الأكبر
المعلم الأكبر: MyVec<T> من الصفر

الآن أعد اشتقاق ما استعملتَه بثقة عمياء منذ الإقليم ٠٠: Vec<T> نفسها. هذا المعلم يُغلق الدائرة التي فتحها الإقليم ٠١: "في C كنتَ أنت الـ garbage collector" — هنا، لمرة واحدة مقصودة، تعود لتكون هو، لكن داخل جدار محكم، وبكامل الوعي بكل ما قد ينهار إن أخطأت.

الشكل الأساسي:

struct MyVec<T> {
    ptr: std::ptr::NonNull<T>,
    len: usize,
    cap: usize,
    _marker: std::marker::PhantomData<T>,
}

(PhantomData<T> تصريح لا يشغل ذاكرة، يخبر المترجم "تصرّف كأنني أملك T فعليًا" لأغراض فحوصات لا تظهر تلقائيًا من NonNull<T> وحدها — ابحث عنها في وثائق std إن أردت التفاصيل الكاملة؛ يكفيك الآن معرفة أنها موجودة وأن أضعها.)

المتطلبات الوظيفية (لا حلّ مرجعي — هذا معلمك لتبنيه):

قيود صارمة: ممنوع استعمال Vec أو Box الحقيقيتين داخل التنفيذ — الهدف أن تلمس alloc/ptr::write/ptr::read بيدك، لا أن تُغلِّف الحل الجاهز. اكتب // SAFETY: فوق كل كتلة unsafe.

اختبار حقيقي (اختياري لكن يستحق التجربة): إن ثبّتَّ Rust nightly، شغّل اختباراتك عبر cargo +nightly miri test — Miri مفسّر يكتشف UB ديناميكيًا (قراءة غير مهيّأة، تسريب، تجاوز حدود) حتى لو "نجح" برنامجك ظاهريًا. هذه هي أداة التحقق الحقيقية التي يستعملها مطوّرو std أنفسهم على كودهم unsafe.


ملاحظة عابرة: union وextern "C"

أقفلنا دائرة فتحها الإقليم ٠٣: تذكّر union C بلا تاغ في مطلعه؟ Rust تمنحك union أيضًا — لكنها استثناء واعٍ الآن: كل قراءة حقل تتطلب unsafe صراحةً (القدرة الخامسة في الجدول)، بعكس افتراض C الصامت. ولأن unsafe تعني أيضًا "تعامل مع الذاكرة كما تفعل C تمامًا"، فهي نفسها الجسر لاستدعاء كود C موجود فعليًا (extern "C" { fn some_c_func(x: i32) -> i32; }) أو تصدير دوال Rust لتُنادى من C — موضوع كامل بذاته (FFI)، خارج عمق هذا المنهج، لكن يستحق أن تعرف أنه موجود ومبني على نفس الأساس الذي بنيتَه للتو.


الخلاصة

بذرة الغموض

كل ما بنيتَه حتى الآن ينفَّذ فورًا حين يُستدعى. آخر مفهوم في هذا المنهج يقلب حتى هذا: قيمة تكتبها الآن، لا تُنفَّذ شيئًا إلا حين يقرر شيء آخر — لم تلتقِه بعد — أن الوقت قد حان لتحريكها خطوة واحدة للأمام. تمهَّل: القاع الذي نزلتَ إليه للتو (المؤشرات الخام) سيظهر مجددًا هناك، من زاوية لم تتوقعها.