לדלג לתוכן

3.5 - מקצים השמדה ואינווריאנטים של בעלות - הרצאה

מקצים מקום ואז מעבירים אליו ערך

נבנה מעטפת קטנה שמחזיקה ערך אחד בהקצאה עצמאית. הספרייה כבר מספקת Box, ולכן המטרה היא לפרק את העבודה שהיא חוסכת לנו: חישוב גודל ויישור, טיפול בכשל, יצירת ערך, השמדה ושחרור באותו חוזה הקצאה.

השלב הראשון הוא Layout. הוא מתאר כמה בייטים נדרשים ואיזה יישור הם חייבים לקבל. אסור להקצות לפי size_of בלבד ואז לשחרר לפי הנחות אחרות. זוג הפעולות alloc ו־dealloc צריך לעבוד עם אותו חוזה של פריסה ומקצה.

use std::alloc::{alloc, dealloc, handle_alloc_error, Layout};
use std::marker::PhantomData;
use std::ptr::NonNull;

struct Owned<T> {
    pointer: NonNull<T>,
    owner: PhantomData<T>,
}

impl<T> Owned<T> {
    fn new(value: T) -> Self {
        let layout = Layout::new::<T>();
        let pointer: NonNull<T> = if layout.size() == 0 {
            NonNull::dangling()
        } else {
            // SAFETY: layout has a nonzero size and valid alignment.
            let raw = unsafe { alloc(layout) }.cast::<T>();
            NonNull::new(raw).unwrap_or_else(|| handle_alloc_error(layout))
        };
        // SAFETY: the pointer is aligned and has storage for T;
        // for a zero-sized T no bytes are accessed.
        unsafe { pointer.as_ptr().write(value); }
        Self { pointer, owner: PhantomData }
    }

    fn get(&self) -> &T {
        // SAFETY: this owner holds an initialized live T.
        unsafe { self.pointer.as_ref() }
    }

    fn get_mut(&mut self) -> &mut T {
        // SAFETY: the exclusive borrow of the owner controls access to T.
        unsafe { self.pointer.as_mut() }
    }
}

impl<T> Drop for Owned<T> {
    fn drop(&mut self) {
        // SAFETY: the initialized T is owned exactly once by this wrapper.
        unsafe { self.pointer.as_ptr().drop_in_place(); }
        let layout = Layout::new::<T>();
        if layout.size() != 0 {
            // SAFETY: this address came from alloc with the same layout.
            unsafe { dealloc(self.pointer.as_ptr().cast::<u8>(), layout); }
        }
    }
}

fn main() {
    let mut owned = Owned::new(String::from("hello"));
    owned.get_mut().push_str(" world");
    assert_eq!(owned.get(), "hello world");
    let empty = Owned::new(());
    assert_eq!(empty.get(), &());
}

האחסון אינו בעל ערך עד הכתיבה

הקריאה alloc מספקת מקום, אבל אינה בונה T ואינה מאתחלת אותו. רק write מעביר את הערך אל האחסון. אם היינו יוצרים לפני כן הפניה רגילה ל־T ומאפשרים לקורא להשתמש בה, היינו טוענים לתקפות שעדיין אינה קיימת.

הפעולה write גם אינה מפעילה destructor על תוכן ישן. במקרה שלנו האחסון חדש, ולכן זהו בדיוק החוזה הדרוש. כאשר נרצה להחליף ערך חי נצטרך להשמיד או להעביר את הישן באופן מבוקר, ולא לדרוס את התיאור שלו ולהשאיר משאבים ללא בעלים.

טיפוס בגודל אפס עדיין יכול להתנהג

עבור T בגודל אפס אין צורך לבקש אזור בעל גודל חיובי מן המקצה. אנחנו משתמשים במצביע dangling מיושר שאינו null, בהתאם לחוזי פעולות על טיפוס כזה. אין לקרוא ל־dealloc עבור כתובת שלא התקבלה מהקצאה מתאימה.

עם זאת, טיפוס בגודל אפס יכול להחזיק destructor בעל תופעת לוואי. לכן העובדה שאין בייטים להקצות אינה פוטרת מן הצורך להשמיד את הערך הלוגי פעם אחת. המימוש קורא ל־drop_in_place גם במקרה הזה ורק מדלג על שחרור ההקצאה.

שחרור צריך להגיע לאותו מקצה

בגבול בין ספריות קל להעביר מצביע ולשכוח מי יצר אותו. אם ספריית C הקצתה נתון במנגנון מסוים, אי אפשר להניח ש־dealloc של ראסט הוא הדרך לשחרר אותו. גם כאשר שתי ספריות מגיעות בסוף לאותו allocator במערכת מסוימת, זה אינו בהכרח החוזה שהממשק שלהן מבטיח.

לכן ממשק בעלות צריך להעביר יחד עם הנתון גם את ידיעת דרך ההשמדה, אם היא אינה קבועה בסוג. אפשר לעשות זאת באמצעות פונקציית שחרור חיצונית, טיפוס אטום או wrapper שקריאתו ל־Drop מגיעה ל־API הנכון. בפרקי FFI נכתוב חוזה כזה במפורש.

כשל הקצאה ופאניקה בהשמדה

המימוש שלנו משתמש במדיניות handle_alloc_error במקרה שהמקצה מחזיר null. זו אינה החזרת Err רגילה, ולכן אין להציג את new הזאת כפונקציה שכל כשליה ניתנים לטיפול באמצעות Result. אם ה־API צריך להחזיר כשל הקצאה, נבנה constructor אחר ונשמור גם את הבעלות ב־value במקרה הכשל.

אם Drop של T נכנס לפאניקה, ההרצה עלולה לעזוב את המעטפת לפני dealloc. המימוש הלימודי יכול במקרה כזה לדלוף את האחסון, אף שאינו יוצר בעקבות כך שחרור כפול. כדי לתת הבטחת ניקוי חזקה יותר נשתמש ב־guard שדואג לשחרור ההקצאה גם בזמן unwinding. צריך גם להתייחס בנפרד למצב abort, שבו אין הבטחה להרצת guards.

השמדה ויחסי טיפוסים

הטיפוס מחזיק T בבעלות מבחינה לוגית אף שהשדה הישיר הוא מצביע. כאשר בונים מבנים גנריים מורכבים, חייבים לבדוק כיצד המהדר מבין את הבעלות לצורך drop checking, שונות ותכונות אוטומטיות. במעטפת כללית נשתמש בסמנים מתאימים ונבחן גם ערכי T שמכילים הפניות, ולא רק String עצמאית.

אין להוסיף מימושי Send ו־Sync רק כדי לאפשר להעביר את המבנה לתהליכון. המימושים האלה מחייבים טיעון על T ועל דרך הגישה. בפרק המקביליות נבדוק את החוזים בנפרד. בשלב הזה המעטפת אינה מבטיחה התנהגות מלאה של Box ואינה תחליף לספרייה הסטנדרטית.

בתרגול נתמקד בבדיקת השמדה אחת וביישור חריג. בדיקות כאלה אינן מכסות את כל תנאי ההקצאה, אבל הן חושפות בקלות מימוש ששוכח את היישור או מבלבל בין אחסון לערך חי.