Skip to main content Ownership: Who Owns, Who Frees | IoT Worker

Ownership: Who Owns, Who Frees

C code already has ownership.

Whenever memory needs free, a file needs fclose, or a handle needs release, there is a question: who is responsible for releasing it?

C’s problem is not the absence of ownership. The problem is that ownership is usually not in the type. It lives in function names, comments, conventions, and team experience. Rust ownership turns those conventions into rules the compiler can check.

This article covers one core question: who owns, who frees, and why a moved value cannot be used again.

C Ownership Is Convention

Consider a common C API:

uint8_t *make_packet(size_t *out_len);
void free_packet(uint8_t *packet);

The caller must know:

  • whether the returned pointer must be freed
  • whether to use free or free_packet
  • whether failure returns NULL
  • whether out_len is valid on failure
  • whether the pointer can be used after release

None of that is in uint8_t *.

Now consider:

void send_packet(const uint8_t *packet, size_t len);

It looks like the function only borrows data. But does it store the pointer? Can the caller free the buffer after the call returns? If the documentation is unclear, the caller guesses.

Rust tries to make these boundaries explicit.

Vec Owns a Buffer

In Rust, a function that creates and returns a buffer often returns Vec<u8>:

fn make_packet(payload: &[u8]) -> Vec<u8> {
    let mut packet = Vec::new();
    packet.push(1);
    packet.push(payload.len() as u8);
    packet.extend_from_slice(payload);
    packet
}

Vec<u8> means the caller owns the buffer.

let packet = make_packet(b"hello");
println!("{}", packet.len());

When packet leaves scope, its memory is released. The caller does not need to find a matching free_packet.

The first model is:

owning Vec<u8> = owning this dynamically allocated byte buffer
variable leaves scope = releases the resources it owns

That is the ownership rule behind Drop.

move Transfers Ownership

If a Vec<u8> is passed to a function that takes Vec<u8>, ownership moves:

fn consume_packet(packet: Vec<u8>) {
    println!("len = {}", packet.len());
}

fn main() {
    let packet = make_packet(b"hello");
    consume_packet(packet);

    // println!("{}", packet.len()); // cannot use here
}

After consume_packet(packet), ownership has moved into the function parameter. When the function ends, the parameter leaves scope and the buffer is released.

The original variable cannot be used again. That prevents one of C’s common bugs:

free(packet);
printf("%zu\n", len);      /* fine */
printf("%u\n", packet[0]); /* use-after-free */

Rust does not allow access through the old variable after ownership has moved away.

This is not syntax preference. It prevents the case where release responsibility is no longer yours, but you still use the resource.

Copy Types Are Different

Not every value becomes unusable after being passed to a function.

Simple values such as integers and booleans usually implement Copy:

fn print_id(id: u32) {
    println!("{id}");
}

fn main() {
    let id = 42u32;
    print_id(id);
    println!("{id}"); // still usable
}

The u32 value is copied. The original id remains usable.

But Vec<u8>, String, and File own resources and do not copy automatically by default. Copying them is not just copying a few bytes; it would raise the question of which value releases the same resource.

The first rule is:

u32 / bool and other simple values: passing usually copies
Vec / String / File and other resource values: passing usually moves ownership

clone Explicitly Copies Resources

If you really need two owned copies, use clone:

let a = String::from("device");
let b = a.clone();

println!("{a}");
println!("{b}");

clone does not bypass ownership. It creates new data. a and b each own their contents and release them separately.

This is like a deep copy in C:

char *b = strdup(a);

Do not use clone as the default way to silence compiler errors. It may allocate, copy data, and change performance behavior. If the function only reads, borrow instead.

Borrow When You Only Use

If a function only reads a packet, it should not take Vec<u8>. It should take a slice:

fn inspect_packet(packet: &[u8]) {
    println!("len = {}", packet.len());
}

fn main() {
    let packet = make_packet(b"hello");
    inspect_packet(&packet);
    println!("{}", packet.len()); // still usable
}

&packet borrows the data. The function can inspect it, but does not own it and does not release it.

This is close to the intent of C const uint8_t *buf, size_t len, but Rust makes the borrowing boundary clearer.

This article does not expand borrowing rules yet. For API design, keep the first decision:

function takes over resource: take Vec<T> / String / File
function only reads: take &[T] / &str
function mutates in place: take &mut [T] or &mut T

Return Values Also Transfer Ownership

In C, returning a pointer often means “caller must free”, but that may be unclear:

char *build_name(void);

In Rust, returning an owned type is explicit:

fn build_name() -> String {
    String::from("device-01")
}

The caller receives the String and owns it. It can pass it on, or let it release at the end of scope.

let name = build_name();
println!("{name}");

If a function returns a borrow such as &str, that means it did not transfer ownership. That touches lifetimes and will be covered later.

For now: returning String, Vec<u8>, or other owned types transfers ownership to the caller.

What to Keep from This Article

The first mapping is:

who frees / closes             -> whoever owns the value
return malloc-allocated ptr    -> return Vec<T> / String / Box<T>
function takes resource        -> parameter takes owned type
function only reads            -> parameter takes a borrow, such as &[u8] / &str
manual deep copy               -> clone
use after free                 -> moved value cannot be used again

Ownership is not a new problem invented by Rust. C projects have always had ownership; they just maintain it by convention.

Rust changes the enforcement: moving ownership makes the old variable unusable, leaving scope releases resources, and copying resources must be explicit.

The next article is borrowing. Ownership answers “who owns”; borrowing answers “who may temporarily read or write without taking ownership.”