How to explain the reference return from another function?

struct A {
  a: i32,
}

fn f1() -> A {
  let a = A { a: 1 };
  println!("&a {:p}", &a);
  a
}

fn f2() -> Vec<usize> {
  let b = vec![1, 2, 3];
  println!("&b {:p}", &b);
  b
}

fn main() {
  let a = f1();
  let b = f2();
  println!("&a {:p}", &a);
  println!("&b {:p}", &b);
}

outputs:

&a 0x16d752cac
&b 0x16d752d58
&a 0x16d752d54
&b 0x16d752d58

why reference of &a change, why reference of vector &b remains? what made vectors different?

Yeah you're right, I got it wrong.

This is a matter of the ABI, or more specifically the calling convention, for how values are passed and returned between functions. Inspecting the generated assembly (x86-64) (this can be done on the Rust Playground by picking "Show Assembly" from the top-left menu), we can see that f1() returns its value in the register eax, since that value is only 32 bits (4 bytes). Registers don't have addresses, so for &a to mean anything at all, the value must be copied out of eax into a stack location, and so it's one chosen by main() with no relationship to what f1() did.

On the other hand, for f2(), a vector's representation is { pointer, len, capacity } which does not fit in a register, and it's returned by storing it on the stack. I'm not familiar enough with x86 assembly and calling convention to completely understand it, but it looks to me like the convention here is that the caller provides a pointer (in register rdx) to a stack address chosen by main. So, f2() writes the newly created Vec into a location that main picked, and main has no reason to change it.

This is easier to see if we change f2() to something that doesn't allocate on the heap and doesn't print, so there is no possibility of allocation failure causing a panic unwind complicating the assembly:

fn f1() -> A {
  A { a: 123 }
}

fn f2() -> [usize; 3] {
  [1, 2, 3]
}

Non-optimized assembly results, with explanation:

playground::f1:
	subq	$4, %rsp           // move stack pointer to allocate stack frame
	movl	$123, (%rsp)       // write literal `A { a: 123 }` into stack
	movl	(%rsp), %eax       // move stack contents into eax
	addq	$4, %rsp           // deallocate stack frame
	retq                       // return to caller

playground::f2:
	movq	%rdi, %rax         // useless instruction that would not be present with optimization
	movq	$1, (%rdi)         // write literal 1 at location pointed to by rdi
	movq	$2, 8(%rdi)        // write literal 2, 8 bytes past rdi
	movq	$3, 16(%rdi)       // write literal 3, 16 bytes past rdi
	retq                       // return to caller

With optimization, f1 completely disappears, and f2 copies the first two array elements simultaneously through a 128-bit register, but the format in which the value is returned (written to where rdi points) is the same.

4 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.