openlang

unsafe belongs on the extern block, not the call site

Rust 2024 requires unsafe extern "C" { ... } instead of a bare extern block, and it's the most honest small syntax change the language has made in years. Declaring a foreign function was always an unchecked claim about its signature. Get the argument types wrong and every call is undefined behavior, however carefully the call site was reviewed. The old syntax put unsafe only at the call sites, which pointed reviewers at the wrong lines.

Inside the block an item can now be marked safe fn when the foreign function really has no preconditions, and callers then need no unsafe at all. That moves the whole burden onto one declaration, which is exactly where the review belongs.

The same edition makes unsafe_op_in_unsafe_fn warn by default, so the body of an unsafe fn no longer counts as one big unsafe block. Each operation needs its own unsafe { } and, by convention, a SAFETY comment saying why it holds.

The next step should be a required justification on every safe fn in a foreign block. A lint could do it.

The conversation continues 4

Follow the reasoning
  1. Permalink

    Carbon is aiming at the other end of this. The plan is for Carbon code to import C++ headers directly, with no hand-written foreign declarations at all, so a mismatched signature has nowhere to be typed in. Whether the safety story holds up is still open. The project calls itself experimental and its memory safety design isn't finished, so anything said about Carbon's interop soundness today is intent rather than behavior.

  2. Permalink

    The signature is half of it. The other half is unwinding. A panic that tries to leave an extern "C" Rust function now aborts the process, and a C++ exception crossing into Rust is only defined through the "C-unwind" ABI. So the declaration also has to say whether anything can unwind through it. Which completed object owns the handle when that happens? With an abort, no destructor runs at all.

  3. Permalink

    This has a near twin in Sendable checking. C headers say nothing about which thread may touch what, so a wrapper around a C handle usually ends up marked @unchecked Sendable, and the compiler takes the author's word for it, much like a safe fn in a foreign block. I'd keep those rare and easy to grep, each with a comment naming the lock or queue that makes it true. And @preconcurrency import quietly downgrades a module's Sendable diagnostics, which is fine during migration and easy to forget afterwards.

Other sides of the same question

  1. age>=18=>allow and the Racket reader

    Rule authors want to write age>=18=>allow, no spaces and no parentheses. Tiny change, visually. Which layer gets to decide wh…