Skip to main contentMutable Borrowing | IoT Worker

Mutable Borrowing

In C, getting a T * usually means “this code may modify the value.” The hard part is that C does not stop you from keeping many access paths alive:

uint8_t *a = buf;
uint8_t *b = buf;

a[0] = 1;
printf("%u\n", b[0]);

This code is not automatically wrong. But in real projects, several pointers reading and writing the same data make state changes hard to track. Who changed it last? Who still holds an old view? Can a function modify it before the next read?

Rust &mut is not just a “writable pointer.” It means exclusive writable access:

During the same period of time, one piece of data has only one writable access path.

At first this feels restrictive to C programmers. In practice it forces the code to state who is responsible for changing the data.

&mut Temporarily Takes Write Permission

A minimal example:

fn bump(value: &mut u32) {
    *value += 1;
}

fn main() {
    let mut count = 0_u32;
    bump(&mut count);
    println!("{count}");
}

count still belongs to main. bump does not take ownership. It temporarily receives write permission.

While a mutable borrow is alive, the original variable cannot be freely accessed elsewhere:

let mut count = 0_u32;
let r = &mut count;

*r += 1;
// println!("{count}"); // not allowed if r will be used again

*r += 1;

This is not because the compiler fails to understand pointers. Rust does not allow one place to write a value while another place reads or writes that same value.

Mutable and Shared Borrows Cannot Overlap

In C, const T * and T * can point at the same memory:

const uint8_t *view = buf;
uint8_t *writer = buf;

view looks read-only, but writer can still modify the underlying data. Whether reads are stable depends on calling discipline.

Rust rejects overlapping shared and mutable borrows:

let mut data = vec![1_u8, 2, 3];

let view = &data;
let writer = &mut data; // not allowed

println!("{}", view.len());
writer.push(4);

Because view is still used, writer cannot exist at the same time. A shared borrow means not only “I will not modify it,” but also “nobody else will modify it through a mutable borrow while I use it.”

If the shared borrow is used first and then ends, mutation is fine:

let mut data = vec![1_u8, 2, 3];

println!("{}", data.len());

let writer = &mut data;
writer.push(4);

Rust tracks the actual use range of borrows. Once the read is over, write permission can be borrowed again.

Use Scopes to Shorten Mutable Borrows

C code often declares pointer variables near the top of a function and keeps passing them around. In Rust, mutable borrows should usually be short:

let mut packet = vec![1_u8, 2, 3];

{
    let payload = &mut packet[1..];
    payload[0] = 9;
}

println!("{packet:?}");

When the block ends, payload disappears and the mutable borrow ends. The code can read packet again.

This pattern is common when handling buffers. Take a writable sub-slice, modify it, and end the borrow quickly:

fn fill_header(buf: &mut [u8], len: u16) {
    let header = &mut buf[..2];
    header.copy_from_slice(&len.to_be_bytes());
}

If later code needs the whole buffer, make the mutable slice’s lifetime obvious:

fn encode(buf: &mut [u8], payload_len: u16) {
    {
        let header = &mut buf[..2];
        header.copy_from_slice(&payload_len.to_be_bytes());
    }

    let total = buf.len();
    println!("encoded {total} bytes");
}

You do not need blocks everywhere. The compiler can often end a borrow after its last use. But when the code is hard to read, a short scope is a practical engineering tool.

Do Not Mutate Container Shape While Iterating It

In C, modifying array elements while iterating is normal:

for (size_t i = 0; i < len; i++) {
    data[i] ^= key;
}

The Rust version is direct:

fn xor_in_place(data: &mut [u8], key: u8) {
    for byte in data {
        *byte ^= key;
    }
}

Here the loop gets mutable borrows of individual elements. It does not change the length or storage of the slice.

Iterating a Vec while calling push is different:

let mut data = vec![1_u8, 2, 3];

for byte in &data {
    if *byte == 2 {
        data.push(4); // not allowed
    }
}

The loop holds a shared borrow of data. push needs a mutable borrow and may reallocate the Vec. Rust does not allow these operations to overlap.

A common fix is to split the read phase from the write phase:

let mut data = vec![1_u8, 2, 3];

let need_append = data.iter().any(|byte| *byte == 2);

if need_append {
    data.push(4);
}

This is not tricking the compiler. It makes the read phase and write phase explicit.

split_at_mut Expresses Non-Overlapping Regions

Sometimes you really do need to mutate two parts of the same buffer at once. In C, you would compute two pointers:

uint8_t *left = buf;
uint8_t *right = buf + mid;

Rust cannot accept arbitrary pairs of &mut references into the same slice, because ordinary indexing does not always prove that the ranges are disjoint.

The standard library provides APIs that express non-overlap:

let mut data = [1_u8, 2, 3, 4];
let (left, right) = data.split_at_mut(2);

left[0] = 10;
right[0] = 30;

split_at_mut returns (&mut [T], &mut [T]). Its type says that the two mutable slices come from the same array but cover different ranges, so they may coexist.

Rust does not forbid modifying two pieces of data at the same time. It asks the code to prove that they are not the same piece.

Struct Fields Can Be Borrowed Separately

When two fields do not overlap, Rust can usually see it:

struct Stats {
    rx: u64,
    tx: u64,
}

let mut stats = Stats { rx: 0, tx: 0 };

let rx = &mut stats.rx;
let tx = &mut stats.tx;

*rx += 1;
*tx += 1;

rx and tx are different fields, so both mutable borrows can exist.

But if a function borrows the whole struct, Rust treats the whole object as borrowed:

fn update_rx(stats: &mut Stats) {
    stats.rx += 1;
}

let mut stats = Stats { rx: 0, tx: 0 };
let tx = &mut stats.tx;

// update_rx(&mut stats); // not allowed while tx is still used
*tx += 1;

The simplest fix is often to reorder the code, or to make the function borrow only the field it actually needs:

fn update_rx(rx: &mut u64) {
    *rx += 1;
}

let mut stats = Stats { rx: 0, tx: 0 };

update_rx(&mut stats.rx);
stats.tx += 1;

Function parameters that match the real need create smaller mutable borrows and compose better.

&mut self in Methods

Rust methods that modify the object usually take &mut self:

struct Counter {
    value: u32,
}

impl Counter {
    fn bump(&mut self) {
        self.value += 1;
    }

    fn value(&self) -> u32 {
        self.value
    }
}

Usage:

let mut counter = Counter { value: 0 };
counter.bump();
println!("{}", counter.value());

During the bump call, the whole counter is mutably borrowed. After the method returns, the borrow ends and shared access is allowed again.

If a method takes &mut self, its API says: during the call, I need exclusive permission to modify this object. That signal is clearer than a C struct Counter *self.

What to Keep

&mut is not an ordinary writable pointer. It is exclusive write permission.

When moving from C to Rust, the common adjustment is not merely adding mut. It is arranging access order:

finish reading, then write
keep mutable borrows short
use APIs like split_at_mut when two writable regions are disjoint
make functions borrow only the field or slice they need to modify

This rule affects code structure, but it gives state changes a clear owner: at any moment, the type system can see who is allowed to modify the data.

The next topic is lifetimes. Mutable borrowing answers who may read or write right now. Lifetimes answer how long a reference may remain valid.