Exactly, calling a closure dispatches statically to a method call (that's why each closure has a different type than any other one, even when they capture the same environment + have the same API), so there is no extra "function pointer" overhead.
The counter example, of course, is when your closure is a
dyn Fn... trait object. Only then will there be a pointer to a vtable holding a pointer to the function's code / body, since because of the type erasure it is no longer possible to store the address of the code within the static information of a type, so it needs to be present at runtime.
This, by the way, is the reason why Rust's
<F : Fn...> (f: F) will be even more performant than C taking two parameters:
_ (*f) (void * state, ...), void * state. That signature is the one of a
f: &[mut] dyn Fn...(...) -> _ argument in Rust. In other words, callbacks in C always use dynamic dispatch, whereas in Rust function genericity / templates where the address of the callback's code is hardcoded into each monomorphised function's body.