Why does Rust allow calling functions via null pointers?
The type of fn foo() {...}
is not a function pointer fn()
, it's actually a unique type specific to foo
. As long as you carry that type along (here as F
), the compiler knows how to call it without needing any extra pointers (a value of such a type carries no data). A closure that doesn't capture anything works the same way. It only gets dicey when the last closure tries to look up val
because you put a 0
where (presumably) the pointer to val
was supposed to be.
You can observe this with size_of
, in the first two calls, the size of closure
is zero, but in the last call with something captured in the closure, the size is 8 (at least on the playground). If the size is 0, the program doesn't have to load anything from the NULL
pointer.
The effective cast of a NULL
pointer to a reference is still undefined behavior, but because of type shenanigans and not because of memory access shenanigans: having references that are really NULL
is in itself illegal, because memory layout of types like Option<&T>
relies on the assumption that the value of a reference is never NULL
. Here's an example of how it can go wrong:
unsafe fn null<T>(_: T) -> &'static mut T {
&mut *(0 as *mut T)
}
fn foo() {
println!("Hello, world!");
}
fn main() {
unsafe {
let x = null(foo);
x(); // prints "Hello, world!"
let y = Some(x);
println!("{:?}", y.is_some()); // prints "false", y is None!
}
}
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::<fn() {foo}>
(using the name from the error message), the expression caller::<F>()
forwards this type to caller
without giving it a value of that type.
Finally, in caller::<fn() {foo}>
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!