25.3 Raw Pointers: *const T
and *mut T
Analogous to C pointers, Rust provides two raw pointer types:
*const T
: A raw pointer to data of typeT
, indicating the pointer itself does not grant permission to mutate the data through it. Roughly corresponds to C’sconst T*
.*mut T
: A raw pointer to data of typeT
, indicating the pointer may be used to mutate the data. Roughly corresponds to C’sT*
.
The const
or mut
primarily signifies the intended use and type system interaction, not necessarily the absolute immutability of the underlying memory (e.g., memory behind a *const T
might still be mutated through other means, like an UnsafeCell
or another *mut T
, if done carefully).
Raw pointers differ significantly from Rust’s references (&T
, &mut T
):
- They can be null.
- They are not guaranteed to point to valid memory (could be dangling or uninitialized).
- They do not have compiler-enforced lifetime constraints.
- They can alias (e.g., multiple
*mut T
can point to the same location), but using them must still respect Rust’s aliasing rules to avoid UB (discussed below). - They require explicit dereferencing using the
*
operator, which is an unsafe operation. - They do not implement automatic dereferencing.
25.3.1 Creating and Using Raw Pointers
Creating raw pointers is safe; the unsafety primarily lies in dereferencing them. Raw pointers can be obtained in several ways:
-
From references: References can be explicitly cast to raw pointers. For a variable
x
of typeT
,&x as *const T
creates a const pointer, and&mut x as *mut T
creates a mutable pointer. This explicit cast is a common way to convert a reference when its lifetime and validity are known but a raw pointer is needed (e.g., for FFI or certain low-level operations). -
Using Raw Borrow Operators (
&raw const
and&raw mut
): Introduced in Rust 1.82, these operators allow for the direct creation of raw pointers (*const T
and*mut T
respectively) from a “place” (a memory location, such as a variable or a field of a struct).The key advantage of
&raw const expr
and&raw mut expr
is that they do not first create an intermediate Rust reference (&T
or&mut T
). This is crucial because Rust references come with strict guarantees: they must always point to valid, initialized, and properly aligned memory. If you were to create a reference to memory that does not uphold these invariants (e.g., an unaligned field in a#[repr(packed)]
struct, or uninitialized memory), even if immediately cast to a raw pointer, it could trigger Undefined Behavior (UB) due to the invalid reference creation itself.For C-programmers, these operators provide a direct analogy to C’s address-of operator (
&
) applied to variables, allowing you to obtain a raw memory address without the implicit safety checks associated with Rust references. This is particularly useful inunsafe
blocks for low-level memory operations, FFI, or when interacting with memory layout that Rust’s reference system might otherwise consider invalid. -
From data structures: Many standard library types that manage contiguous data, such as slices (
[T]
),Vec<T>
, andString
, provide methods likeas_ptr()
(to get a*const T
) andas_mut_ptr()
(to get a*mut T
from a mutable instance). For example,my_slice.as_ptr()
returns a*const T
to the beginning of the slice’s data. These methods are the idiomatic way to obtain pointers to the internal buffer of such types, as shown in the pointer arithmetic examples later. -
From memory addresses: An integer representing a memory address can be cast to a raw pointer (e.g.,
address_usize as *const T
). This is highly platform-dependent and typically used for memory-mapped I/O or interacting with hardware. The programmer must ensure the address is valid, properly aligned, and points to memory of typeT
. -
Null pointers: The
std::ptr
module providesnull()
to create a*const T
null pointer andnull_mut()
for a*mut T
null pointer (e.g.,let p_null: *const T = std::ptr::null();
). -
From FFI calls: Functions defined in
extern
blocks (especially C functions) may return raw pointers.
Passing and storing raw pointers is generally safe. Comparing raw pointers for equality (e.g., using std::ptr::eq
or the ==
operator) is also safe and compares their addresses. Ordinal comparison (<
, >
, etc.) on raw pointers is defined and performs a byte-wise comparison of the addresses; however, the resulting order may be inconsistent or platform-dependent if the pointers do not originate from, or point within, the same allocated object.
#[repr(packed)] // For demonstration: a struct with potentially unaligned fields. struct PacketHeader { version: u8, flags: u8, // data_length might be unaligned if accessed as u16 from specific byte offsets. data_length: u16, } fn main() { let mut data = 10; // Safe: Create raw pointers from references using explicit casts. let p_const: *const i32 = &data as *const i32; let p_mut: *mut i32 = &mut data as *mut i32; println!("--- Creating Pointers from References ---"); println!("Address from const reference: {:p}", p_const); println!("Address from mut reference: {:p}", p_mut); println!("Value from const reference: {}", unsafe { *p_const }); println!("Value from mut reference: {}", unsafe { *p_mut }); // Safe: Create a raw pointer from an address (usize). Caution: validity unknown. let address = 0x1234_5678_usize; let p_addr: *const i32 = address as *const i32; println!("\n--- Creating Pointer from Raw Address ---"); println!("Address from integer literal: {:p}", p_addr); // Safe: Create and store a null pointer. let null_ptr: *const i32 = std::ptr::null(); println!("\n--- Creating Null Pointer ---"); println!("Null pointer address: {:p}", null_ptr); // # Using Raw Borrow Operators (&raw const, &raw mut) since Rust 1.82 # println!("\n--- Using Raw Borrow Operators (&raw const, &raw mut) ---"); let mut header = PacketHeader { version: 1, flags: 0b1010_1010, data_length: 512, }; // For C programmers, this is akin to `&header.data_length;` // In Rust, using `&header.data_length` directly would be UB if `data_length` // is unaligned due to `#[repr(packed)]` and the CPU requires alignment for u16 // access. The `&raw const` operator avoids this UB by not creating an // intermediate Rust reference. let raw_len_ptr: *const u16 = &raw const header.data_length; println!("Address of data_length (raw const): {:p}", raw_len_ptr); unsafe { // Accessing the value through the raw pointer. // The safety contract requires ensuring the pointer is valid and aligned. // Given this specific scenario with #[repr(packed)] it must be handled carefully. println!("Value of data_length (raw const): {}", *raw_len_ptr); } // We can also get a mutable raw pointer. let raw_flags_ptr: *mut u8 = &raw mut header.flags; println!("Address of flags (raw mut): {:p}", raw_flags_ptr); unsafe { println!("Original flags: {:#b}", *raw_flags_ptr); *raw_flags_ptr = 0b0101_0101; // Mutate through the raw pointer println!("Modified flags: {:#b}", *raw_flags_ptr); } println!("Header flags after modification: {:#b}", header.flags); // Obtaining a pointer from a slice using as_ptr() is shown in section 25.3.2. // For example: // let numbers = [1, 2, 3]; // let slice_ptr: *const i32 = numbers.as_ptr(); // println!("Address from slice.as_ptr(): {:p}", slice_ptr); }
You might wonder if 0 as *const T
could also be used to create a null pointer, similar to how (T*)0
is used in C. Indeed, Rust defines that casting a literal 0
to a pointer type produces a null pointer, and the std::ptr::null()
function is documented as being equivalent to 0 as *const T
for creating a pointer to address zero. However, using std::ptr::null()
and std::ptr::null_mut()
is generally preferred in Rust. These functions clearly convey the intent to create a null pointer and promote consistency by using the dedicated API from the std::ptr
module, which centralizes pointer-related utilities. While currently resulting in the same zero-address pointer on common platforms, the explicit functions are the idiomatic choice.
Dereferencing a raw pointer (*p
) to access the pointed-to data is unsafe, requiring an unsafe
block, because the pointer’s validity (i.e., being non-null, aligned, pointing to initialized memory of the correct type, and not dangling) cannot be guaranteed by the compiler.
fn main() { let mut num = 5; let p_const = &num as *const i32; let p_mut = &mut num as *mut i32; // Unsafe: Dereferencing requires an unsafe block. unsafe { println!("Reading via *const T: {}", *p_const); // Writing requires a *mut T. *p_mut = 10; println!("Reading via *mut T after write: {}", *p_mut); } println!("Final value of num: {}", num); // num is now 10 // Example: Dereferencing an arbitrary address is highly likely UB. let invalid_addr = 0x1 as *const i32; // unsafe { println!("{}", *invalid_addr); } // Likely crash or incorrect behavior }
Important Note for C/C++ Programmers: Although raw pointers seem to bypass Rust’s borrowing rules (e.g., allowing multiple *mut T
to the same data), Rust still imposes strict aliasing rules, even within unsafe
code. The exact rules are formalized by models like Stacked Borrows or Tree Borrows (these models are still evolving). Violating these rules—for instance, writing through a *mut T
while a shared reference &T
to the same location exists and is considered “live”—is undefined behavior. This is stricter than C’s aliasing rules in some respects. Tools like Miri are invaluable for detecting such violations.
25.3.2 Pointer Arithmetic
Raw pointers support arithmetic to calculate new pointer addresses based on existing ones. Common methods include add(count)
for moving forward (with count
as usize
), sub(count)
for moving backward (with count
as usize
), and offset(count)
which takes a signed isize
for count
and can move in either direction. The offset
method is often preferred for its versatility in handling both positive and negative displacements with a single method, as it directly accepts an isize
argument. These operations adjust the pointer address by count * std::mem::size_of::<T>()
bytes, similar to C pointer arithmetic.
All these fundamental pointer arithmetic methods (add
, sub
, offset
) are unsafe
functions. This is because even though the address calculations themselves typically handle overflow by wrapping (producing some address value rather than panicking the way standard integer +
might in debug mode), the resulting pointer might be misaligned, point outside allocated memory regions, or be otherwise invalid to dereference. The unsafe
contract means the caller is responsible for ensuring that the arguments (like count
) are valid in the current context and that any subsequent use of the calculated pointer, especially dereferencing, is safe.
fn main() { let numbers = [10i32, 20, 30, 40, 50]; let start_ptr: *const i32 = numbers.as_ptr(); // Pointer to the first element. unsafe { // Using offset(): move pointer to the third element (index 2). // 'offset' takes an isize, so it can be positive or negative. let third_elem_ptr = start_ptr.offset(2); println!("Third element (via offset(2)): {}", *third_elem_ptr); // Outputs 30 // Using add(): move pointer to the second element (index 1). // 'add' takes a usize, for forward movement. let second_elem_ptr = start_ptr.add(1); println!("Second element (via add(1)): {}", *second_elem_ptr); // Outputs 20 // Using offset() for backward movement. let first_elem_again_ptr = third_elem_ptr.offset(-2); println!("First element (via offset(-2)): {}", *first_elem_again_ptr); // 10 // Calculating the difference between pointers (in units of T). // 'offset_from' is also an unsafe method. let diff = third_elem_ptr.offset_from(start_ptr); println!("Offset difference (third_elem_ptr from start_ptr): {}", diff); // 2 // Creating a pointer outside the bounds is possible with these methods. // Dereferencing it is Undefined Behavior. // let invalid_ptr = start_ptr.offset(10); // Points beyond the allocation // println!("{}", *invalid_ptr); // Undefined Behavior! } }
Pointer arithmetic should be used with extreme caution. Ensure that any pointer you dereference remains within the bounds of a single valid memory allocation and is properly aligned. Safer alternatives, like slice indexing (numbers[i]
) or iterators, should always be preferred when applicable.
For scenarios where pointer arithmetic might overflow and wrapping behavior is explicitly desired for the address calculation itself, Rust provides wrapping_add(count)
, wrapping_sub(count)
, and wrapping_offset(count)
. Unlike their non-wrapping counterparts, these wrapping_*
methods are safe to call (they are not unsafe fn
). This is because they guarantee that the pointer address calculation itself will wrap on overflow (consistent with twos-complement arithmetic) instead of panicking or causing other undefined behavior from the arithmetic operation itself. This can be useful in certain low-level algorithms where pointer values are treated more like integers that are allowed to wrap around the address space.
However, it’s crucial to remember that while calling these wrapping_*
methods is safe, dereferencing the resulting pointer still requires an unsafe
block and is only permissible if the pointer is valid (non-null, aligned, pointing to accessible and correctly typed memory, etc.). Using a wrapped pointer that is invalid for dereferencing will lead to undefined behavior.
25.3.3 Fat Pointers
Raw pointers to Dynamically Sized Types (DSTs), such as slices ([T]
) or trait objects (dyn Trait
), are “fat pointers.” They consist of two components: the pointer to the data and associated metadata.
*const [T]
,*mut [T]
: Contain the address of the first element and the number of elements (length).*const dyn Trait
,*mut dyn Trait
: Contain the address of the object data and the address of its virtual method table (vtable).
Converting between thin pointers (*const T
) and fat pointers usually requires specific functions like std::slice::from_raw_parts
or std::slice::from_raw_parts_mut
, which are often unsafe
.