返回文章列表

Rust 原始指標記憶體管理

本文探討 Rust 中的原始指標(Raw Pointers)使用,包含基本規則、RefWithFlag 例項、裸指標與記憶體管理、空指標、型別大小與對齊、指標運算、移動值與記憶體初始化狀態、使用原始指標進行記憶體操作、GapBuffer 實作範例以及缺口緩衝區實作解析等內容,幫助開發者理解如何在 Rust

程式語言 系統程式設計

Rust 的原始指標允許直接操作記憶體地址,但也繞過了 Rust 的安全檢查機制。正確使用原始指標需要遵循借用規則、有效性、偏移量計算和型別不變性等基本規則。RefWithFlag 範例展示瞭如何利用原始指標將參照和布林值封裝到一個機器字中。理解裸指標與記憶體管理的關係,以及如何使用 PhantomData 避免未使用引數警告至關重要。空指標的處理、型別大小與對齊的考量、指標運算的注意事項,以及如何管理記憶體的初始化狀態,都是安全使用原始指標的關鍵。std::ptr 模組提供了 readwrite 等函式,方便進行精細的記憶體操作。Vecpushpop 操作也體現了這些原理。最後,GapBuffer 的實作範例展示瞭如何利用原始指標和記憶體管理技巧構建高效的資料結構。

Rust 中的原始指標(Raw Pointers)使用

Rust 的原始指標是一種強大但需謹慎使用的工具。它們允許開發者直接操作記憶體地址,但同時也繞過了 Rust 的安全檢查機制。因此,瞭解如何正確使用原始指標對於編寫高效且安全的 Rust 程式碼至關重要。

原始指標的基本規則

使用原始指標時,必須遵守以下基本規則,以確保程式的安全性:

  1. 借用規則(Borrowing Rule):當你擁有某個值的可變參照時,你不能同時擁有該值的不可變參照。對於原始指標,這意味著當你使用一個原始指標時,必須確保它所指向的值不會被其他參照所修改,除非該參照是隻讀的。

  2. 有效性(Validity):你只能在原始指標指向一個有效的值時,才可以對其進行解參照(dereference)。例如,當你對一個 *const char 進行解參照時,必須確保它指向一個有效的 Unicode 碼點。

  3. 偏移量(Offset)和包裝偏移量(Wrapping Offset):你只能使用 offsetwrapping_offset 方法來計算原始指標在同一變數或堆積分配記憶體區塊內的偏移量,或是指向該區域之後的第一個位元組。

  4. 型別不變性(Type Invariants):當你對原始指標所指向的值進行指定時,不能違反該值的型別不變性。例如,如果你有一個指向 String 中某個位元組的 *mut u8,你只能儲存那些保持 String 為有效 UTF-8 的值。

例項:RefWithFlag

下面是一個利用原始指標實作的 RefWithFlag 型別,它將一個參照和一個布林值封裝到一個機器字(machine word)中。這種技術在垃圾收集器和虛擬機器中非常常見,因為它可以節省記憶體。

mod ref_with_flag {
    use std::marker::PhantomData;
    use std::mem::align_of;

    /// 一個 `&T` 和一個 `bool` 的組合,包裝在單一字中。
    /// 型別 `T` 必須至少需要兩個位元組的對齊。
    pub struct RefWithFlag<'a, T: 'a> {
        ptr_and_bit: usize,
        behaves_like: PhantomData<&'a T>,
    }

    impl<'a, T: 'a> RefWithFlag<'a, T> {
        pub fn new(ptr: &'a T, flag: bool) -> RefWithFlag<T> {
            assert!(align_of::<T>() % 2 == 0);
            RefWithFlag {
                ptr_and_bit: (ptr as *const T as usize) | (flag as usize),
                behaves_like: PhantomData,
            }
        }

        pub fn get_ref(&self) -> &'a T {
            unsafe {
                let ptr = (self.ptr_and_bit & !1) as *const T;
                &*ptr
            }
        }

        pub fn get_flag(&self) -> bool {
            self.ptr_and_bit & 1 != 0
        }
    }
}

內容解密:

  1. RefWithFlag 結構體:它包含兩個欄位:ptr_and_bit 用於儲存封裝後的指標和布林值,behaves_like 是一個 PhantomData 例項,用於告訴 Rust 如何處理 RefWithFlag 的生命週期。

  2. new 方法:它檢查 T 的對齊是否適合封裝操作,然後將參照轉換為原始指標,接著轉換為 usize,並與布林值進行按位或運算。

  3. get_ref 方法:它透過遮蔽 ptr_and_bit 的最低位來還原始指標,然後解參照並傳回一個參照。這個過程是 unsafe 的,因為它涉及到原始指標的操作。

  4. get_flag 方法:它簡單地檢查 ptr_and_bit 的最低位是否非零,以傳回布林值。

  5. PhantomData 的作用:它告訴 Rust 將 RefWithFlag 當作包含一個 &'a T 一樣處理,即使實際上並沒有儲存任何參照。這對於生命週期的管理至關重要。

Rust 中的裸指標與記憶體管理

Rust 的裸指標(Raw Pointers)是一種底層的指標型別,它與參照(References)不同,不受 Rust 的借用規則(Borrowing Rules)約束。裸指標主要用於與 C 語言程式碼互動、實作複雜的資料結構,或是在某些效能關鍵的場合下進行最佳化。

使用 PhantomData 避免未使用引數警告

在某些情況下,編譯器可能會警告某些引數未被使用,例如在 RefWithFlag 結構中。為瞭解決這個問題,可以使用 PhantomData 來告訴編譯器,這些引數雖然未直接使用,但它們對於型別的語義是有意義的。

use std::marker::PhantomData;

struct RefWithFlag<T> {
    ptr_and_bit: usize,
    _marker: PhantomData<T>,
}

內容解密:

  • PhantomData 是一種零大小的型別,用於在編譯期告訴 Rust 編譯器某個型別引數是有意義的,即使它在執行時並未被使用。
  • RefWithFlag 中,_marker 欄位使用了 PhantomData<T>,這樣可以避免編譯器警告 T 未被使用。
  • 這種技巧常用於實作底層資料結構或與 FFI(Foreign Function Interface)互動時,用於確保型別的正確性和安全性。

空指標(Nullable Pointers)

Rust 中的空裸指標表示為零地址,與 C 和 C++ 中的空指標類別似。可以使用 std::ptr::nullstd::ptr::null_mut 分別建立 *const T*mut T 型別的空指標。

let null_ptr: *const i32 = std::ptr::null();
let null_mut_ptr: *mut i32 = std::ptr::null_mut();

內容解密:

  • std::ptr::nullstd::ptr::null_mut 用於建立空的裸指標。
  • 可以使用 is_null 方法檢查一個裸指標是否為空。
  • as_refas_mut 方法可以將裸指標轉換為 Option<&T>Option<&mut T>,從而更方便地進行空指標檢查。

型別大小與對齊

每個 Sized 型別在記憶體中佔據固定的位元組數,並且必須放置在符合其對齊(Alignment)要求的位置上。可以使用 std::mem::size_ofstd::mem::align_of 函式查詢型別的大小和對齊要求。

assert_eq!(std::mem::size_of::<i64>(), 8);
assert_eq!(std::mem::align_of::<(i32, i32)>(), 4);

內容解密:

  • std::mem::size_of::<T>() 傳回型別 T 的大小(以位元組為單位)。
  • std::mem::align_of::<T>() 傳回型別 T 的對齊要求。
  • 型別的大小總是會向上捨入到其對齊的倍數,即使實際上它可以佔用更少的空間。
  • 對於未定大小的型別(Unsized Types),其大小和對齊取決於具體的值,可以使用 std::mem::size_of_valstd::mem::align_of_val 函式查詢。

指標運算(Pointer Arithmetic)

Rust 保證陣列、切片或向量中的元素在記憶體中是連續排列的。因此,可以透過對裸指標進行運算來存取特定的元素。

struct Iter<'a, T: 'a> {
    ptr: *const T,
    end: *const T,
}

內容解密:

  • 在 Rust 中,陣列和切片的元素在記憶體中是連續存放的,這使得透過裸指標進行運算來存取元素成為可能。
  • offset 方法可以對裸指標進行偏移運算,從而得到指向陣列中其他元素的指標。
  • 使用 offset 時需要注意不要越界,否則會導致未定義行為。
  • 可以使用 wrapping_offset 方法進行環繞偏移,但需要注意這樣得到的指標可能無效。

管理記憶體

當實作自行管理記憶體的型別時,需要追蹤哪些部分是已初始化的,哪些是未初始化的。

let pot = "pasta".to_string();
let plate;
plate = pot;

內容解密:

  • 在實作自管理記憶體的型別時,需要小心處理值的移動和複製,以確保記憶體的安全性。
  • Rust 的所有權系統和借用檢查器有助於避免常見的記憶體錯誤,如空指標解參照、資料競爭等。
  • 正確地管理記憶體需要深入理解 Rust 的所有權模型和生命週期機制。

深入理解 Rust 中的原始指標與記憶體管理

Rust 的記憶體管理機制是其安全性的重要保障,但在某些情況下,我們需要使用原始指標(Raw Pointers)來直接操作記憶體。本文將探討 Rust 中的原始指標、記憶體初始化狀態的追蹤,以及如何使用 std::ptr 模組中的函式來實作自定義的記憶體管理資料結構。

移動值與記憶體初始化狀態

在 Rust 中,當我們將一個值從一個變數移動到另一個變數時,原來的變數會變成未初始化狀態。例如:

let mut pot = "pasta".to_string();
let plate = pot;

在這個例子中,pot 原本擁有字串 “pasta”,但在將其移動到 plate 之後,pot 變成了未初始化狀態,而 plate 成為了字串的新擁有者。

內容解密:

  1. let mut pot = "pasta".to_string();:建立一個可變的字串變數 pot,並初始化為 “pasta”。
  2. let plate = pot;:將 pot 中的字串移動到 plate,此時 pot 變成未初始化狀態。
  3. 移動操作通常不會改變源值的位元模式,但 Rust 保證我們不會誤用未初始化的值。

同樣的原理適用於管理自身記憶體的資料結構,如 Vec。當我們對 Vec 執行 pushpop 操作時,Vec 會內部管理其緩衝區的初始化狀態。

使用原始指標進行記憶體操作

Rust 提供了 std::ptr 模組中的函式來實作對記憶體的精細控制,主要包括:

  • std::ptr::read(src):將 src 指標指向的值移動到呼叫者,之後 src 指向的記憶體被視為未初始化。
  • std::ptr::write(dest, value):將 value 移動到 dest 指標指向的未初始化記憶體。

這些函式是實作自定義記憶體管理資料結構的基礎。例如,Vec::pop 使用 read 將值從緩衝區中取出,並標記該空間為未初始化;Vec::push 使用 write 將新值寫入下一個可用的空間,並更新長度。

程式碼範例:

let mut noodles = vec!["udon".to_string()];
let soba = "soba".to_string();
noodles.push(soba);
// soba 現在是未初始化狀態

內容解密:

  1. 建立一個包含 “udon” 字串的向量 noodles
  2. 建立一個字串 soba,內容為 “soba”。
  3. soba 推入 noodles 向量中,此時 soba 變成未初始化狀態。

GapBuffer 實作範例

GapBuffer 是一種用於文字編輯器的資料結構,能夠在插入和刪除操作上提供較好的效能。以下是一個簡化的 GapBuffer 實作:

struct GapBuffer {
    // 資料儲存
    buffer: Vec<char>,
    // 空隙起始位置
    gap_start: usize,
    // 空隙結束位置
    gap_end: usize,
}

impl GapBuffer {
    fn new() -> Self {
        GapBuffer {
            buffer: Vec::new(),
            gap_start: 0,
            gap_end: 0,
        }
    }

    fn insert(&mut self, c: char) {
        // 在空隙起始位置插入字元,並更新空隙位置
        self.buffer[self.gap_start] = c;
        self.gap_start += 1;
    }

    // 其他方法實作...
}

內容解密:

  1. 定義 GapBuffer 結構,包含字元向量 buffer、空隙起始位置 gap_start 和結束位置 gap_end
  2. insert 方法中,在空隙起始位置插入新字元,並更新空隙起始位置。

缺口緩衝區(Gap Buffer)實作解析

缺口緩衝區是一種特殊的資料結構,用於高效地處理文字編輯操作。它透過在記憶體中維護一個「缺口」(gap),使得插入和刪除操作變得更加快速。本文將探討 Rust 語言中缺口緩衝區的實作細節。

基本結構

首先,我們來看看 GapBuffer 的基本結構定義:

pub struct GapBuffer<T> {
    storage: Vec<T>,
    gap: Range<usize>
}

這裡,storage 欄位用於儲存實際的資料,而 gap 欄位則代表了缺口的位置和大小。值得注意的是,storage 的長度始終保持為 0,但其容量會根據需求進行調整。

關鍵方法實作

初始化與基本操作

impl<T> GapBuffer<T> {
    pub fn new() -> GapBuffer<T> {
        GapBuffer { storage: Vec::new(), gap: 0..0 }
    }

    pub fn len(&self) -> usize {
        self.capacity() - self.gap.len()
    }

    pub fn position(&self) -> usize {
        self.gap.start
    }
}

這些方法提供了基本的初始化和查詢功能。len 方法傳回當前緩衝區中元素的數量,而 position 方法則傳回目前的插入位置。

元素存取

/// Return a reference to the `index`'th element,
/// or `None` if `index` is out of bounds.
pub fn get(&self, index: usize) -> Option<&T> {
    let raw = self.index_to_raw(index);
    if raw < self.capacity() {
        unsafe {
            Some(&*self.space(raw))
        }
    } else {
        None
    }
}

get 方法用於存取指定索引處的元素。它首先透過 index_to_raw 方法將邏輯索引轉換為實際的記憶體索引,然後進行邊界檢查,最後傳回元素的參考。

缺口移動

/// Set the current insertion position to `pos`.
/// If `pos` is out of range, panic.
pub fn set_position(&mut self, pos: usize) {
    // ...
}

set_position 方法用於移動缺口到指定的位置。這涉及到將元素從一個位置複製到另一個位置,以維持資料的正確性。

插入操作

/// Insert `elt` at the current insertion position,
/// and leave the insertion position after it.
pub fn insert(&mut self, elt: T) {
    if self.gap.len() == 0 {
        self.enlarge_gap();
    }
    unsafe {
        let index = self.gap.start;
        std::ptr::write(self.space_mut(index), elt);
    }
    self.gap.start += 1;
}

插入操作首先檢查是否需要擴大缺口。如果需要,則呼叫 enlarge_gap 方法進行擴充。然後,它使用 std::ptr::write 將新元素寫入缺口的起始位置,並更新缺口的位置。

實作細節與安全性考量

GapBuffer 的實作中,多處使用了 unsafe 程式碼區塊,這是因為它直接操作了原始指標。為了確保安全性,實作者需要謹慎地處理指標運算和記憶體存取。例如,在 get 方法中,透過 index_to_raw 方法計算出的索引必須在有效範圍內,這一點透過邊界檢查來保證。

內容解密:

  1. GapBuffer 結構設計:使用 Vec<T> 作為底層儲存,但保持其長度為 0,利用其容量來儲存資料。這種設計允許靈活管理記憶體。
  2. get 方法實作:透過 index_to_raw 將邏輯索引對映到實際記憶體索引,並進行邊界檢查以確保安全性。
  3. set_position 方法:移動缺口到指定位置,涉及元素的複製和缺口範圍的更新。這是缺口緩衝區實作中的關鍵步驟。
  4. 插入操作的安全性:在插入新元素前檢查缺口大小,必要時擴大缺口。使用 std::ptr::write 安全地寫入新元素。
  5. 效能考量:缺口緩衝區的設計最佳化了插入和刪除操作的效能,特別是在處理大量資料時。