I was experimenting with function pointer magic in Rust and ended up with a code snippet which I have absolutely no explanation for why it compiles and even more, why it runs.>
This program never actually constructs a function pointer at all- it always invokes foo
and those two closures directly.
Every Rust function, whether it's a closure or a fn
item, has a unique, anonymous type. This type implements the Fn
/FnMut
/FnOnce
traits, as appropriate. The anonymous type of a fn
item is zero-sized, just like the type of a closure with no captures.
Thus, the expression create(foo)
instantiates create
's parameter F
with foo
's type- this is not the function pointer type fn()
, but an anonymous, zero-sized type just for foo
. In error messages, rustc calls this type fn() {foo}
, as you can see this error message.
Inside create::
(using the name from the error message), the expression caller::
forwards this type to caller
without giving it a value of that type.
Finally, in caller::
the expression closure()
desugars to FnMut::call_mut(closure)
. Because closure
has type &mut F
where F
is just the zero-sized type fn() {foo}
, the 0
value of closure
itself is simply never used1, and the program calls foo
directly.
The same logic applies to the closure || println!("Okay...")
, which like foo
has an anonymous zero-sized type, this time called something like [closure@src/main.rs:2:14: 2:36].
The second closure is not so lucky- its type is not zero-sized, because it must contain a reference to the variable val
. This time, FnMut::call_mut(closure)
actually needs to dereference closure
to do its job. So it crashes2.
1 Constructing a null reference like this is technically undefined behavior, so the compiler makes no promises about this program's overall behavior. However, replacing 0
with some other "address" with the alignment of F
avoids that problem for zero-sized types like fn() {foo}
, and gives the same behavior!)
2 Again, constructing a null (or dangling) reference is the operation that actually takes the blame here- after that, anything goes. A segfault is just one possibility- a future version of rustc, or the same version when run on a slightly different program, might do something else entirely!