Allow coercing a `fn` value to a `&'static mut dyn FnMut` value?

Currently, if I write a function that takes a &mut dyn FnMut argument, and I have a fn value, I have to pass the reference to that fn value:

fn foo(&mut dyn FnMut()) {}

fn bar(mut f: fn()) {
    foo(&mut f)
}

I wonder if Rust can allow a fn value to be coerced to a &'static mut dyn FnMut value, so I don’t need to take the reference:

fn foo(&mut dyn FnMut()) {}

fn bar(f: fn()) {
    foo(f) // Type coercion.
}

The &mut dyn FnMut() fat pointer can be constructed from:

  • The fn value as the data pointer,
  • and a reference to a compiler generated vtable that calls the data pointer as a fn value.
2 Likes

You're essentially asking to add a coercion from fn(In) -> Out to &mut dyn FnMut(In) -> Out. That wouldn't interact nicely with other coercions. Coercions are implicit and transitive. Closures which don't capture anything coerce to the corresponding function type. That would mean that your sugar syntax would also work for non-capturing closures, but not for capturing ones, which would be inconsistent and confusing (why does changing the closure's body suddenly requires adding a reference?).

fn foo(_: &mut Fn()) {}

let f = || { };
foo(f);   // works with the proposal

let x = 5;
let f = || { x; };
foo(f);   // doesn't work, requires reference

It also doesn't make sense to construct &mut dyn FnMut() with the fn() item being the mutable data pointer. fn() items are already pointers --- function pointers to the code of the corresponding functions. Function pointers are not data pointers. Casting between a function pointer and a data pointer may be UB on some systems, and even on x86 executable code is often non-modifiable, thus it makes no sense to turn fn() pointer into a data pointer for &mut FnMut().

It would be very confusing if fn() pointer was suddenly turned into a &mut T. Does it mean that I can now modify the code of the function?

If that coercion is added, the data pointer should be a dangling &mut (), while the function pointer should be fn().

More generally, why is adding a reference even a problem? That's a tiny amount of syntax which helps to avoid ambiguities. Rust doesn't usually perform complex conversions between types, including pointers.

2 Likes

why does changing the closure's body suddenly requires adding a reference?

The same also applies to coercing a closure to a fn value:

let f: fn() = || {};

If the closure captures some outer variable, the fn() type will not work anymore, this means changing body already requires modifying other codes, so maybe it is not a big issue.

It also doesn't make sense to construct &mut dyn FnMut() with the fn() item being the mutable data pointer.

A fn value already implies it will not change any code during its execution, converting it to a mutable reference won’t change its behavior, so I think it is safe to do so.

If that coercion is added, the data pointer should be a dangling &mut () , while the function pointer should be fn().

I suspect the making the function pointer fn will not work, since the functions in the vtable should take one more pointer argument as the self argument, it does not have the same number of arguments as the original fn value, putting it there will violate the vtable’s semantic.

More generally, why is adding a reference even a problem?

It is not a problem, but I think it maybe unnecessary. We already have many coercion that simplifies coding, why not one more?

Do that ten times, and now you have a mess of the language where nothing works like it seems to. Adding implicit coercions needs a much stronger reason than "I don't want to write a &".

I can get the data pointer out of a &mut dyn FnMut() by using a pointer cast

let p: &mut dyn FnMut()  = ...;
let d: *mut () = p as *mut dyn FnMut() as *mut ();

Now I have a mutable data pointer, which is really a function pointer (so that simply producing it may be UB, as I said above). Reading it in safe way is very hard, since you don't know the function code size, and it may be non-contiguous. Writing it is very likely UB, but nothing in the code hints at that.

1 Like

This is quite sketchy though. I don't think Rust currently guarantees function pointers will always be compatible with normal memory pointers, in particular it may want to support some platform where this isn't true.

I believe it is already untrue in wasm, though I suppose Rust could be using extra indirection to get around that.

Yeah, it does seem to be the case: https://rust-lang.github.io/unsafe-code-guidelines/layout/function-pointers.html#representation.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.