Capturing globals in function pointers through indirection

Is there anything in type system fundamentally preventing global variables passed as parameters through enclosing function to be captured by function pointers fn(...) -> ...?
I know that current implementation forbids such behavior, however given a very first line of static definition

A static item is a value which is valid for the entire duration of your program (a 'static lifetime).

It makes me wonder why i can't have a program like this:


static ZERO_ADD: i32 = 0;

static ADD: fn(&i32, &i32) -> i32 = |a, b| a + b;

fn binop_fold_fn_ptr(op: &'static fn(&i32, &i32) -> i32, zero: &'static i32) -> fn(&Vec<i32>) -> i32 {
    |rest: &Vec<i32>| match &rest[..] {
        [a, b] => op(a, b),
        [] => panic!("zero arguments"),
        _ => rest.iter().fold(*zero, |acc, x| op(&acc, x)),
    }
}

It won't compile due to

error[E0308]: mismatched types

closures can only be coerced to fn types if they do not capture any variables

Capturing of op and zero params leads to this error no matter of their 'static lifetime.
So I either have to use ZERO_ADD and ADD directly in the fn pointer body or switch to closure and return Box<dyn Fn(&Vec<i32>) -> i32 + Send + Sync + 'static>.

I'm unable to realize any fundamental blockers for the compiler to accept such programs.

Full code at rust playground.

The problem is not with lifetimes, the problem is with what a function pointer is. A function pointer is just that – a function pointer. It's not a function pointer plus some other data. There's no place to store the captured data inside it. What you are describing is not a bare function pointer, it's exactly a full-fledged closure.

3 Likes

As far as I understand ZERO_ADD and ADD addresses are compiled into executable segment where returned fn pointer points to.

static ZERO_ADD: i32 = 0;
static ADD: fn(&i32, &i32) -> i32 = |a, b| a + b;
fn binop_fold_fn_ptr() -> fn(&Vec<i32>) -> i32 {
    |rest: &Vec<i32>| match &rest[..] {
        [a, b] => ADD(a, b),
        [] => panic!("zero arguments"),
        _ => rest.iter().fold(ZERO_ADD, |acc, x| ADD(&acc, x)),
    }
}

How it is different when additional layer of indirection through enclosing function parameters is added? I'm speaking precisely about references to statics with 'static lifetime, not the arbitrary parameters.

Again, lifetimes have nothing to do with this. A reference to a 'static item is still a reference, it's still additional data that has to be stored somewhere.

If you invoke whatever through a function pointer, then that might be dynamic, as far as the compiler is concerned. The bad_binop_fold_fn_ptr() function would have to work no matter what op is pointing to. It's not statically knowable (based on only function-local analysis) that it can only be X or Y function that you happen to have specified.

This is not the case when you directly refer to an item, because then it is 100% sure that the item is whatever it is, it can't be anything else, and so dynamism doesn't need to be accounted for.

2 Likes

It seems that you expect that references with 'static lifetime should somehow always be "constants" in the program, so that a closure which captures only such variables can avoid storing those references entirely by generating a unique function pointer type for every combination of arguments.

But this isn't true:

fn makes_statics() -> &'static i32
{
  Box::leak(Box::new(0))
}

Surely a function pointer compiles to a single address, nothing more. So what are the addresses of the variables below?

let a = binop_fold_fn_ptr(&ADD, makes_statics());
let b = binop_fold_fn_ptr(&ADD, makes_statics());
2 Likes

Thank you @H2CO3 and @yuriy0 , pointing to difference between 'static lifetime and "constants" helped me.