I'm having trouble following an example in "The Stack and the Heap"

I am new to Rust and was reading the book, and when I got to A Complex Example of section 4.1 (The Stack and the Heap).

I thought I was doing well, until I reached the sixth table, where f at address 11 has value 4. I expected it to have value 9, since when baz is called from bar, it receives e as an argument, which in the same table is shown to have value 9.

What am I missing here?

Thanks!

Ok. The chain starts with d. To write out the binding in full, we have:

// At address 9:
let d: Box<i32> = Box::new(5); /* = 2^30 - 1 */

So d is at 9, pointing to 230-1.

We than take the address of d:

// At address 10:
let e: &Box<i32> = &d; /* = 9 */

And then we call baz and the wheels fall off because of something I don't think the book has explained to that point. See, baz takes a &i32, but e is a &Box<i32>. In order to make this work, the compiler does something called "deref coercion". Basically, it "unwraps" the Box to get at the pointer inside of it. If you have &Ptr<X>, where Ptr is some kind of pointery thing, and ask for a &X, the compiler will repeatedly unwrap the layers of pointery-ness until it finds a plain old &X. So the actual chain goes &Box<i32> (= 9) → &i32 (= 230-1).

As a result, I think f should actually be 230-1: it's the same value as the box, not a pointer to the box (which would be 9. And certainly not 4, which I can't even work out how that happened at all.

Looks like a bug in the explanation.

cc @steveklabnik

1 Like

F should be 9, yes. This is a typo :frowning: It is fixed on beta: The Stack and the Heap

@DanielKeep I wasn't even taking Deref into account here, at all. Hm.

So the example was meant to have 9 but due to deref coercion it would actually be 230-1, like @DanielKeep pointed out?

I believe so, yes, but haven't tried to verify it myself.

Ok, here's a modified version that reveals the horrifying truth!

macro_rules! show {
    ($ptr:ident) => {
        println!("{} at 0x{:016x} → 0x{:016x}",
            stringify!($ptr),
            (&$ptr) as *const _ as usize,
            (&*$ptr) as *const _ as usize
        );
    };
}

fn bar() {
    let d = Box::new(5);
    show!(d);
    let e = &d;
    show!(e);

    baz(e);
}

fn baz(f: &i32) {
    show!(f);
}

fn main() {
    bar();
}

Output:

d at 0x00007fffb35c10a8 → 0x00007fee14c23010
e at 0x00007fffb35c0fa8 → 0x00007fffb35c10a8
f at 0x00007fffb35c0e50 → 0x00007fee14c23010
2 Likes

So, the Deref does kick in then. That's what I'd expect, but it does make for another wrinkle that makes the example hard to follow...

So f and d are pointing to the same thing then.

New question: what is the thing f and d are pointing to? Is it an i32 or a Box?

f is a &i32, therefore the thing at 0x00007fee14c23010 must be (or at least be indistinguishable from) an i32.

So that means that d is pointing to an i32 as well, which means that, at least in this case, at runtime a Box is just a regular reference to the thing that it contains, and all the magic was done at compile time. It that reasoning correct?

Not quite. Two things:

First, there's no guarantee that 0x00007fee14c23010 is really an i32. For example, it could be a pointer to the second field of:

struct Alloc {
    size: usize,
    value: i32,
    guard: usize,
}

where the Box implementation uses those other fields for things, and just moves its internal pointer around as neccesary. It never tells you this, and you can't really observe it, but it might be there.

Secondly, Box is not just a normal pointer at runtime: when a Box is destroyed, it has to run code to deallocate the memory it was pointing to, something normal pointers don't do.

That all having been said, by and large a Box<i32> is more or less indistinguishable from an &i32 in most of the important ways.

Which is a long-winded way of saying "yes, Box<i32> is a regular pointer... except for the details, maybe." :stuck_out_tongue:

1 Like

Sounds like this rabbit hole goes way deep, but I think I get the main points.

Thank you for the explanation!