Playing with Pointer Arithmetic in Rust

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 and b, two u8 (1-byte) fields,
  • i, an i32 (4-byte) field, and
  • c, another u8 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
}
}

Base struct memory representation

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
Click Here to Learn More

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.

  1. 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.
  2. Ensure Null-Terminated Strings for C Compatibility:
    If you treat memory as a C-style string, ensure it’s null-terminated. Otherwise, functions like CStr::from_ptr may read past the intended memory, potentially causing a crash or exposing sensitive data.
  3. 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’s unsafe block leaves these checks to the developer, so it’s on you to ensure that all offsets are within valid memory.
  4. Minimize unsafe Blocks:
    Where possible, confine unsafe 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.
  5. Use Rust’s High-Level Abstractions Where Possible:
    Even in unsafe 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:

  1. 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.

Click Here to Learn More

🚀 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

Read more