Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Tracing Stack and Heap Values in Rust

Hello! In the first module, you wrote functions and worked with values such as integers, arrays, and structs. This module asks a new question: where does a value live while the program runs? That question will help explain Rust’s rules for ownership and borrowing later on.

Today you’ll trace a small program’s stack frames, heap allocation, and string data. The goal is not to memorize machine addresses. It is to look at a Rust value and identify which parts have a fixed size, which parts need storage allocated at runtime, and what happens when a function returns.


Two kinds of storage

The stack holds function-call frames. A frame provides space for a function’s local values and other information needed for the call. Frames follow a last-in, first-out pattern: if main calls measure, the measure frame is the first of those two frames to finish.

The heap provides storage requested while the program runs. Unlike a stack frame, a heap allocation can hold data whose required size is determined at runtime. Allocating it involves finding a suitable region of memory and keeping track of that region until it can be released.

Watch these short animations before reading the more precise account below.

Rust Memory Model Explained: Stack vs Heap (with Animations)

Watch “Rust Memory Model Explained: Stack vs Heap (with Animations)” by Max Taylor for a visual account of function frames and heap allocation.

Watch stack frames, including the nested function-call example. Then watch heap allocation. Focus on the different questions each region answers: where does this function keep its local data, and where can a program obtain storage at runtime?

The Rust book gives the underlying model. Its description of stack storage applies to values with a known, fixed size; it does not mean that every fixed-size value must be on the stack.

What is Ownership? - The Rust Programming Language

Read “The Stack and the Heap” in The Rust Programming Language for the distinction between a stack frame and a heap allocation.

Under “The Stack and the Heap,” begin with the paragraph starting “Both the stack and the heap are parts of memory available to your code to use at runtime.” Read the explanation through the final paragraph about function calls. Focus particularly on why a fixed-size pointer can be kept in a frame even when the data it identifies is on the heap.

A useful rule for tracing code is: locate the value, then locate any data it points to. Those may be in different places. For example, an integer local can be represented entirely within a function’s frame. A String local has a fixed-size part in the frame, but its text is normally in a separate heap allocation.


A String has two locations to trace

Consider:

let mut name = String::from("rust");
name.push_str(" api");

A String value contains three pieces of information: a pointer to its text buffer, the length of the text currently stored there, and the buffer’s capacity. This fixed-size information can reside in a stack frame. The buffer containing the UTF-8 bytes of rust api resides on the heap.

Length and capacity are measured in bytes. After the append, the length here is 8 bytes. The capacity is at least 8, but you should not guess its exact value: the implementation may reserve extra room. Appending might also require a new, larger heap allocation if the old buffer cannot hold the additional text.

What is Ownership? - The Rust Programming Language

Continue in The Rust Programming Language to see why growable text needs an allocation and what, precisely, is stored in a String value.

In “Memory and Allocation,” start at the paragraph beginning “In the case of a string literal, we know the contents at compile time.” Read through the scope example; stop before “Variables and Data Interacting with Move.” Then, in “Variables and Data Interacting with Move,” find the paragraph beginning “Take a look at Figure 4-1.” Read the layout description through the definitions of length and capacity. Leave the later discussion of assignment and moves for the next lesson.

The supplied diagram is useful if you read its right-hand side as a picture of a value and the bytes it points to:

The right-hand examples show fixed-size `String` and `&str` values pointing to text stored elsewhere. The `String` points to a growable heap buffer; the pictured `&str` points to a fixed-size region of bytes. The diagram’s left-hand labels are shorthand, not a rule that every `String` is wholly on the heap or every `&str` points to heap data.

That last distinction matters. A string literal such as "api" has type &str. You can think of the &str value as a pointer and a length: it describes some text but does not own a growable buffer. The bytes of a string literal are stored with the compiled program, not in a new heap allocation each time the literal is used. A &str can also point to bytes inside a String’s heap buffer. We’ll study the borrowing rules that make this safe in later lessons; for now, just trace the view separately from the bytes it describes.


Trace a complete program

Here is a small program using types you have already encountered:

fn measure(label: &str) -> usize {
    let overhead: usize = 2;
    label.len() + overhead
}

fn main() {
    let count: i32 = 7;
    let points: [i32; 3] = [4, 5, 6];
    let literal: &str = "api";

    let mut name = String::from("rust");
    name.push_str(" api");

    let size = measure(name.as_str());
    println!("{count} {points:?} {literal} {name} {size}");
}

Read it in snapshots rather than trying to imagine all memory at once.

Before measure is called, main has a stack frame. In the straightforward memory model, count and the three elements of points fit within fixed-size local storage. literal is a fixed-size view; its "api" bytes belong to the program’s literal data, not to main’s stack frame. name has its pointer, length, and capacity in main’s frame, while its rust api bytes are in a heap buffer.

While measure runs, it has its own stack frame. name.as_str() supplies a &str view of name’s text. The parameter label describes the same eight bytes in the heap buffer; calling measure does not create a second copy of that text. The local overhead is a fixed-size usize. The function calculates 8 plus 2 and returns the usize value 10.

After measure returns, its frame is gone. The size value in main is 10, and main can still use name: the heap text was not part of the temporary measure frame. Once main finishes and name goes out of scope, Rust arranges for the allocation owned by the String to be released. The exact ownership rule behind that cleanup is a topic for the next lesson.

Item What you trace Where the underlying bytes are
count, points, size, overhead Fixed-size values in their respective function frames No separate heap buffer is needed
literal A &str view in main’s frame "api" is literal data in the program
name Pointer, length, and capacity in main’s frame rust api is in a heap buffer
label A &str view for the measure call It describes name’s existing heap bytes

This is a conceptual tracing model, not a promise about each machine instruction. An optimizing compiler may keep a small local in a register or inline a function. Conversely, a fixed-size value can be placed in heap storage as part of a heap-allocated container. Stack versus heap tells you about storage in the model; it is not a type label that permanently attaches to every integer or string.


What to carry forward

When tracing a Rust program, separate three questions: What is the value’s fixed-size representation? Where is any data it points to? Which function call or allocation keeps that storage available? For an integer local, the first question may be enough. For a String or &str, answering only “stack” or “heap” misses an important part of the picture.

Next, you’ll use this layout to predict what assignment and function calls do to values—and why copying an integer is different from transferring a String.

Can't find a good explanation? Sign up and we'll make it for you

Sign up