返回文章列表

Rust 內部可變性與智慧指標

Rust 的所有權和借用機制確保了記憶體安全,但有時會限制程式碼的靈活性。本文探討 Rust 的內部可變性與智慧指標,例如 `Cell`、`RefCell` 和 `Cow`,以及如何使用它們來實作更靈活的資料結構操作,同時維持 Rust

程式語言 系統程式設計

Rust 的所有權系統和借用規則雖然保障了記憶體安全,但在某些情況下,需要更彈性的資料操作方式。內部可變性允許在不直接持有資料所有權的情況下修改資料。Cell 適用於簡單型別,而 RefCell 則透過執行時借用檢查,允許修改更複雜的資料結構,例如文章中示範的雙向連結串列。Cow 智慧指標則提供了一種在需要修改時才複製資料的策略,提升了效率。此外,Rust 還允許開發者實作自定義分配器,以滿足特定的效能或安全需求,例如使用平台特定的 API 來分配對齊的記憶體,並控制記憶體的存取許可權。

深入理解 Rust 的內部可變性與智慧指標

Rust 語言透過其所有權系統和借用檢查器提供了記憶體安全保證,但在某些情況下,這些規則可能會限制程式碼的靈活性。為瞭解決這些問題,Rust 提供了 CellRefCell 這兩種型別,以實作內部可變性(interior mutability)。

內部可變性的必要性

在 Rust 中,借用檢查器確保了記憶體安全,但有時會限制程式碼的靈活性。當需要在不可變參照背後修改資料時,內部可變性就顯得尤為重要。CellRefCell 允許開發者在遵守 Rust 安全規則的同時,實作對資料的修改。

CellRefCell

  • CellCell 是一種允許在不可變參照背後修改值的型別。它透過將值移入和移出來實作可變性。Cell 適合用於簡單型別的資料,如數值或布林值。

  • RefCell:相比之下,RefCell 提供了更靈活的借用檢查機制。它允許在執行時檢查借用規則,而不是在編譯時。這使得 RefCell 成為處理複雜資料結構(如連結串列或圖)的理想選擇。

實作雙向連結串列

使用 Rc(參照計數)和 RefCell,可以實作一個雙向連結串列。這個例子展示瞭如何透過這些智慧指標來管理複雜的資料結構。

use std::cell::RefCell;
use std::rc::Rc;

struct ListItem<T> {
    prev: Option<ItemRef<T>>,
    data: Box<T>,
    next: Option<ItemRef<T>>,
}

type ItemRef<T> = Rc<RefCell<ListItem<T>>>;

struct DoublyLinkedList<T> {
    head: ItemRef<T>,
}

impl<T> ListItem<T> {
    fn new(data: T) -> Self {
        ListItem {
            prev: None,
            data: Box::new(data),
            next: None,
        }
    }

    fn data(&self) -> &T {
        self.data.as_ref()
    }
}

impl<T> DoublyLinkedList<T> {
    fn new(data: T) -> Self {
        DoublyLinkedList {
            head: Rc::new(RefCell::new(ListItem::new(data))),
        }
    }

    fn append(&mut self, data: T) {
        let tail = Self::find_tail(self.head.clone());
        let new_item = Rc::new(RefCell::new(ListItem::new(data)));
        new_item.borrow_mut().prev = Some(tail.clone());
        tail.borrow_mut().next = Some(new_item);
    }

    fn head(&self) -> ItemRef<T> {
        self.head.clone()
    }

    fn tail(&self) -> ItemRef<T> {
        Self::find_tail(self.head())
    }

    fn find_tail(item: ItemRef<T>) -> ItemRef<T> {
        if let Some(next) = &item.borrow().next {
            Self::find_tail(next.clone())
        } else {
            item.clone()
        }
    }
}

內容解密:

  1. ListItem 結構體:定義了雙向連結串列的節點,包含前驅節點 prev、資料 data 和後繼節點 next
  2. DoublyLinkedList 結構體:代表雙向鏈表本身,只包含指向頭節點的 head
  3. append 方法:在連結串列的尾部新增新節點。首先找到當前尾節點,然後建立新節點並更新相關指標。
  4. find_tail 方法:遞迴查詢連結串列的尾節點。

複製時寫入(Clone on Write)

複製時寫入是一種設計模式,避免了對資料的直接修改。相反,當需要修改資料時,會建立資料的副本,修改副本,然後傳回對副本的參照。Rust 中的 Cow(Copy on Write)智慧指標正是根據這一模式。

Cow 的定義

pub enum Cow<'a, B> where
    B: 'a + ToOwned + ?Sized,
{
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}

內容解密:

  1. Cow 是一個列舉:它可以是 Borrowed(借用)或 Owned(擁有)。這使得 Cow 可以根據需要選擇是否擁有資料。
  2. Cow 的靈活性:透過使用 Cow,可以在需要修改資料時才進行複製,從而提高效率並減少不必要的記憶體分配。

使用Cow實作不可變單向連結串列

為了展示Cow的用法,我們將更新單向連結串列的例子,使其成為不可變的資料結構。首先,讓我們看看以下程式碼,它與前一個版本相比,除了增加了#[derive(Clone)]之外,沒有太大的不同。

ListItem結構定義

#[derive(Clone)]
struct ListItem<T>
where
    T: Clone,
{
    data: Box<T>,
    next: Option<Box<ListItem<T>>>,
}

impl<T> ListItem<T>
where
    T: Clone,
{
    fn new(data: T) -> Self {
        ListItem {
            data: Box::new(data),
            next: None,
        }
    }

    fn next(&self) -> Option<&Self> {
        if let Some(next) = &self.next {
            Some(&*next)
        } else {
            None
        }
    }

    fn mut_tail(&mut self) -> &mut Self {
        if self.next.is_some() {
            self.next.as_mut().unwrap().mut_tail()
        } else {
            self
        }
    }

    fn data(&self) -> &T {
        self.data.as_ref()
    }
}

內容解密:

  1. ListItem結構代表連結串列中的一個節點,包含資料和指向下一個節點的指標。
  2. #[derive(Clone)]自動為ListItem實作Clone特性,使得該結構可以被克隆。
  3. new方法用於建立新的ListItem例項。
  4. next方法傳回當前節點的下一個節點的參照。
  5. mut_tail方法傳回連結串列的最後一個節點的可變參照,用於修改連結串列。
  6. data方法傳回當前節點資料的參照。

使用Cow的SinglyLinkedList

接下來,讓我們看看以下程式碼,它展示了在連結串列中如何使用Cow。

#[derive(Clone)]
struct SinglyLinkedList<'a, T>
where
    T: Clone,
{
    head: Cow<'a, ListItem<T>>,
}

impl<'a, T> SinglyLinkedList<'a, T>
where
    T: Clone,
{
    fn new(data: T) -> Self {
        SinglyLinkedList {
            head: Cow::Owned(ListItem::new(data)),
        }
    }

    fn append(&self, data: T) -> Self {
        let mut new_list = self.clone();
        let mut tail = new_list.head.to_mut().mut_tail();
        tail.next = Some(Box::new(ListItem::new(data)));
        new_list
    }

    fn head(&self) -> &ListItem<T> {
        &self.head
    }
}

內容解密:

  1. SinglyLinkedList結構包含一個Cow型別的頭節點,代表連結串列的起始。
  2. new方法建立一個新的連結串列,並初始化頭節點。
  3. append方法在連結串列的末尾新增新的節點,並傳回新的連結串列例項。這個操作是透過克隆原連結串列並修改克隆後的連結串列來實作的。
  4. head方法傳回連結串列頭節點的參照。

自定義分配器

在某些情況下,您可能需要自定義記憶體分配行為。Rust提供了GlobalAlloc API和Allocator API來實作自定義分配器。

編寫自定義分配器

讓我們來探索如何編寫一個自定義的分配器。我們的分配器將簡單地呼叫malloc()free()函式。首先,讓我們看看Allocator特性的定義。

pub unsafe trait Allocator {
    fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError>;
    unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout);
    // ... 其他方法
}

內容解密:

  1. Allocator特性定義了記憶體分配器的介面。
  2. allocate方法根據給定的佈局分配記憶體。
  3. deallocate方法釋放之前分配的記憶體。

實作簡單的分配器

#![feature(allocator_api)]
use std::alloc::{AllocError, Allocator, Global, Layout};
use std::ptr::NonNull;

struct SimpleAllocator;

unsafe impl Allocator for SimpleAllocator {
    fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
        // 呼叫malloc()進行記憶體分配
        let ptr = Global.allocate(layout)?;
        Ok(ptr.cast())
    }

    unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
        // 呼叫free()釋放記憶體
        Global.deallocate(ptr, layout);
    }
}

內容解密:

  1. 我們定義了一個名為SimpleAllocator的結構,並為其實作了Allocator特性。
  2. allocate方法中,我們呼叫全域性分配器的allocate方法來分配記憶體。
  3. deallocate方法中,我們呼叫全域性分配器的deallocate方法來釋放記憶體。

自定義記憶體分配器:原理與實作

在 Rust 程式設計中,記憶體管理是一個重要的議題。自定義分配器(Custom Allocator)允許開發者控制記憶體的分配和釋放,以滿足特定的效能或安全需求。本篇文章將探討 Rust 中的自定義分配器,包括其基本原理、實作方法和應用場景。

為什麼需要自定義分配器?

Rust 的標準函式庫提供了一個全域分配器(Global Allocator),用於管理記憶體的分配和釋放。然而,在某些情況下,開發者可能需要更細粒度的控制,例如:

  • 效能最佳化:特定的應用程式可能需要最佳化記憶體分配的效能。
  • 安全關鍵系統:某些系統需要嚴格控制記憶體存取,以防止安全漏洞。
  • 特殊硬體支援:某些硬體平台可能需要特殊的記憶體管理機制。

基本自定義分配器實作

要實作一個自定義分配器,需要實作 Allocator 特徵(Trait)。以下是一個簡單的例子,使用 mallocfree 函式進行記憶體管理:

#![feature(allocator_api)]
use std::alloc::{AllocError, Allocator, Layout};
use std::ptr::NonNull;
use libc::{free, malloc};

pub struct BasicAllocator;

unsafe impl Allocator for BasicAllocator {
    fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
        unsafe {
            let ptr = malloc(layout.size() as libc::size_t);
            if ptr.is_null() {
                return Err(AllocError);
            }
            let slice = std::slice::from_raw_parts_mut(ptr as *mut u8, layout.size());
            Ok(NonNull::new_unchecked(slice))
        }
    }

    unsafe fn deallocate(&self, ptr: NonNull<u8>, _layout: Layout) {
        free(ptr.as_ptr() as *mut libc::c_void);
    }
}

內容解密:

  1. allocate 方法:使用 malloc 分配記憶體,並將其轉換為 Rust 的切片(Slice)型別。檢查 malloc 的傳回值,如果為空指標,則傳回 AllocError
  2. deallocate 方法:使用 free 釋放記憶體。需要注意的是,這個方法已經被標記為 unsafe,因為它處理的是原始指標。

進階自定義分配器:受保護記憶體

在某些情況下,需要對記憶體進行額外的保護,以防止惡意存取。例如,在處理敏感資料(如加密金鑰)時,需要確保記憶體的安全性。

以下是一個進階的自定義分配器範例,使用平台特定的函式(如 posix_memalignVirtualAlloc)來分配對齊的記憶體,並使用 mprotectVirtualProtect 來控制記憶體存取許可權。

fn allocate(&self, layout: Layout) -> Result<ptr::NonNull<[u8]>, AllocError> {
    let pagesize = *PAGESIZE;
    let size = _page_round(layout.size(), pagesize) + 2 * pagesize;

    #[cfg(unix)]
    let out = {
        let mut out = ptr::null_mut();
        let ret = unsafe { libc::posix_memalign(&mut out, pagesize as usize, size) };
        if ret != 0 {
            return Err(AllocError);
        }
        out
    };

    #[cfg(windows)]
    let out = {
        use winapi::um::winnt::{MEM_COMMIT, MEM_RESERVE, PAGE_READWRITE};
        unsafe { winapi::um::memoryapi::VirtualAlloc(ptr::null_mut(), size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE) }
    };

    // ...
}

內容解密:

  1. 頁面對齊:根據平台的頁面大小(Page Size)對記憶體進行對齊,以滿足特定硬體或作業系統的需求。
  2. 平台特定分配:使用 posix_memalign(UNIX-like 系統)或 VirtualAlloc(Windows)來分配對齊的記憶體。
  3. 額外保護:在目標記憶體區域前後分配額外的頁面,並設定為不可存取,以提供額外的保護。