Playing with Pointer Arithmetic in Rust
In most modern languages, pointer arithmetic is an unusual sight, often left behind in favor of safety and memory management features.
Rust, however, provides the flexibility to work with low-level memory manipulation through pointers, but always in a way that prioritizes safety by requiring unsafe
blocks for operations that could otherwise lead to undefined behavior.
While it’s generally discouraged in Rust, pointer arithmetic can be a powerful tool in certain cases.
In this article, we’ll explore how to perform pointer arithmetic in Rust, using an example adapted from C, and see why Rust handles such operations differently.
Why Use Pointer Arithmetic?
Pointer arithmetic allows direct manipulation of memory addresses. This can be useful when navigating complex data structures, interfacing with C libraries, or handling raw memory buffers. However, in Rust, pointer arithmetic is marked as unsafe
, signaling that we’re bypassing Rust’s strict safety checks. It’s a powerful tool, but one that must be used with caution.
Let’s dive into an example inspired by C-style memory manipulation to understand how to work with pointers in Rust.
Example Scenario: Direct Memory Manipulation
Consider a C-style struct that we want to manipulate using pointer arithmetic. The goal is to adjust memory around specific struct fields, imitating the behavior of pointer arithmetic in C, and then print the results.
Step 1: Defining the Struct with C-Layout
First, let’s define our structure, which contains a few fields of different types. To ensure predictable memory layout, we’ll use #[repr(C)]
, instructing Rust to arrange the struct fields in memory as a C compiler would.
#[repr(C)]
struct Base {
a: u8,
b: u8,
i: i32,
c: u8,
}
Here, we have:
a
andb
, twou8
(1-byte) fields,i
, ani32
(4-byte) field, andc
, anotheru8
field.
Using #[repr(C)]
ensures that Base
will have a predictable memory layout, essential for pointer manipulation.
Step 2: Using Pointer Arithmetic with Raw Pointers
Now, let’s perform some pointer arithmetic. Rust discourages this approach in general, but if we want to directly access and manipulate memory locations around the i
field, we need to obtain a pointer to it. We’ll use an unsafe
block to perform these operations since Rust cannot guarantee safety with raw pointers.
fn main() {
let mut mybase = Base { a: 0, b: 0, i: 0, c: 0 };
unsafe {
// Obtain a mutable raw pointer to `i`
let ptr = &mut mybase.i as *mut i32;
// Perform pointer arithmetic and write values
*ptr.offset(-1) = 1819043144; // Writes before `i`, modifying `a` and `b`
*ptr.offset(0) = 1867980911; // Writes to `i`
*ptr.offset(1) = 65811362; // Writes after `i`, affecting `c` and potential padding
}
}
Breaking Down the Pointer Arithmetic
let ptr = &mut mybase.i as *mut i32;
This line obtains a raw, mutable pointer to i
, the i32
field in mybase
. Rust’s raw pointers are similar to C’s pointers but require an unsafe
context for operations that could affect memory safety.
*ptr.offset(-1) = 1819043144;
This line performs pointer arithmetic to move to the memory immediately before i
. Since ptr
is an i32
pointer, offset(-1)
moves it back by 4 bytes. Writing to this location modifies the bytes occupied by a
and b
.
*ptr.offset(0) = 1867980911;
This writes directly to i
.
*ptr.offset(1) = 65811362;
This moves forward by 4 bytes, potentially writing into c
and any padding that might follow.
Step 3: Printing the Result
Finally, let’s try to print the data starting from a
, treating it as a C-style string.
unsafe {
let a_ptr = &mybase.a as *const u8;
let c_str = std::ffi::CStr::from_ptr(a_ptr as *const i8);
println!("{:?}", c_str);
}
Here’s what this does:
let a_ptr = &mybase.a as *const u8;
We get a pointer to a
, which is now treated as the start of our C-style string.
let c_str = std::ffi::CStr::from_ptr(a_ptr as *const i8);
This interprets the pointer a_ptr
as a null-terminated C string. Rust’s CStr
type lets us safely print C strings, which will continue until a null byte (\0
) is encountered.
println!("{:?}", c_str);
We print c_str
, which outputs the bytes starting from a
.
Expected Output
When executed, this code will print an unpredictable sequence of characters from memory a
until a null terminator is found. The characters are derived from the bytes in memory, created by setting 1819043144
, 1867980911
, and 65811362
at specific positions.
Depending on your OS, you will probably see something like “Hello World”, as for instance:
"Hello Wo\xa23\xec\x03"
Why Use unsafe
?
In Rust, unsafe
signals that we’re performing operations without Rust’s usual safety checks, such as bounds checking, null dereferencing, or data races. Pointer arithmetic falls into this category because it allows us to move around in memory without restriction. Rust trusts that we know what we’re doing, but if we make a mistake, it can lead to undefined behavior.
When to Use Pointer Arithmetic in Rust
Pointer arithmetic is an advanced feature that’s often unnecessary in typical Rust programming due to the language’s emphasis on safety.
However, there are legitimate cases where pointer arithmetic can be useful:
- Interfacing with C Libraries:
Rust’s foreign function interface (FFI) makes it possible to call C functions and pass data structures directly to C. If you’re working with C libraries that expect raw pointers, pointer arithmetic allows you to manipulate memory in ways compatible with C’s expectations. This is useful in system programming, game development, or embedded systems where you might use C libraries for performance reasons. - Working with Raw Binary Data:
When you need to parse binary data or manipulate raw memory buffers (e.g., in networking, file parsing, or graphics), direct access to memory can improve performance and reduce overhead. In these cases, pointer arithmetic can allow you to efficiently navigate large buffers without the constraints of Rust’s type safety. - Optimized Memory Management:
If you’re writing a custom allocator or need low-level control over memory (like in real-time systems or custom memory pools), pointer arithmetic provides the granular control necessary for performance-critical code. For example, in scenarios where performance is paramount, you might use raw pointers to work directly with memory, minimizing the abstractions Rust offers. - Experimental or Educational Purposes:
Pointer arithmetic can be a learning tool, helping you understand how memory is organized and managed by the system. Although Rust typically abstracts away the need for pointer arithmetic, experimenting with it can deepen your understanding of how memory works, especially if you come from a C or C++ background.
Common Pitfalls and Best Practices
Because pointer arithmetic in Rust bypasses the compiler’s safety checks, it’s important to follow best practices to minimize the risk of undefined behavior.
- Verify Memory Layout with
#[repr(C)]
:
Always use#[repr(C)]
for structs if you’re planning to perform pointer arithmetic, as it guarantees a predictable memory layout. Without this attribute, Rust might rearrange fields in a struct for optimization, making memory offsets unpredictable. - Ensure Null-Terminated Strings for C Compatibility:
If you treat memory as a C-style string, ensure it’s null-terminated. Otherwise, functions likeCStr::from_ptr
may read past the intended memory, potentially causing a crash or exposing sensitive data. - Bounds Checking:
Be very careful with bounds when performing pointer arithmetic. Moving a pointer too far before or after its target memory can cause segmentation faults or data corruption. Rust’sunsafe
block leaves these checks to the developer, so it’s on you to ensure that all offsets are within valid memory. - Minimize
unsafe
Blocks:
Where possible, confineunsafe
operations to small, isolated blocks. This makes it easier to review and reason about code, and to ensure that only the necessary parts are unchecked. - Use Rust’s High-Level Abstractions Where Possible:
Even inunsafe
code, prefer Rust’s safe abstractions (like slices) when possible, since they provide many of the benefits of direct memory access without the risks of pointer arithmetic.
Safer Alternatives to Pointer Arithmetic in Rust
Rust provides several safer alternatives that often eliminate the need for direct pointer manipulation:
- Slices and Iterators:
Rust’s slices (&[T]
or&mut [T]
) are a safer way to access contiguous memory. Slices are bounds-checked, preventing out-of-bounds access at runtime.
let arr = [10, 20, 30];
for elem in &arr {
println!("{}", elem);
}
This code iterates over arr
safely, ensuring each element access is within bounds.
2. Rust’s Memory Management Primitives:
Rust has types like Box
, Rc
, Arc
, and Vec
, which manage memory in a way that’s safe and ergonomic. These abstractions are highly optimized and eliminate the need for manual memory management in most cases.
3. The std::ptr
Module for Safer Pointer Manipulation:
Rust’s standard library provides helper functions like std::ptr::read
and std::ptr::write
to work with raw pointers more safely, reducing the chance of undefined behavior.
4. The std::slice::from_raw_parts
Function:
If you need to access raw memory as a slice, std::slice::from_raw_parts
allows you to create a slice from a pointer and a length, giving you bounds-checked access to the data.
unsafe {
let data = vec![1, 2, 3, 4];
let ptr = data.as_ptr();
let slice = std::slice::from_raw_parts(ptr, data.len());
println!("{:?}", slice); // Prints [1, 2, 3, 4]
}
5. While still in an unsafe
block, using a slice provides safer, bounds-checked access than raw pointer arithmetic.
🚀 Discover More Free Software Engineering Content! 🌟
If you enjoyed this post, be sure to explore my new software engineering blog, packed with 200+ in-depth articles, 🎥 explainer videos, 🎙️ a weekly software engineering podcast, 📚 books, 💻 hands-on tutorials with GitHub code, including:
🌟 Developing a Fully Functional API Gateway in Rust — Discover how to set up a robust and scalable gateway that stands as the frontline for your microservices.
🌟 Implementing a Network Traffic Analyzer — Ever wondered about the data packets zooming through your network? Unravel their mysteries with this deep dive into network analysis.
🌟Implementing a Blockchain in Rust — a step-by-step breakdown of implementing a basic blockchain in Rust, from the initial setup of the block structure, including unique identifiers and cryptographic hashes, to block creation, mining, and validation, laying the groundwork.
and much more!
✅ 200+ In-depth software engineering articles
🎥 Explainer Videos — Explore Videos
🎙️ A brand-new weekly Podcast on all things software engineering — Listen to the Podcast
📚 Access to my books — Check out the Books
💻 Hands-on Tutorials with GitHub code
📞 Book a Call
👉 Visit, explore, and subscribe for free to stay updated on all the latest: Home Page
LinkedIn Newsletter: Stay ahead in the fast-evolving tech landscape with regular updates and insights on Rust, Software Development, and emerging technologies by subscribing to my newsletter on LinkedIn. Subscribe Here
🔗 Connect with Me:
- LinkedIn: Join my professional network for more insightful discussions and updates. Connect on LinkedIn
- X: Follow me on Twitter for quick updates and thoughts on Rust programming. Follow on Twitter
Wanna talk? Leave a comment or drop me a message!
All the best,
Luis Soares
luis@luissoares.dev
Lead Software Engineer | Blockchain & ZKP Protocol Engineer | 🦀 Rust | Web3 | Solidity | Golang | Cryptography | Author