Skip to main content Borrowing: References Are Not Plain Pointers | IoT Worker

Borrowing: References Are Not Plain Pointers

The previous article covered ownership: who owns, who frees.

Most functions do not want to take over a resource. They only want to inspect data temporarily, or mutate it temporarily. In C, this usually means passing a pointer:

void inspect(const uint8_t *buf, size_t len);
void update(uint8_t *buf, size_t len);

In Rust, this is borrowing:

fn inspect(buf: &[u8]) {}
fn update(buf: &mut [u8]) {}

Borrowing is not just “Rust pointer syntax.” It is access permission: temporarily read or write without taking ownership.

This article covers the first engineering meaning of &T and &mut T, without expanding lifetime annotations yet.

C Pointers Often Mean Temporary Use

Consider a C function:

void print_packet(const uint8_t *buf, size_t len)
{
    for (size_t i = 0; i < len; i++) {
        printf("%02x ", buf[i]);
    }
}

The caller still owns the buffer:

uint8_t packet[] = {1, 2, 3};
print_packet(packet, sizeof(packet));

The function only reads temporarily. It should not free the buffer and should not store the pointer. But these rules are not fully in the type:

  • whether it stores buf
  • whether someone mutates the buffer during the call
  • whether buf and len match
  • whether the caller can keep using the buffer after return

Rust borrowing makes the intent more explicit.

&T Is a Shared Read-Only Borrow

If a function only reads a value, it can take &T:

fn print_name(name: &String) {
    println!("{name}");
}

fn main() {
    let name = String::from("sensor");
    print_name(&name);
    println!("{name}");
}

&name borrows name. The function does not take ownership. After the call, name is still usable.

For Rust APIs, &str is usually better than &String:

fn print_name(name: &str) {
    println!("{name}");
}

For buffers, read-only borrowing is usually a slice:

fn checksum(buf: &[u8]) -> u16 {
    buf.iter().map(|b| *b as u16).sum()
}

The first model is:

&T    = temporary read-only access to a T
&[u8] = temporary read-only access to bytes
&str  = temporary read-only access to UTF-8 text

Multiple read-only borrows can coexist because they do not mutate the data.

&mut T Is an Exclusive Writable Borrow

If a function needs to mutate data, it takes &mut T:

fn reset_count(count: &mut u32) {
    *count = 0;
}

fn main() {
    let mut count = 10;
    reset_count(&mut count);
    println!("{count}");
}

&mut count temporarily lends count to the function for mutation.

For buffers:

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

&mut [u8] means exclusive writable access to a byte region.

“Exclusive” is the key. Rust does not allow a mutable borrow and other conflicting borrows of the same data to be active at the same time. That prevents the kind of confusion C code can create when one path reads while another mutates.

During a Mutable Borrow, the Original Is Restricted

Example:

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

view.push(4);
// println!("{}", data.len()); // not allowed while view is still used

From a C perspective, this is like handing out a writable pointer. During that period, you cannot also pretend you can freely access the original object.

This prevents aliasing problems:

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

a[0] = 1;
printf("%u\n", b[0]); /* who is reading, who is mutating? */

C allows this, but large programs can make data changes hard to track. Rust &mut narrows “who has write permission right now.”

Borrowing Does Not Release Resources

Borrowing is not ownership transfer.

fn inspect(data: &[u8]) {
    println!("{}", data.len());
}

fn main() {
    let packet = vec![1, 2, 3];
    inspect(&packet);
    println!("{}", packet.len());
}

inspect borrows the packet contents. It cannot release the memory or keep it as its own long-term resource.

If a function takes Vec<u8>:

fn consume(data: Vec<u8>) {
    println!("{}", data.len());
}

Then it takes ownership. After the caller passes the value, the old variable is no longer usable.

API design starts with:

only read: &[u8] / &str / &T
mutate: &mut [u8] / &mut T
take over resource: Vec<u8> / String / File

That decision matters more than memorizing syntax.

A Borrow Cannot Outlive Its Owner

This article avoids lifetime annotations, but borrowing has one basic limit: a borrow cannot live longer than the value it borrows from.

This C code is dangerous:

const char *bad_name(void)
{
    char buf[32] = "sensor";
    return buf;
}

After the function returns, buf is gone. The returned pointer dangles.

Rust rejects the equivalent. You cannot return a reference into a local String:

fn bad_name() -> &str {
    let name = String::from("sensor");
    &name
}

This does not compile. The reason is simple: name is released when the function ends, so the returned reference would point to data that no longer exists.

Lifetimes will be covered later. For now: borrowing is temporary access, and it cannot outlive the owner.

What to Keep from This Article

The first mapping is:

const T * temporary read       -> &T
const uint8_t * + len          -> &[u8]
const char * text view         -> &str or &CStr, depending on boundary
T * temporary mutation         -> &mut T
uint8_t * + len mutation       -> &mut [u8]
function takes resource        -> takes owned type, not a borrow

Borrowing answers: who may temporarily access data without taking ownership?

&T is a shared read-only borrow. &mut T is an exclusive writable borrow. They are not plain C pointer syntax replacements; they put access permission and temporary boundaries into the type.

The next article is lifetimes: how long these borrows may live, and why references to local variables cannot be returned.