๐Ÿฆ€ Functional Rust

699: Raw Pointer Basics

Difficulty: 4 Level: Expert Create, cast, and safely dereference `const T` and `mut T` raw pointers.

The Problem This Solves

Safe Rust's reference rules โ€” one writer XOR many readers, always valid, always aligned โ€” give the borrow checker everything it needs to prove memory safety at compile time. But those same rules make certain low-level patterns impossible to express: building a custom allocator, writing into a C library's output parameter, slicing into a buffer at an arbitrary byte offset, or constructing a self-referential data structure. Raw pointers exist for exactly these situations. They carry none of the borrow checker's guarantees, which means you must enforce those guarantees manually. Creating a raw pointer is always safe in Rust; dereferencing it requires an `unsafe` block where you assert, in a `// SAFETY:` comment, that the pointer is valid, aligned, non-null, and not aliased by a conflicting reference. unsafe is a tool, not a crutch โ€” use only when safe Rust genuinely can't express the pattern.

The Intuition

Rust has two raw pointer types: `const T` (read-only) and `mut T` (read/write). Think of them as street addresses โ€” you can write down any address without leaving your house (creation is safe), but physically going there and reading or writing memory is a commitment you make at your own risk (dereferencing requires `unsafe`). The compiler cannot verify that the address is currently occupied, that the occupant is what you expect, or that nobody else is modifying it at the same time. It steps back and trusts your `// SAFETY:` comment.

How It Works in Rust

// Creating raw pointers โ€” safe, no dereference yet
let value: i32 = 42;
let const_ptr: *const i32 = &value as *const i32;

let mut mutable_val: i32 = 100;
let mut_ptr: *mut i32 = &mut mutable_val as *mut i32;

// Dereferencing โ€” requires unsafe + proof
let read_val = unsafe {
 // SAFETY: const_ptr derives from `value` which is still live on the
 // stack; the pointer is valid, aligned, and no &mut alias exists.
 *const_ptr
};

// Safe wrapper: bounds-check before touching raw memory
fn safe_read(slice: &[u32], idx: usize) -> Option<u32> {
 if idx >= slice.len() { return None; }
 let ptr: *const u32 = slice.as_ptr();
 Some(unsafe {
     // SAFETY: idx < slice.len() verified above; ptr is valid for
     // slice.len() elements; slice invariants guarantee alignment.
     *ptr.add(idx)
 })
}
Key invariants you must guarantee manually: 1. The pointer is non-null. 2. The memory it points to is valid (live, properly allocated). 3. The type's alignment requirement is met. 4. No conflicting `&mut` reference exists at the same time. 5. The memory has been fully initialised before reading.

What This Unlocks

Key Differences

ConceptOCamlRust
Raw memory access`Bigarray`, `Bytes.unsafe_get``const T` / `mut T` in `unsafe` block
Safety guaranteeRuntime checks or trustCompile-time (references) or manual (raw pointers)
Null pointerNot a concern (GC heap)`*mut T` can be null; use `NonNull<T>` to rule it out
Lifetime trackingGarbage collectorManual: you guarantee the pointee outlives the pointer
Borrow rulesNot enforcedSuspended for raw pointers โ€” you own the contract
//! 699 โ€” Raw Pointer Basics
//! *const T and *mut T: creation, casting, and safe wrapping.

fn main() {
    // Creating raw pointers (safe โ€” no dereference yet)
    let value: i32 = 42;
    let const_ptr: *const i32 = &value as *const i32;

    let mut mutable_val: i32 = 100;
    let mut_ptr: *mut i32 = &mut mutable_val as *mut i32;

    // Dereferencing requires unsafe + SAFETY proof
    let read_val = unsafe {
        // SAFETY: const_ptr was derived from `value` which is still live on
        // the stack; it is valid, aligned, and no &mut alias exists.
        *const_ptr
    };
    println!("const_ptr deref: {}", read_val);

    unsafe {
        // SAFETY: mut_ptr was derived from `mutable_val` which is still live;
        // we are the sole writer; no shared reference exists concurrently.
        *mut_ptr = 200;
    }
    println!("mutable_val after write: {}", mutable_val);

    // Safe wrapper around raw pointer access
    let arr = [1u32, 2, 3, 4, 5];
    println!("safe_read(2): {:?}", safe_read(&arr, 2));
    println!("safe_read(10): {:?}", safe_read(&arr, 10));

    // const vs mut pointer distinction
    let x = 7i64;
    let cp: *const i64 = &x;
    // *cp = 8;  // compile error: cannot write through *const
    let _ = cp;
}

/// Read `idx` from a slice via raw pointer, returning None for out-of-bounds.
fn safe_read(slice: &[u32], idx: usize) -> Option<u32> {
    if idx >= slice.len() {
        return None;
    }
    let ptr: *const u32 = slice.as_ptr();
    Some(unsafe {
        // SAFETY: idx < slice.len() checked above; ptr is valid for
        // slice.len() elements; alignment guaranteed by slice invariant.
        *ptr.add(idx)
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_safe_read_in_bounds() {
        let arr = [10u32, 20, 30];
        assert_eq!(safe_read(&arr, 0), Some(10));
        assert_eq!(safe_read(&arr, 2), Some(30));
    }

    #[test]
    fn test_safe_read_out_of_bounds() {
        let arr = [10u32, 20, 30];
        assert_eq!(safe_read(&arr, 3), None);
        assert_eq!(safe_read(&arr, usize::MAX), None);
    }

    #[test]
    fn test_raw_ptr_mut_write() {
        let mut val: i64 = 7;
        let p: *mut i64 = &mut val;
        unsafe {
            // SAFETY: p derived from `val` still live; no other references.
            *p = 42;
        }
        assert_eq!(val, 42);
    }

    #[test]
    fn test_const_ptr_read() {
        let data = [100u8, 200, 150];
        let ptr: *const u8 = data.as_ptr();
        let second = unsafe {
            // SAFETY: offset 1 < data.len() = 3; valid, aligned.
            *ptr.add(1)
        };
        assert_eq!(second, 200);
    }
}
(* OCaml: no raw pointers โ€” all memory is managed by the GC.
   We model the concept of "address-indexed" access with arrays,
   which is the closest safe analogue. *)

(** Read value at given index, returning None for out-of-bounds. *)
let read_at (arr : 'a array) (index : int) : 'a option =
  if index >= 0 && index < Array.length arr then Some arr.(index)
  else None

(** Write to a mutable slot, returning false if out-of-bounds. *)
let write_at (arr : 'a array) (index : int) (value : 'a) : bool =
  if index >= 0 && index < Array.length arr then begin
    arr.(index) <- value;
    true
  end else false

let () =
  let data = [| 10; 20; 30; 40; 50 |] in
  (match read_at data 2 with
   | Some v -> Printf.printf "Read at index 2: %d\n" v
   | None   -> print_endline "Out of bounds");
  let ok = write_at data 2 99 in
  Printf.printf "Write succeeded: %b\n" ok;
  Printf.printf "After write: %d\n" data.(2)