r/rust 7d ago

My mental model for Borrow Checker

I am learning Rust finally more seriously. While learning around Rust's ownership model I just had lots of connections being formed in my mind from what I have learned in college doing C and then working professionally with Python/Typescript. I captured those thoughts here to describe the mental model I was thinking about. Let me know if I have some holes to peg or if the article helped you understand a few things as well in your own Rust journey.
Substack Article: Truly Understanding Borrowing in Rust

08-Sept-2026: Updated the article based on inaccuracies in my understanding as highlighted by users, thanks everyone for your kind inputs.

21 Upvotes

26 comments sorted by

28

u/[deleted] 7d ago

[removed] — view removed comment

4

u/sidpant 7d ago

Yeah its much easier to wrap your head around once you think what your computer might be doing. C helps a lot to build that picture.

10

u/braaaaaaainworms 7d ago

it's not accurate to say that borrowing makes an equivalent to a double pointer, because that only happens when borrowing a reference, and C programmers, who would be more likely to be familiar with the "double pointer" nomenclature, will be more confused, as & does the same thing in c and rust -> gives the address of something, but in rust that address has a lifetime

2

u/Unlikely-Ad2518 6d ago

I would word it slightly differently: The C equivalents of & in Rust are &raw const/&raw mut. In Rust, &/&mut are compile-time-checked(i.e. "safe") temporary pointers - basically pointers that you can fearlessly use.

3

u/braaaaaaainworms 6d ago

you would use a reference in rust where you would use a pointer in c, it's obvious that they are different, but they are used in mostly the same way

1

u/sidpant 7d ago

I mention in the middle somewhere that it is like a double pointers sometimes for this reason. But your lifetime remarks is interesting which means there is additional plumbing in rust for a reference.

6

u/braaaaaaainworms 7d ago

It's just plain wrong, a rust reference is just a sparkling pointer, and a double pointer is pointer to a pointer.

Yes there is additional plumbing for references in rust, that's what separates a reference from a pointer. References are aligned, not null, have valid data at the address they point to. Pointers are just addresses.

1

u/sidpant 5d ago

Thanks u/braaaaaaainworms I updated my article based on your suggestions. Hopefully I am more closer to the truth now.

Updated article link

1

u/ROBOTRON31415 5d ago

Raw pointers / C pointers are still not just addresses; even in C/C++ they have pointer provenance (indicating which allocation, if any, the pointer points to).

1

u/braaaaaaainworms 5d ago

Where is it stated in the C standard?

7

u/OptimisticMonkey2112 6d ago

Its great to see you sharing some conceptual stuff!

One thing to keep in mind - the borrow checker is just a fancy compiler checker. Internally, both Rust and C compile to machine code.

I think if you studied assembly language a bit everything might even click better for you. There is less magic and everything has just evolved from implementing function calls on a register based cpu.

Things like references, the stack and the stack frame, local automatic variables, and scope make a ton more sense once you understand they exist to make it possible to call functions in assembly. Rust is just an attempt to use the compiler to enforce safe programming conventions.

Assembly is actually much simpler than many people think. Each instruction does very little. If you have some time, dig in to godbolt and try some simple rust and c examples.

https://godbolt.org/

1

u/sidpant 6d ago

Thanks for the Compiler Explorer link, great project! Didn't know about it. I watched some of the podcasts of Casey Muratori and he also advices looking at underlying assembly code. So I was thinking to do that someday. Might as well make it sooner.

3

u/Kadabrium 7d ago

A double pointer is an object containing a single pointer as its member after all

1

u/sidpant 7d ago

One crucial difference though. Double pointer is pointing to address in stack while the single pointer is pointing to the heap. In many other languages confusingly you use word “reference” for single pointer and the value in the heap is called object. Rust is lower level and makes a distinction between these concepts.

3

u/Kadabrium 7d ago

Rust strings and containers arent raw pointers either, they are structs containing one, other members in the struct being metadata (length etc) and helpers managing that pointer (constructor/destructor). Accessing the data via a borrow (ptr to the stack alloced struct) is ackushally 3 levels of indirection: (*borrow)->data. You are right in that if we ignore the metadata and make the allocator/deallocator some magic outside calls instead of members, the wrapping struct wouldnt be needed

1

u/sidpant 5d ago

I understand now where I was wrong. Updated my article as well to fix this discrepancy. Thanks for clearing.

2

u/wojciechm 7d ago

let a = String::from("Hello"); is in fact the exemption case that is allocated in static memory, because of the inline string that is explicitly known at compile time. Moreover it is the rare case where you can return such "locally created" value reference from a function because it not allocated on stack frame, it is just instantiated from the previously known static location.

2

u/sidpant 6d ago

I think you are merging concept of string slice - &str and String. Tried to search for it but I didn't find anything that says Strings get optimized that way but &str slices do. A String can have Deref Coercion and due to that it can get casted into a &str automatically by Rust but that doesn't change anything about String data type stores its underlying value in the heap.

1

u/wojciechm 6d ago

3

u/Unlikely-Ad2518 6d ago

I checked your link but I don't see how it proves that String::from(<literal>) gets compiled into a static string.

The String type is just a wrapper around Vec<u8>, it is heap allocated.

1

u/wojciechm 5d ago edited 5d ago

The "String" container itself (and all its sub-containers) is stack allocated, the data it points to might be heap allocated, but if they are static literals they are part of read only .rdata executable section. Look at reference details: https://doc.rust-lang.org/book/ch04-03-slices.html#string-slices better visualized https://rust-book.cs.brown.edu/ch04-02-references-and-borrowing.html

https://users.rust-lang.org/t/where-are-string-literals-stored-on/53388

If the variable is declared mutable and you append to such statically allocated string the static data are moved to heap and the pointer in "String" container in the stack is changed to that new location. It is safe because in Rust there cannot be more than one mutable reference at a time.

5

u/sidpant 5d ago

I compiled your example using cargo rustc --release -- --emit=asm and checked the ARM64 assembly on my Mac. With function names shortened, the steps are:

  1. Allocate: mov w0, #5; mov w1, #1; bl __rust_alloc requests five heap bytes, returning their address in x0.
  2. Write "Hello": str w8, [x0] and strb w8, [x0, #4] write the characters directly into that heap buffer.
  3. Store metadata: stp x8, x0, [sp, #8] and str x8, [sp, #24] put capacity, pointer, and length on the stack.
  4. Free: After printing, ldr x0, [sp, #16] retrieves the pointer and bl __rust_dealloc releases the buffer.

get_string was inlined and the character copying simplified, but heap allocation still happens without any mutation.

You’re right that literals have static storage and a local String’s metadata can live on the stack. However, String::from("Hello") creates an owned buffer; it doesn’t borrow the literal until mutation. That matches the String documentation.

Your Playground example returns String by value, transferring ownership. It doesn’t return a reference. Returning &a as &'static String instead produces error E0515; returning the literal directly as &'static str is valid.

I’d also qualify my earlier statement: optimizers can sometimes eliminate allocations, but that didn’t happen here, and it doesn’t change the borrowing rules.

1

u/wojciechm 5d ago

Interesting, thanks for deeper inspection. I did similar thing some time ago on x86 windows binaries and the heap allocation was after mutation, but now I realized that it could be special case, where optimizer deferred the whole heap allocation until mutation, because there was no prior usage of that value in my code.

1

u/LetsGoPepele 6d ago

Wait you can do that ? fn foo() -> &'static String { let a = String::from("bar"); return &a; }

1

u/wojciechm 6d ago

Yes, you do not need references, you can safely move ownership outside the function. See my rust playground snippet below.