Add ForeignRef<T> and make FetchHeaders the owned handle

JA
4mo ago
+10182-10137

What

bun_opaque gains a generic owned handle for C++-allocated objects that Rust releases through an FFI destructor:

  • ForeignOwned — "this opaque type has a release extern"
  • ForeignRef<T> — Deref + Drop, #[repr(transparent)] over NonNull<T>, zero cost
  • foreign_owned!(T, release_fn) — one line to get both

FetchHeaders is the first user, and the owned handle takes the public name (FetchHeaders : HeadersRef :: Path : PathBuf, with the raw ZST moved to an extern-only sys module). Four such handles were previously hand-rolled and ~27 more C++ types are still released by hand at every call site; those are the follow-up.

The rest of the diff converts the unsafe { &mut *ptr } pattern this grew out of — raw-pointer fields become the in-tree owners (JsCell, Cell, ParentRef, BackRef, Box<Self> receivers) across the runtime, and receivers that only ever needed &self now say so. unsafe { &mut * } goes from 1241 sites to 807.

No allocation, lock, Rc/Arc, or refcount is added anywhere in the diff. (Checked per-round: every added heap::release/RefPtr::new/Box::leak replaces an existing Box::into_raw(Box::new(..)) 1:1.)

Why &self everywhere on FetchHeaders

An opaque_ffi! type is a #[repr(C)] UnsafeCell<[u8;0]> ZST precisely so that &T carries no noalias/readonly and C++ may mutate through it. &mut self on such a type asserts an exclusivity that is never true (C++ owns the object and re-enters through the same pointer) and never needed (every FFI shim already takes &T). All 18 receivers were &mut self; that one wrong choice had grown a DerefMut, two &self -> &mut accessors, a duplicate RAII handle, and forced callers to launder &T into *mut T.

Behavior fix included

Making the receivers honest stopped server.fetch(url, { headers }) from compiling, which surfaced that it was sharing the caller's Headers with the internal Request instead of copying it. Two user-visible consequences: a header the handler set on req.headers showed up on the caller's Headers object, and the two owners disagreed about the object's lifetime. It now copies via clone_this, the same path new Response(_, { headers }) already takes. Regression test added; the aliasing half is asserted deterministically and the whole test also runs under the AddressSanitizer CI job.

Verification

  • cargo check --workspace clean; bun bd (debug + ASan) clean
  • release build + 1,895 tests across serve/fetch/headers/node-http2/fs/resolve/bunshell: 1,789 pass, 0 real failures
  • ASan run over headers/node-http2/serve (642 tests): 0 reports
  • Miri under -Zmiri-tree-borrows: 27 crates, 146 tests, 0 UB
  • The new regression test fails on a build without the fix and passes with it