Mehdi Akiki
Rust Failure Atlas / FFI and targets

RFA-571 · Case file with fixtures · Case 543 of 694 · Compiler evidence

A Rust Function Item Must Coerce Before Pointer-Level Conversion

A named function has a unique zero-sized item type that normally coerces safely to a function pointer. Make that coercion explicit before low-level pointer work.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all Rust targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
Compile-time function identity was mistaken for the runtime address representation produced by the language's normal function-item coercion.
First discriminating check
Assign the function item to an explicitly typed function pointer and verify signature, safety, ABI, and library lifetime before low-level use.

The name of a Rust function is callable, but its type is not already the ordinary fn() pointer type written in an API. Every named function has a distinct, zero-sized function-item type. Rust normally converts that item to a function pointer automatically when context asks for one.

The failing fixture skips that conversion and gives the item directly to transmute. Rust 1.98.1 reports E0591 because the source is zero-sized while the requested pointer has an address-sized representation.

Function identity can exist entirely in the type

A function item does not need to store an address in each value. Its unique type already tells the compiler exactly which body a call reaches. This lets calls through the item be statically dispatched and optimised without loading a pointer.

The Reference describes function item types as unique to the function, its early-bound lifetime arguments, and its type or const arguments. The type is usually displayed in diagnostics with braces, such as fn() {callback}. That spelling is a useful clue: it is not the same type as plain fn().

Because the item carries no runtime data, byte-for-byte reinterpretation cannot manufacture an address from it. transmute checks source and destination sizes, and the mismatch exposes the incorrect model.

Safe coercion is the intended conversion

The repaired fixture writes let pointer: fn() = callback;. The expected type requests the built-in coercion, after which pointer contains a callable function pointer.

No unsafe block is necessary. I prefer this assignment because it says exactly what I need and gives rustc a normal type-checked conversion. An explicit cast such as callback as fn() may also be useful where inference needs help, but a typed binding or typed argument is often clearer.

Function pointer types can be created from function items and non-capturing closures. Capturing closures are different because their environments contain runtime state. They cannot become bare function pointers unless no capture is required.

Generic functions produce specialised items

For a generic function, selecting type arguments identifies a particular function item that can then coerce to a compatible pointer. Different monomorphisations are different items. If I am building a callback table, I make every slot's full signature explicit, including safety, ABI, and lifetime relationships.

This matters at FFI boundaries. extern "C" fn(...) and Rust's default fn(...) do not promise the same calling convention. A safe pointer and an unsafe fn pointer also carry different caller obligations. Making a function item coerce does not erase these distinctions, and I do not transmute between incompatible signatures to silence them.

Transmute is not a general conversion API

transmute reinterprets bits under strict validity and size requirements. It does not run a language coercion, call From, adjust an ABI, or prove that a foreign address points to a compatible function. When ordinary coercion exists, using transmute only hides intent and enlarges the unsafe proof.

Sometimes low-level code obtains a symbol address from an operating-system loader. That is a separate unsafe boundary. I isolate platform-specific conversion there, validate the documented ABI, keep the library handle alive, and expose a typed wrapper. E0591 should not lead to a casual intermediate cast that pretends all data and function pointer representations are interchangeable on every target.

I also test behaviour, not pointer numeric values. Function addresses can change between builds, link modes, and processes. The stable contract is that calling through a correctly typed pointer reaches compatible code while all lifetime and library-loading requirements remain true.

My E0591 checklist

  • Does the diagnostic display a function-item type with the function name in braces?
  • Can a typed binding or function argument request the normal coercion?
  • Does the pointer signature match parameters, return type, safety, and ABI?
  • Is the source a non-capturing closure or a closure with an environment?
  • For a generic function, have the intended type arguments been selected?
  • Why is any transmute needed after the safe coercion?
  • At FFI boundaries, who keeps code and library storage alive?
  • Are platform assumptions documented and tested on every supported target?

The core principle is that callability does not imply identical representation. A function item gives rustc compile-time identity; a function pointer carries a runtime callable address. I let Rust perform the safe coercion between those concepts before considering any genuinely low-level operation.