ExamplesBy LevelBy TopicLearning Paths
699 Fundamental

Raw Pointer Basics

Functional Programming

Tutorial

The Problem

Raw pointers (*const T and *mut T) are Rust's lowest-level memory access primitive — the equivalent of C's pointers. They bypass the borrow checker entirely: no lifetime tracking, no aliasing rules, no validity guarantees. They are essential for FFI, custom allocators, lock-free data structures, and performance-critical code that cannot afford the overhead of safe abstractions. The safe-wrapper idiom encapsulates raw pointer operations behind a safe function boundary, isolating the unsafe footprint.

🎯 Learning Outcomes

  • • How *const T and *mut T differ from references: no lifetime, no borrow rules
  • • How to create raw pointers from references: slice.as_ptr(), &x as *const T
  • • How ptr.add(idx) performs bounds-checked offset arithmetic
  • • How to write safe wrappers that validate preconditions before using raw pointers
  • • Where raw pointers are necessary: FFI, custom allocators, MaybeUninit initialization
  • Code Example

    #![allow(clippy::all)]
    //! 699 — Raw Pointer Basics
    //! *const T and *mut T: creation, casting, and safe wrapping.
    
    /// 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);
        }
    }

    Key Differences

  • Memory model: Rust raw pointers are explicit in source code and require unsafe; OCaml's GC manages all pointers transparently.
  • SAFETY comments: Rust's idiom requires // SAFETY: comments explaining why each unsafe block is sound; OCaml has no equivalent because unsafe is not part of the language.
  • Bounds checking: Rust manually checks bounds before raw pointer access; OCaml array access always bounds-checks.
  • FFI integration: Rust raw pointers map directly to C pointers in FFI; OCaml's ctypes library wraps C pointers in type-safe OCaml values.
  • OCaml Approach

    OCaml's GC manages all memory — raw pointer access is not part of normal OCaml programming. Unsafe memory operations use Bigarray, Bytes, or the ctypes FFI library:

    (* OCaml equivalent: use array indexing with bounds checking *)
    let safe_read arr idx =
      if idx >= Array.length arr then None
      else Some arr.(idx)
    

    Full Source

    #![allow(clippy::all)]
    //! 699 — Raw Pointer Basics
    //! *const T and *mut T: creation, casting, and safe wrapping.
    
    /// 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);
        }
    }
    ✓ Tests Rust test suite
    #[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);
        }
    }

    Exercises

  • Stride access: Implement fn every_nth(slice: &[i32], n: usize) -> Vec<i32> using raw pointer arithmetic — validate n > 0 and bounds check before each unsafe dereference.
  • Swap via pointers: Write unsafe fn swap_raw<T>(a: *mut T, b: *mut T) and a safe wrapper fn safe_swap<T>(a: &mut T, b: &mut T) — include SAFETY documentation.
  • SAFETY audit: Take the source code and add SAFETY comments to every unsafe operation listing: (a) the precondition that must hold, (b) why it holds in this context.
  • Open Source Repos