Okay, I’m not entirely sure what you mean by “calling return” here. For sure, there’s the return
keyword in Rust, there’s the ret
instruction in (most) assembly languages and there’s the concept of return values and “returning from a function”. Then there’s also final expressions in blocks or functions in Rust that can be used to indicate return values. There’s also a distinction between dropping a value in Rust and “calling drop
”, where the latter could mean specifically the drop
function or the Drop::drop
method (two different things yet again).
So, as you’ve asked before editing your question: Is there a relationship between “returning from a function” and the ret
instruction? Yes, but not as straightforward. Returning from a function involves a bunch of steps dictated by the calling convention of Rust functions, which is currently not specified/stable as in: it might change between Rust versions, between functions, etc... A calling convention is part of a programming language’s ABI, so this fact might be pointed out by statements such as “Rust doesn’t have a stable ABI”.
So the calling convention describes how function calls get translated into assembly. Ordinarily, functions are implemented using a call stack, and returning from a function (think “function without return value” here, e.g. fn foo(bar: Bar)
or more explicitly fn foo(bar: Bar) -> ()
) involves multiple steps like
- restoring overwritten values in registers that the caller needs
- shrink the stack so that the current function’s stack frame gets “deallocated”
- load a return address (from stack) and jump to it
That last step, “load a return address (from stack) and jump to it” is what the ret
assembly instruction does and it is necessarily (or even: by definition) that a function does.
Similarly, “returning from the function” is, by definition, the last thing that a Rust function does. So dropping local variables happens before this. We haven’t considered “return values” yet though. And all these assembly steps listed above that are part of the whole “returning from the function” business are thus the last thing that happens, too. Dropping local variables always happens before returning.
So, return values... basically, to be honest you wouldn’t really need them, you could just pass pointers as function arguments and set up the convention that the return value is written to those pointers. But there’s a cheaper way to return small values: Leave them behind in a specific register. Or multiple registers for e.g. structs. Or have no return value at all at runtime, if the value is zero-sized. (So ()
is a special case of this.)
Now when is the return value returned? Hard to answer. What counts? The time when it’s written to the return register? Or just at the same time that the function “returns” (i.e. ret
is called)? Up to interpretation; technically return values do not exist; as in: There’s nothing that distinguishes a return value from just a bunch of assembler instructions that turn out to behave as the Rust language specifies. Of course you’ll often be able to interpret “returning from a function” as something that your assembly does, especially if you know the calling convention, but it’s not set in stone.
In particular, as long as order doesn’t matter, the compiler, especially when optimizing, can do whatever it wants. First assign a return value to a register and then shrink the stack or the other way around? Who cares. You (the compiler) have figured out that calculating the return value and dropping some local variables doesn’t influence each other? Do it in the assembly in whatever order you like.
At last, on your Rust code examples. The only thing to consider here is actually the order in which local variables are dropped. For this, it is important to note that local variables include named local variables and temporary variables/values (aka “temporaries”).
The basic rules in Rust are:
- local variables are dropped at the end of the block, in reverse order of declaration
- temporaries are dropped at the end of the containing statement
now that latter part “the end of the containing statement” is not quite true, in actuality temporaries are dropped at the end of ...IT’S COMPLICATED... . And part of this “IT’S COMPLICATED” is this note
Notes :
Temporaries that are created in the final expression of a function body are dropped after any named variables bound in the function body, as there is no smaller enclosing temporary scope.
AFAICT this is to mirror the behavior on blocks. In other words, a
fn foo(x: Bar) -> Baz {
/* some code */;
/* some code */;
/* some expression */
}
behaves (AFAIK) the same as
fn foo(x: Bar) -> Baz {
{
/* some code */;
/* some code */;
/* some expression */
}
}
And blocks might behave this way since the final expression of a block is not really a “statement”. So the “containing statement” is bigger.
Anyways, for your examples this means:
pub fn demo() -> i32 {
let x = RefCell::new(Foo{ val: 10 });
x.borrow().val // this expression contains the temporary that’s holding the
// return value of `x.borrow()`.
// By the rule in the “note” above, this temporary has to
// be dropped AFTER x is dropped. Which doesn’t work, since
// the destructor of the `std::cell::Ref` in this temporary has
// access to and modifies the `RefCell` itself, which would
// result in a use-after-free.
}
On the other hand
pub fn demo2() -> usize {
let x = "gui".to_owned();
let y = &x;
y.len() // there’s (potentially / I’m not sure) a temporary here
// that’s holding a copy of `y`, which *is* a reference to `x`.
// But since references such as `y` don’t have any destructor
// it wouldn’t be problematic to “drop” y after x anyways.
}
also note that in both cases the actual function calls for evaluating the return value, i.e. .borrow()
or .len()
happen before any local variable or temporary (from the return value expression) is dropped.
For both these examples, how “returning” in the control-flow sense works has not really got anything to do with this, and the “returning from the function” happens after any local (named or temporary) variable is dropped, anyways.