ExamplesBy LevelBy TopicLearning Paths
703 Fundamental

raw pointer arithmetic

Functional Programming

Tutorial

The Problem

This example covers a specific aspect of Rust's unsafe programming model: raw memory manipulation, FFI interop, allocator customization, or soundness principles. These topics are essential for systems programming — writing OS components, device drivers, game engines, and any code that must interact with C libraries or control memory layout precisely. Rust's unsafe system is designed to confine unsafety to small, auditable regions while maintaining safety in the surrounding code.

🎯 Learning Outcomes

  • • The specific unsafe feature demonstrated: raw pointer arithmetic
  • • When this feature is necessary vs when safe alternatives exist
  • • How to use it correctly with appropriate SAFETY documentation
  • • The invariants that must be maintained for the operation to be sound
  • • Real-world contexts: embedded systems, OS kernels, C FFI, performance-critical code
  • Code Example

    pub fn strided_collect_safe(slice: &[i32], stride: usize) -> Vec<i32> {
        if stride == 0 { return vec![]; }
        (0..slice.len()).step_by(stride).map(|i| slice[i]).collect()
    }

    Key Differences

  • Safety model: Rust requires explicit unsafe for these operations; OCaml achieves safety through the GC and type system without explicit unsafe regions.
  • FFI approach: Rust uses raw C types directly with extern "C"; OCaml uses ctypes which wraps C types in OCaml values.
  • Memory control: Rust allows complete control over memory layout (#[repr(C)], custom allocators); OCaml's GC manages memory layout automatically.
  • Auditability: Rust unsafe regions are syntactically visible and toolable; OCaml unsafe operations (Obj.magic, direct C calls) are also explicit but less common.
  • OCaml Approach

    OCaml's GC and type system eliminate most of the need for these unsafe operations. The equivalent functionality typically uses:

  • • C FFI via the ctypes library for external function calls
  • Bigarray for controlled raw memory access
  • • The GC for memory management (no manual allocators needed)
  • Bytes.t for mutable byte sequences
  • OCaml programs rarely need operations equivalent to these Rust unsafe patterns.

    Full Source

    #![allow(clippy::all)]
    //! 703 — Raw Pointer Arithmetic
    //!
    //! Demonstrates `ptr.add()`, `ptr.sub()`, and `ptr.offset()` with safe wrappers.
    //! Each unsafe block carries a `// SAFETY:` comment explaining why the offset is valid.
    
    /// Collect every `stride`-th element using raw pointer arithmetic.
    ///
    /// # Panics
    /// Never panics — returns empty vec for empty slice or stride == 0.
    pub fn strided_collect(slice: &[i32], stride: usize) -> Vec<i32> {
        if slice.is_empty() || stride == 0 {
            return vec![];
        }
        let mut result = Vec::new();
        let base: *const i32 = slice.as_ptr();
        let len = slice.len();
        let mut offset = 0usize;
        while offset < len {
            // SAFETY: `offset` is always < `len` == `slice.len()`.
            // `base` is valid for `len` elements (derived from a live slice).
            // Alignment is guaranteed by the slice invariant.
            result.push(unsafe { *base.add(offset) });
            offset = offset.saturating_add(stride);
        }
        result
    }
    
    /// Reverse a slice in-place using raw pointer swap via converging lo/hi pointers.
    pub fn reverse_in_place(slice: &mut [i32]) {
        let len = slice.len();
        if len < 2 {
            return;
        }
        // SAFETY: `lo` starts at index 0 and `hi` at index len-1, both within bounds.
        // We only dereference while lo < hi, so the two pointers never alias.
        unsafe {
            let base: *mut i32 = slice.as_mut_ptr();
            let mut lo = base;
            let mut hi = base.add(len - 1);
            while lo < hi {
                core::ptr::swap(lo, hi);
                lo = lo.add(1);
                hi = hi.sub(1);
            }
        }
    }
    
    /// Copy bytes from `src` to `dst` using `ptr.add()` inside a manual loop.
    ///
    /// Demonstrates walking two raw pointers in lockstep.
    pub fn manual_copy(src: &[u8], dst: &mut [u8]) {
        let count = src.len().min(dst.len());
        if count == 0 {
            return;
        }
        // SAFETY: `count` <= both slice lengths, so every offset in `0..count`
        // is valid for both `src_ptr` and `dst_ptr`.  The slices do not overlap
        // because one is shared and the other is exclusively borrowed.
        unsafe {
            let src_ptr: *const u8 = src.as_ptr();
            let dst_ptr: *mut u8 = dst.as_mut_ptr();
            for i in 0..count {
                *dst_ptr.add(i) = *src_ptr.add(i);
            }
        }
    }
    
    /// Read a value at a signed `offset` from the start of the slice using `ptr.offset()`.
    ///
    /// Returns `None` when the computed index is out of bounds.
    pub fn read_at_offset(slice: &[i32], offset: isize) -> Option<i32> {
        let len = slice.len() as isize;
        if offset < 0 || offset >= len {
            return None;
        }
        // SAFETY: we just checked 0 <= offset < len == slice.len(), so the pointer
        // stays within the allocation.
        Some(unsafe { *slice.as_ptr().offset(offset) })
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // --- strided_collect ---
    
        #[test]
        fn strided_collect_every_other() {
            let data: Vec<i32> = (0..10).collect();
            assert_eq!(strided_collect(&data, 2), vec![0, 2, 4, 6, 8]);
        }
    
        #[test]
        fn strided_collect_every_third() {
            let data: Vec<i32> = (0..10).collect();
            assert_eq!(strided_collect(&data, 3), vec![0, 3, 6, 9]);
        }
    
        #[test]
        fn strided_collect_stride_one_equals_slice() {
            let data = vec![7, 8, 9];
            assert_eq!(strided_collect(&data, 1), data);
        }
    
        #[test]
        fn strided_collect_empty_or_zero_stride() {
            assert_eq!(strided_collect(&[], 2), vec![]);
            assert_eq!(strided_collect(&[1, 2, 3], 0), vec![]);
        }
    
        #[test]
        fn strided_collect_stride_larger_than_len() {
            let data = vec![10, 20, 30];
            // stride = 5 > len = 3; only the first element is picked
            assert_eq!(strided_collect(&data, 5), vec![10]);
        }
    
        // --- reverse_in_place ---
    
        #[test]
        fn reverse_even_length() {
            let mut v = vec![1, 2, 3, 4];
            reverse_in_place(&mut v);
            assert_eq!(v, vec![4, 3, 2, 1]);
        }
    
        #[test]
        fn reverse_odd_length() {
            let mut v = vec![1, 2, 3, 4, 5];
            reverse_in_place(&mut v);
            assert_eq!(v, vec![5, 4, 3, 2, 1]);
        }
    
        #[test]
        fn reverse_single_and_empty() {
            let mut single = vec![42];
            reverse_in_place(&mut single);
            assert_eq!(single, vec![42]);
    
            let mut empty: Vec<i32> = vec![];
            reverse_in_place(&mut empty);
            assert_eq!(empty, vec![]);
        }
    
        // --- manual_copy ---
    
        #[test]
        fn manual_copy_basic() {
            let src = vec![1u8, 2, 3, 4, 5];
            let mut dst = vec![0u8; 5];
            manual_copy(&src, &mut dst);
            assert_eq!(dst, src);
        }
    
        #[test]
        fn manual_copy_dst_shorter() {
            let src = vec![10u8, 20, 30, 40];
            let mut dst = vec![0u8; 2];
            manual_copy(&src, &mut dst);
            assert_eq!(dst, vec![10, 20]);
        }
    
        // --- read_at_offset ---
    
        #[test]
        fn read_at_offset_valid() {
            let data = vec![100, 200, 300];
            assert_eq!(read_at_offset(&data, 0), Some(100));
            assert_eq!(read_at_offset(&data, 2), Some(300));
        }
    
        #[test]
        fn read_at_offset_out_of_bounds() {
            let data = vec![1, 2, 3];
            assert_eq!(read_at_offset(&data, 3), None);
            assert_eq!(read_at_offset(&data, -1), None);
        }
    
        #[test]
        fn read_at_offset_empty() {
            assert_eq!(read_at_offset(&[], 0), None);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // --- strided_collect ---
    
        #[test]
        fn strided_collect_every_other() {
            let data: Vec<i32> = (0..10).collect();
            assert_eq!(strided_collect(&data, 2), vec![0, 2, 4, 6, 8]);
        }
    
        #[test]
        fn strided_collect_every_third() {
            let data: Vec<i32> = (0..10).collect();
            assert_eq!(strided_collect(&data, 3), vec![0, 3, 6, 9]);
        }
    
        #[test]
        fn strided_collect_stride_one_equals_slice() {
            let data = vec![7, 8, 9];
            assert_eq!(strided_collect(&data, 1), data);
        }
    
        #[test]
        fn strided_collect_empty_or_zero_stride() {
            assert_eq!(strided_collect(&[], 2), vec![]);
            assert_eq!(strided_collect(&[1, 2, 3], 0), vec![]);
        }
    
        #[test]
        fn strided_collect_stride_larger_than_len() {
            let data = vec![10, 20, 30];
            // stride = 5 > len = 3; only the first element is picked
            assert_eq!(strided_collect(&data, 5), vec![10]);
        }
    
        // --- reverse_in_place ---
    
        #[test]
        fn reverse_even_length() {
            let mut v = vec![1, 2, 3, 4];
            reverse_in_place(&mut v);
            assert_eq!(v, vec![4, 3, 2, 1]);
        }
    
        #[test]
        fn reverse_odd_length() {
            let mut v = vec![1, 2, 3, 4, 5];
            reverse_in_place(&mut v);
            assert_eq!(v, vec![5, 4, 3, 2, 1]);
        }
    
        #[test]
        fn reverse_single_and_empty() {
            let mut single = vec![42];
            reverse_in_place(&mut single);
            assert_eq!(single, vec![42]);
    
            let mut empty: Vec<i32> = vec![];
            reverse_in_place(&mut empty);
            assert_eq!(empty, vec![]);
        }
    
        // --- manual_copy ---
    
        #[test]
        fn manual_copy_basic() {
            let src = vec![1u8, 2, 3, 4, 5];
            let mut dst = vec![0u8; 5];
            manual_copy(&src, &mut dst);
            assert_eq!(dst, src);
        }
    
        #[test]
        fn manual_copy_dst_shorter() {
            let src = vec![10u8, 20, 30, 40];
            let mut dst = vec![0u8; 2];
            manual_copy(&src, &mut dst);
            assert_eq!(dst, vec![10, 20]);
        }
    
        // --- read_at_offset ---
    
        #[test]
        fn read_at_offset_valid() {
            let data = vec![100, 200, 300];
            assert_eq!(read_at_offset(&data, 0), Some(100));
            assert_eq!(read_at_offset(&data, 2), Some(300));
        }
    
        #[test]
        fn read_at_offset_out_of_bounds() {
            let data = vec![1, 2, 3];
            assert_eq!(read_at_offset(&data, 3), None);
            assert_eq!(read_at_offset(&data, -1), None);
        }
    
        #[test]
        fn read_at_offset_empty() {
            assert_eq!(read_at_offset(&[], 0), None);
        }
    }

    Deep Comparison

    OCaml vs Rust: Raw Pointer Arithmetic

    Side-by-Side Code

    OCaml

    (* OCaml hides pointer arithmetic entirely; arrays are always bounds-checked. *)
    let strided_read (arr : 'a array) ~(start : int) ~(stride : int) : 'a list =
      let n = Array.length arr in
      let rec go i acc =
        if i >= n then List.rev acc
        else go (i + stride) (arr.(i) :: acc)
      in
      go start []
    

    Rust (idiomatic — safe slice indexing)

    pub fn strided_collect_safe(slice: &[i32], stride: usize) -> Vec<i32> {
        if stride == 0 { return vec![]; }
        (0..slice.len()).step_by(stride).map(|i| slice[i]).collect()
    }
    

    Rust (unsafe — raw pointer arithmetic)

    pub fn strided_collect(slice: &[i32], stride: usize) -> Vec<i32> {
        if slice.is_empty() || stride == 0 { return vec![]; }
        let mut result = Vec::new();
        let base: *const i32 = slice.as_ptr();
        let len = slice.len();
        let mut offset = 0usize;
        while offset < len {
            // SAFETY: offset < len; base valid for len elements; alignment guaranteed.
            result.push(unsafe { *base.add(offset) });
            offset = offset.saturating_add(stride);
        }
        result
    }
    

    Rust (in-place reversal via converging pointers)

    pub fn reverse_in_place(slice: &mut [i32]) {
        let len = slice.len();
        if len < 2 { return; }
        // SAFETY: lo starts at 0, hi at len-1; loop stops before they cross; no alias.
        unsafe {
            let base: *mut i32 = slice.as_mut_ptr();
            let mut lo = base;
            let mut hi = base.add(len - 1);
            while lo < hi {
                core::ptr::swap(lo, hi);
                lo = lo.add(1);
                hi = hi.sub(1);
            }
        }
    }
    

    Type Signatures

    ConceptOCamlRust
    Array element accessarr.(i) (safe, bounds-checked)*ptr.add(i) (unsafe, manual proof)
    Stride looptail-recursive go (i + stride)while offset < len + ptr.add
    In-place mutationArray.blit or index assignment*mut T + core::ptr::swap
    Safety boundaryRuntime exception on OOBunsafe block + // SAFETY: comment
    Signed offsetinteger subtraction on indexptr.offset(isize)

    Key Insights

  • No pointer exposure in OCaml: OCaml arrays are always accessed through safe, bounds-checked index operators. Pointer arithmetic is an implementation detail of the GC runtime, never surfaced to user code.
  • **ptr.add(n) advances by elements, not bytes:** Unlike C's char * arithmetic, Rust raw pointer arithmetic scales by size_of::<T>() automatically, matching OCaml's array indexing semantics while operating at the address level.
  • **unsafe as a proof obligation:** Rust doesn't forbid pointer arithmetic — it requires you to localise it in an unsafe block and document the invariant with // SAFETY:. OCaml enforces safety by construction; Rust enforces it by contract.
  • Converging-pointer reversal is idiomatic C/Rust, not OCaml: The lo/hi swap pattern has no natural OCaml equivalent. OCaml prefers Array.blit or functional reversal; Rust can express the in-place algorithm directly without intermediate allocation.
  • **ptr.offset vs ptr.add/ptr.sub:** ptr.offset(isize) is the signed, general form (positive = forward, negative = backward). ptr.add and ptr.sub are unsigned convenience wrappers that make intent clearer for unidirectional traversal.
  • When to Use Each Style

    Use safe slice indexing when: bounds checking overhead is negligible and code clarity matters — which is almost always.

    Use raw pointer arithmetic when: you've already verified the range at the call site and want to avoid redundant per-element checks in a tight inner loop, or when expressing a two-pointer algorithm (convergent swap, custom stride walk) that doesn't map cleanly to Rust's iterator combinators.

    Exercises

  • Minimize unsafe: Find the smallest possible unsafe region in the source and verify that all safe code is outside the unsafe block.
  • Safe alternative: Identify if a safe alternative exists for the demonstrated technique (e.g., bytemuck for transmute, CString for FFI strings) and implement it.
  • SAFETY documentation: Write a complete SAFETY comment for each unsafe block listing preconditions, invariants, and what would break if violated.
  • Open Source Repos