ptr: add OwnedRef<T>, RefPtr::to_owned, and AnyRefCounted::ref_guard

Closed
JA
3mo ago
+544-1

What does this PR do?

RefPtr<T> deliberately has no Drop impl — dropping one leaks the strong ref, so every consumer balances ref_()/deref() by hand. This PR adds the owning RAII form so those call sites can be migrated type-by-type in follow-ups. No callers are migrated here, and RefPtr itself is unchanged (adding Drop to it would double-free every existing manually-balanced site).

New API in src/ptr/ref_count.rs:

  • OwnedRef<T> — holds exactly one strong ref and releases it on Drop (every exit path: early return, ?, unwind). Deref to &T, Clone bumps the count, into_raw/from_raw for FFI hand-offs, new/acquire constructors. Double-release is impossible by construction: the only way to give up the ref without releasing it is into_raw, which consumes the handle.
  • RefPtr::to_owned(&self) -> OwnedRef<T> — safe bridge from a manually-balanced RefPtr to the RAII handle (takes an additional ref; the RefPtr's own ref is still owed).
  • AnyRefCounted::ref_guard(&self) -> ScopedRef<Self> — RAII replacement for the hand-paired this.ref_(); … defer this.deref(); keep-alive bracket. This one is an unsafe fn: ScopedRef carries no lifetime, so the compiler cannot stop the guard from outliving a stack-backed *self, and the storage-governance precondition ("freed only by the count-zero destructor") goes under # Safety. It still collapses the N per-exit-path deref() calls into one audited acquisition.
  • The RefPtr doc comment now points new code at OwnedRef.

Unsafe count (src/ptr/ref_count.rs)

Before: 89 → after: 129. The increase breaks down as 23 in the new #[cfg(test)] module (test-type trait impls that mirror the derive output, plus the raw-pointer round-trip and ref_guard tests) and the rest inside the new abstractions' bodies, each with a SAFETY: comment. This project adds the safe surface; the net reduction lands when the ~700 hand-balanced call sites migrate onto it in follow-up PRs.

Cost

Zero-cost: OwnedRef is the same NonNull<T> (+ debug-only tracking id) as RefPtr; Drop/Clone compile to the same deref()/ref_() calls the manual sites already make.

Sites that do not fully meet the "no safe-call UB" bar

  • ref_guard is kept unsafe fn rather than safe: the requirement that *self's storage is governed by its embedded refcount is a precondition the compiler cannot see (a stack-allocated T: RefCounted is constructible in safe code, and ScopedRef carries no lifetime to stop the guard outliving it), so per the project rules it stays unsafe with the precondition under # Safety.
  • ref_guard's pointer is derived from &self; if the guard holds the final ref, the destructor reclaims the allocation through that shared-provenance pointer — the same posture as the existing as_ctx_ptr() → deref() release sites, which Tree Borrows rejects. Documented under # Safety; the test for that path runs under the normal harness but is #[cfg_attr(miri, ignore)]d. The bracket use case (an outer owner survives the scope) is fully Miri-clean.

OwnedRef and RefPtr::to_owned meet the bar: every safe path acquires and releases exactly one ref, and the only safe constructor (new) heap-allocates.

Other notes

  • The scoped_log! diagnostics in ref_()/deref() are compiled out of bun_ptr's own unit-test target only (#[cfg(not(test))]): they reach the OutputSink link interface whose Sys arm lives in bun_sys, which the leaf-crate test binary cannot link. Downstream builds are unaffected.
  • The ref-count debug tracker skips its libc::backtrace capture under Miri (unsupported foreign function); it records an empty trace instead.
  • OwnedRef is intentionally not added to lib.rs's flat re-export list — this PR confines itself to ref_count.rs so the follow-up migration PRs don't conflict with it. It is reachable as bun_ptr::ref_count::OwnedRef.

How did you verify your code works?

  • cargo test -p bun_ptr — 12/12 pass.
  • bun run rust:miri -p bun_ptr (-Zmiri-tree-borrows) — 9 pass, 3 ignored with documented reasons (two need ThreadLock's libc::backtrace, one exercises the &self-derived deallocation path described above). The Miri-covered set includes the full OwnedRef lifecycle for the thread-safe flavor: construct/drop balance, clone balance, into_raw/from_raw round-trip, to_owned, ref_guard, and destructor-runs-exactly-once assertions.
  • cargo clippy -p bun_ptr --all-targets — clean.
  • bun bd — full debug build compiles.