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