Can we have as cast *T to unsafe fn

I believe it is always sound (for any T: Sized) to cast/transmute from *const T/*mut T to an unsafe function pointer... unless we emit metadata that fn() pointers are valid function pointers, which tbh sounds familiar.

Still, I think it would be reasonable to allow as to cast from pointers-to-sized-T to function pointers in unsafe contexts. Currently any use case that type-erases function pointers is required to mem::transmute to get the type back. This is still just a pointer cast, though, so it seems reasonable to be done with as. What do you think?

(Counterargument: *const T to &T is "just a pointer cast," but cannot be done with as, requiring a prefix &*. Counter-counterargument: maybe it should be possible with as, to remove the pingpong control flow of &*(ptr as *const _)?)

If only I had a time machine, I'd argue for today's fn() to be written &fn(), and for fn() to be an unsized type representing the "actual function," rather than the function pointer. Then the answer to the above would be obvious: as cast to *const fn(), which is allowed to dangle/misalign/null, and do the normal *const T -> &T conversion. (And yes, non-'static function pointers are awkward, but that's to figure out during the RFC process in this alternate pre-1.0 timeline.)

Alas, I cannot time travel anything other than 1s/s forward in time (in the Earth's frame of reverence), so we have fn() being a guaranteed-valid function pointer.

In the process of writing the OP, I became less sure that allowing as to make this cast feels "good." I still think that this cast requiring transmute is unfortunate, but I'm not sure as is the correct tool to do the operation, and a function interface would just be transmute.

11 Likes

This might be a dumb question, but would this (&fn()) change be possible in an edition?

It should be, but would it be worth the churn? I lean towards yes. Normal function pointers could cast to &'static fn(signature) -> _, but this may require a fundemental change to the language, because fn(&'non_static i32) is not 'static

1 Like

In case anyone reads this and wonders "when on earth would you need to do that?", use cases that I can think of, off the top of my head, include

  • wrapping dlsym (Unix) / GetProcAddress (Windows)
  • the implementation of execve
  • the implementation of ld.so
  • calling arbitrary functions from a debugger
  • JIT code generators
8 Likes

Previously: In Function pointers are inconsistent with other language features, which seemed to resonate with a few people, the idea was that we should accept that fn is another way of spelling this particular & and thus potentially add the lifetime parameter after it. And also that we could defer introduction of the pointee type until extern types are more fleshed out—and making the code section to which the function pointer refers an extern type, of which you can't always calculate the layout.

Note that it can only be treated as a 'simple' pointer cast on machines of von Neumann type and not on Harvard architectures.

2 Likes

how do you make an fn(&'non_static i32) tho? is &'static for<'a> fn(&'a i32) not valid?

Like this: Rust Playground. Note that foo's parameter is fn(&'not_static i32) (unless 'a is 'static)

fn foo<'a>(callback: fn(&'a i32)) {}
fn bar(x: &i32) {}
fn call_foo() {
    foo(bar)
}

Ah.

Hm. Any convoluted example where you can't just wave it away with an &'_ or &'a?

Technically yes, as the older editions could emit &'static fn when they see fn.

Logistically, though, it'd be challenging. For practical understandability reasons we'd rather not repurpose syntax that drastically across a single edition boundary. (We can do it if mostly it works the same in both editions, but that wouldn't be the case here.

There are also some semantic questions -- this gets into truly-unsized types, since people don't want &fn to be a fat pointer carrying the "size" of the function. What's mem::size_of_val(my_function as &fn()), for example?

But these are all things that could be worked through, if someone was motivated.

3 Likes

Why would &fn be a fat pointer? It seems like it could be treated similarly to extern types, such that &fn would be a thin pointer.

2 Likes

One issue is that fn() is guaranteed non-null: Rust niche-optimizes Option<fn()> to use a null pointer for None (this is guaranteed, not just an optimization that rustc chooses to perform). So it's not always sound to cast from *mut T to unsafe fn(), because the former may be null.

It's not necessarily otherwise a validity invariant that fn() may be callable (merely a safety invariant).

3 Likes

Hmm, so *mut T as Option<unsafe fn()> instead, then? (or whatever the notation would actually be)

The answer to that question has been debated for the longest time ever though. And the arguments for both sides have been reiterated time and time again, so it's more of a lang team bandwidth issue and not being motivated issue :frowning:

I don’t know if Rust will ever support them, but there are some Harvard architecture microprocessors out there. On those platforms, function pointers refer to a different address space than data pointers, and might even have different pointer sizes.

1 Like

Rust already has tier 3 support for avr-unknown-gnu-atmega328, which has (according to Wikipedia) a "modified Harvard architecture". Admittedly, I've heard the support isn't great and that function pointers aren't actually supported.

1 Like

Tools such as the Control Flow Guard family and ARM's signed pointers require additional information bundled with the function pointer in order to call it properly. In many cases that information is embedded at the call site, but it doesn't have to be.

which implies... a from_raw_parts for function pointers?

In case it isn't obvious: wasm is a Tier 2 target and a prominent Harvard architecture. We don't need to ask ourselves if Rust ever might support such platforms. However, it is comparatively convenient as its function pointers have the same pointer size as standard pointers.

2 Likes