9

I'm tracking down an error in third party code and I narrowed it down to something along the lines of.

use libc::c_void;

pub unsafe fn foo() {}

fn main() {
    let ptr = &foo as *const _ as *const c_void;
    println!("{:x}", ptr as usize);
}

Ran on stable 1.38.0 this prints the function pointer, but beta (1.39.0-beta.6) and nightly return '1'. (Playground)

What is the _ getting inferred to and why has the behaviour changed?

I assume the correct way to cast this would simply be foo as *const c_void, but this is not my code.

2
  • I can't answer the "why has it changed", but I agree with you that the code is incorrect to start with. foo is already a function pointer, so you should not take an address to it. That creates a double reference, seemingly to a zero-sized type (thus the magic value 1). Commented Oct 16, 2019 at 15:35
  • This doesn't exactly answer your question, but you probably want: let ptr = foo as *const fn() as *const c_void; Commented Oct 16, 2019 at 16:52

2 Answers 2

3

This answer is based on the replies on the bug report motivated by this question.

Each function in Rust has its individual function item type, which is distinct from the function item type of every other function. For this reason, an instance of the function item type does not need to store any information at all – what function it points to is clear from its type. So the variable x in

let x = foo;

is a variable of size 0.

Function item types implicitly coerce to function pointer types where necessary. The variable

let x: fn() = foo;

is a generic pointer to any function with signature fn(), and thus needs to store a pointer to the function it actually points to, so the size of x is the size of a pointer.

If you take the address of a function, &foo, you are actually taking the address of a zero-sized temporary value. Before this commit to the rust repo, zero-sized temporaries used to create an allocation on the stack, and &foo returned the address of that allocation. Since this commit, zero-sized types don't create allocations anymore, and instead use the magic address 1. This explains the difference between the different versions of Rust.

Sign up to request clarification or add additional context in comments.

3 Comments

This makes sense, but I am unconvinced that it's generally desired behaviour because it is built on a precarious assumption. Within safe Rust code, there is no reason to distinguish pointers to a value of a ZST - because there is only one possibly value which is known at compile time. This breaks down once you need to use a ZST value outside of the Rust type system, such as here. It likely only affects fn item types and non-capturing closures and for those there is a workaround, as in my answer, but it's still quite a foot-gun!
Ok, I hadn't read the newer responses on the Github issue. I could get a segfault with that code but, if the code could cause a segfault then I guess the new behaviour is ok.
Great answer. @PeterHall I was thinking the same thing, and I'm still not 100% on the subject, but at least for temporaries and other stack variables, there should be no problem with putting all zero-sized values at 0x1 because the compiler makes no guarantees about stack layout, and you can't guarantee uniqueness of pointers to ZSTs anyway. This is different from, say, casting a *const i32 to *const c_void which, to my understanding, is still guaranteed to preserve the identity of the pointer.
2

What is the _ getting inferred to and why has the behaviour changed?

Each time you do a raw pointer cast, you can only change one piece of information (reference or raw pointer; mutability; type). Therefore, if you do this cast:

let ptr = &foo as *const _

since you've changed from a reference to a raw pointer, the type inferred for _ must be unchanged and is therefore the type of foo, which is some inexpressible type for the function foo.

Instead of doing that, you can directly cast to a function pointer, which is expressible in Rust syntax:

let ptr = foo as *const fn() as *const c_void;

As for why it has changed, that's hard to say. It could be a bug in the nightly build. It is worth reporting it — even if it is not a bug, you will likely get a good explanation from the compiler team about what is actually going on!

2 Comments

@MaciejGoszczycki Thanks for reporting! The responses actually did clear things up for me – I'll post an answer based on the responses there.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.