Zero-sized fn types: should we change behavior of transmute?


#1

So thanks to @eddyb we recently landed a fix for https://github.com/rust-lang/rust/issues/19925. However, one effect of this was that support for a somewhat common pattern of using transmute to get a fn pointer is now slated to be removed. In particular, code that transmutes from a fn item into a fn pointer, like the following, now gets a “forward compat warning”:

extern "C" fn foo(userdata: Box<i32>) {
   ...
}

let f: extern "C" fn(*mut i32) = transmute(foo);
callback(f);

Please see my write-up in https://github.com/rust-lang/rust/issues/19925 for details. Unfortunately, there is no mega-simple work-around. You can refactor things to not require the transmute, or you can add a cast, like so:

  • let f: extern "C" fn(*mut i32) = transmute(foo as extern "C" fn(_))
  • let f: extern "C" fn(*mut i32) = transmute(foo as usize) /* works too, if yucky */

However, another possibility is that we could simply remove the warnings, and continue to allow transmute from a fn item to a reified fn pointer to reify the fn pointer. It is an odd special-case, but doesn’t pose any particular problem.

I’m not entirely clear on how big the impact of this change is, but I wanted to raise the point for discussion.

cc @eddyb cc @pcwalton


#2

All usecases I’ve seen would be better served by changing the signature of the function, which has the advantage of not being insanely dangerous.

For example, we currently do not guarantee that Box<T> is passed the same as a raw void pointer, yet people define extern "C" functions that take Box<T> and then transmute them to function pointers taking *mut c_void, presumably after turning the “C types” lint off for the definition.


#3

The ffi types lint is only active when declaring prototypes for extern functions. If you define a new extern “C” function with body, it is not used.


#4

I think we need to start considering offering more guarantees in terms of the in-memory representation of Rust types. Not saying we need to go crazy, but there are a few common-sense things that people no doubt rely on widely, and I doubt we could get away with breaking them (this would largely be silent breakage, too). Some examples:

  • fn() is a pointer
  • extern "C" fn() is a pointer
  • Box<T> (where T: Sized) is a pointer
  • Option<Box<T>> (where T: Sized) is a pointer
  • &T (where T: Sized) is a pointer
  • Option<&T> (where T: Sized) is a pointer

Hmm, I am sensing a theme. :slight_smile: The ability to know that something is a pointer is pretty important for C interop, which tends to use void* as the general user-data – and you don’t really want to have to box things and then use the raw-parts APIs all the time.

We might be able to get away with reordering fields in a struct or reordering enum variants or something. I’m still unconvinced that we’ll ever be smart enough to pick a better layout in some reliable way, though.


#5

&[T; N] has also been discussed recently. Should ideally be ffi compatible as a *const T.


#6

A bit of a tangent related to function casts and insane danger, it’s a bit unfortunate that mem::transmute doesn’t warn when converting from a Fn(&Type) to Fn(&Trait). http://is.gd/XqRCQI demonstrates this doing bad things. Though warning on this would require special recognition for functions, and it could encourage a false sense of security when using the transmute footgun.


#7

Also, Fn is a trait that has nothing to do with function pointers (other than the fact that function pointers implement it).