RFA-622 · Case file with fixtures · Case 594 of 694 · Compiler evidence
track_caller Cannot Cross a Non-Rust ABI
Caller tracking changes Rust call behaviour through hidden location data. Keep the foreign ABI exact, and add a Rust wrapper when diagnostics need the original call site.
- 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
- A Rust-specific hidden diagnostic input was attached to a foreign calling convention whose callers know only the declared C signature.
- First discriminating check
- Keep the exported ABI exact and move caller tracking into an ordinary Rust wrapper, using explicit error context across FFI.
#[track_caller] improves diagnostics by making a function observe the location where its caller invoked it. That information is not declared as a normal source parameter. It is part of the Rust calling convention. The failing fixture combines the attribute with extern "C" and receives E0737.
An ABI is the complete calling contract
At an FFI boundary, both sides must agree on symbol naming, argument placement, return rules, unwind behaviour, and relevant layout. Adding implicit Rust-specific information would make the apparent C signature incomplete.
The official E0737 explanation says caller tracking requires the Rust ABI. The Reference documents the track_caller attribute and its restrictions.
I treat the diagnostic as protection against ABI drift. The foreign caller cannot supply a Rust caller location according to a convention it does not know.
Keep the export thin and add a Rust wrapper
The repaired fixture removes the attribute from the C function. In production I usually split responsibilities further. A thin exported function validates raw pointers and primitive values, catches or prevents forbidden unwinding, and converts into safe Rust values. It then calls ordinary Rust logic.
The Rust-facing wrapper can use #[track_caller] when its callers benefit from a precise panic or assertion location. The C entrypoint instead records explicit boundary context: operation code, request identifier, validated sizes, or an error value defined by the ABI.
This does not magically recover the foreign source line. Native stacks and debug information can help during debugging, but they are a different mechanism from Rust’s caller-location propagation.
Diagnostics should cross FFI explicitly
I avoid using panic location as the primary error channel across languages. C callers need documented status codes, output parameters, or opaque error handles with explicit lifetime functions. Logs can attach a stable boundary name and correlation identifier.
The Rustonomicon’s FFI chapter covers the wider unsafe contract. Caller tracking solves only one diagnostic detail and does not validate pointers, strings, alignment, ownership, callbacks, or thread rules.
If an exported function can fail due to caller input, that failure belongs in the public foreign contract. If it indicates a Rust invariant violation, I keep the failure contained, record enough evidence, and avoid unwinding through an ABI that does not permit it.
Function pointer types must remain compatible
Callbacks are especially easy to get wrong. A C library stores a function pointer matching one exact signature. A Rust wrapper with extra behaviour can call that pointer, but the pointer itself remains extern "C" fn(...) with no hidden Rust caller data.
I compile a small C harness for important boundaries. Rust-only tests may call an export successfully while missing a header mismatch, platform calling convention difference, or wrong integer width. Header generation and layout assertions help, but an actual foreign call proves more.
Target architectures can pass structures and returns differently. I keep exported signatures conservative, use fixed-width C-compatible types, and test every supported target in CI where feasible.
track_caller is not free provenance
Even on a Rust ABI function, the reported location depends on calls participating in caller tracking. Indirect calls and closures have documented behaviour. I write assertions against stable properties rather than depending on an exact line through many wrapper layers.
Adding the attribute to a public Rust function may change ABI details. The Reference notes that tracked functions have restrictions around function pointers and coercions. I review its use as an interface decision, not merely logging decoration.
For libraries, a structured error normally survives refactors better than a source line. Caller location is excellent supplementary evidence for contract violations, especially in internal APIs, but it should not become the only machine-readable diagnosis.
My E0737 checklist
- Is
#[track_caller]attached to a non-Rust ABI function? - Which side of the boundary is supposed to provide location information?
- Can a thin foreign export delegate to an ordinary tracked Rust wrapper?
- Does the external API return errors explicitly rather than depending on panic text?
- Are unwinding rules safe for the chosen ABI?
- Do callback function pointers preserve the exact foreign signature?
- Has a real foreign-language harness exercised the symbol on supported targets?
- Would structured context be more stable than a source location for this failure?
The core principle is that hidden caller information is still part of a calling convention. E0737 keeps Rust’s diagnostic mechanism out of an ABI that cannot carry it. I keep the boundary exact and move richer diagnostics into an explicit Rust layer.