Skip to content

view: add OwnedView::reborrow() and ViewReborrow trait (#82) #92

Closed
tejas-dharani wants to merge 3 commits intoanthropics:mainfrom
tejas-dharani:view/owned-view-reborrow
Closed

view: add OwnedView::reborrow() and ViewReborrow trait (#82) #92
tejas-dharani wants to merge 3 commits intoanthropics:mainfrom
tejas-dharani:view/owned-view-reborrow

Conversation

@tejas-dharani
Copy link
Copy Markdown
Contributor

Description:

Closes #82.

OwnedView stores V = FooView<'static> — a soundness fiction required to keep the view
self-contained alongside its Bytes buffer. Deref exposes &FooView<'static>, so field borrows appear
'static to the compiler. This makes it impossible to write a handler that returns a view field tied
to the OwnedView's real lifetime.

Fix

New unsafe trait ViewReborrow with a GAT type Reborrowed<'b> and a new OwnedView::reborrow<'b>(&'b
self) -> &'b V::Reborrowed<'b> method. The implementation uses a pointer cast (*const V → *const
V::Reborrowed<'b>) rather than transmute_copy — no value is duplicated, so there is no double-free
risk for views containing heap-owning fields (RepeatedView, MapView, MessageFieldView,
UnknownFieldsView). The cast is sound because ViewReborrow's safety contract requires Reborrowed<'b>
to be the same struct with only its lifetime parameter shortened, and OwnedView's constructor
invariant ensures all borrows point into self.bytes.

Codegen emits unsafe impl ViewReborrow automatically for every generated view type. Hand-written
view types opt in with unsafe impl.

Tests

Unit tests in buffa/src/view.rs cover pointer-identity (no copy), simultaneous reborrows, and a
compile_fail,E0597 doctest confirming the borrow checker rejects escapes. End-to-end tests in
buffa-test/src/tests/view.rs exercise the full generated PersonView: scalars, bytes, repeated,
nested message, oneof, and the motivating RPC handler pattern.

buffa-types/src/generated/ regenerated (task gen-wkt-types).

`OwnedView<V>` stored `V = FooView<'static>` — a soundness fiction where
the actual borrows point into the internal `Bytes` buffer. `Deref` exposed
`&FooView<'static>`, making field borrows appear `'static` to the compiler.
This made it impossible to write an RPC handler that returns a view field
tied to the `OwnedView`'s real lifetime.

Add `ViewReborrow` — an `unsafe trait` with a GAT:

    pub unsafe trait ViewReborrow: MessageView<'static> {
        type Reborrowed<'b>: MessageView<'b, Owned = ...>;
    }

And `OwnedView::reborrow()`:

    pub fn reborrow<'b>(&'b self) -> &'b V::Reborrowed<'b>

The implementation uses a pointer cast (`*const V` → `*const V::Reborrowed<'b>`)
rather than `transmute_copy`, so no value is duplicated and there is no
double-free risk for views containing heap-owning fields (RepeatedView,
MapView, MessageFieldView, UnknownFieldsView).

Codegen emits `unsafe impl ViewReborrow` automatically for every generated
view type. Hand-written view types opt in with `unsafe impl`.

- buffa/src/view.rs: ViewReborrow trait + OwnedView::reborrow() + tests
- buffa/src/lib.rs: re-export ViewReborrow; fixture unsafe impl
- buffa-codegen/src/view.rs: emit unsafe impl ViewReborrow per view type
- buffa-types/src/generated/: regenerated WKTs (task gen-wkt-types)
- buffa-test/src/tests/view.rs: end-to-end integration tests
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 3, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

Three fixes from review of anthropics#92:

H1 — `OwnedView::reborrow` now contains an inline-const `size_of`/
`align_of` guard that catches mistyped `unsafe impl ViewReborrow` at
monomorphization rather than as latent UB at the cast site. The trait's
`# Safety` doc previously claimed such a guard existed; that's now true.
The doc also calls out explicitly that the same-struct requirement is
enforced *only* by the contract — the GAT bound and layout guard rule
out subsets of the misuse, but a different struct that happens to share
layout and `Owned` is still UB.

M1 — codegen-emitted `unsafe impl ViewReborrow` previously had a
`// SAFETY:` comment in the codegen template that quote! silently drops.
Switched to a `///` doc comment so the safety rationale survives into
the generated source (visible in every regenerated buffa-types __view.rs
file). Per project convention every `unsafe` needs a SAFETY justification
in user-visible source.

Ergonomics M2/M3 — `docs/guide.md` "OwnedView in async trait
implementations" gains a "Returning a borrow into the request:
reborrow()" subsection showing the failing case and the fix; the
`OwnedView` rustdoc gains a one-paragraph breadcrumb pointing users who
hit `error[E0597]: borrowed value does not live long enough` at
`reborrow()`.

Buffa-types view files regenerated to pick up the SAFETY doc-attr. All
605 lib + 289 e2e + 9 doctests pass; clippy clean.
@iainmcgin
Copy link
Copy Markdown
Collaborator

[claude code] Pushed a fixup commit (01f7229) addressing the review findings:

H1 — inline-const layout guard (buffa/src/view.rs): OwnedView::reborrow now contains a const { assert!(size_of::<V> == size_of::<V::Reborrowed<'static>>); ... } guard that catches mistyped unsafe impl ViewReborrow at monomorphization rather than as latent UB at the cast site. The trait's # Safety doc previously claimed such a guard existed; that's now true. Also explicitly notes that the same-struct requirement is enforced only by the contract — the GAT bound and layout guard rule out subsets of the misuse, but a different struct that happens to share layout and Owned is still UB and the impl author is responsible.

M1 — surviving SAFETY comment in generated code (buffa-codegen/src/view.rs): the // SAFETY: comment in the codegen template was being silently dropped by quote!. Switched to a /// doc comment so the rationale survives into the generated __view.rs files (verified by regenerating the WKTs and checking the output). Project convention is that every unsafe needs a visible justification, including in generated code.

Ergonomics M2/M3 — guide section + rustdoc breadcrumb (docs/guide.md, buffa/src/view.rs): the "OwnedView in async trait implementations" guide section gains a "Returning a borrow into the request: reborrow()" subsection that walks through the failing-without-reborrow case and the fix. OwnedView's rustdoc gains a one-paragraph breadcrumb naming the borrow-checker error verbatim ("borrowed value does not live long enough", "lifetime may not live long enough") so users can grep their compile error against the docs and find reborrow().

Verified: workspace check clean, 605 lib + 289 e2e + 9 doctests pass, clippy -D warnings clean. WKT view files regenerated to pick up the SAFETY doc-attr — that's why the diff touches buffa-types/src/generated/.

Lows from review (move-while-borrowed compile_fail doctest, variance compile-time assertion, # Safety heading on a safe fn, em-dash style) flagged for future touch-ups; not blocking.

@iainmcgin
Copy link
Copy Markdown
Collaborator

Posted #95 as a draft alternative formulation that makes ViewReborrow a safe trait via subtyping coercion (the body { this } works for any covariant view, which all generated views are). Net -34 lines, no unsafe trait, no pointer cast, no layout guard. Tagged you there for review — landing this PR vs swapping to that shape is a judgment call I'd like your input on.

@iainmcgin
Copy link
Copy Markdown
Collaborator

closing in favor of #95

@iainmcgin iainmcgin closed this May 5, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators May 5, 2026
iainmcgin added a commit that referenced this pull request May 5, 2026
**Alternative to #92 — for review/comparison only.** This PR is a draft
to show what the `OwnedView::reborrow` mechanism looks like as a *safe*
trait. Intended as decision material; if we land this we should close
#92, otherwise close this and keep #92.

@tejas-dharani — would you take a look? Curious whether you considered
this shape and rejected it, or whether it's worth swapping in. I think
the mechanical soundness check at impl-site is a strict win, but I want
your read since you wrote the original.

## What changes

`ViewReborrow` becomes a safe trait whose method body coerces `&'b Self`
(= `&'b FooView<'static>`) into `&'b Self::Reborrowed<'b>` (= `&'b
FooView<'b>`). For covariant view types — which all generated views are
— Rust's lifetime variance accepts the body `this` as a plain subtype
coercion. No `unsafe`, no pointer cast, no `SAFETY` rationale.

```rust
// before (#92)
pub unsafe trait ViewReborrow: MessageView<'static> {
    type Reborrowed<'b>: MessageView<'b, Owned = ...>;
}

// after (this PR)
pub trait ViewReborrow: MessageView<'static> {
    type Reborrowed<'b>: MessageView<'b, Owned = ...> where Self: 'b;
    fn reborrow<'b>(this: &'b Self) -> &'b Self::Reborrowed<'b>;
}
```

Generated code (per view) goes from:

```rust
// before
// SAFETY: Reborrowed<'b> is the same generated struct ...
unsafe impl ::buffa::ViewReborrow for FooView<'static> {
    type Reborrowed<'b> = FooView<'b>;
}
```

to:

```rust
// after
impl ::buffa::ViewReborrow for FooView<'static> {
    type Reborrowed<'b> = FooView<'b>;
    fn reborrow<'b>(this: &'b Self) -> &'b Self::Reborrowed<'b> {
        this
    }
}
```

`OwnedView::reborrow` collapses to one line:

```rust
pub fn reborrow<'b>(&'b self) -> &'b V::Reborrowed<'b>
where V: ViewReborrow,
{
    V::reborrow(&self.view)
}
```

## Why this is sound

`FooView<'a>` is covariant in `'a` if every field uses `'a` covariantly
— `&'a str`, `&'a [u8]`, `RepeatedView<'a>`, `MessageFieldView<'a>`,
`PhantomData<&'a ()>`, etc. all qualify. Generated views are uniformly
covariant.

For a covariant `FooView`, `'static : 'b` (always) gives us
`FooView<'static>: FooView<'b>` (subtyping). Then `&'b FooView<'static>:
&'b FooView<'b>` (since `&'b _` is covariant in the referent). The body
`this` flows into the return slot via this exact subtyping.

If a hand-written view type is *invariant* (has `Cell<&'a T>`, `&'a mut
T`, `fn(&'a T)` somewhere), the body fails to compile and the impl is
rejected — which is precisely what should happen, because narrowing an
invariant lifetime is unsound. Compare with #92's design, where the same
hand-written view can write a buggy `unsafe impl ViewReborrow` and
produce UB; here the compiler catches it for them.

## What goes away

- `unsafe trait` keyword on `ViewReborrow`
- The pointer cast in `OwnedView::reborrow`
- The inline-const `size_of`/`align_of` guard added in #92's review
fixes
- The `SAFETY:` doc attribute on every generated `impl`
- Hand-written impls on test fixtures stop needing `unsafe impl`

## What stays the same

- The user-facing API: `owned_view.reborrow()` returns `&'b
V::Reborrowed<'b>` exactly as in #92.
- All tests from #92 pass without modification (605 lib + 289 e2e + 9
doctests; clippy `-D warnings` clean).
- Same `#[diagnostic::on_unimplemented]` notes (text adjusted to drop
the `unsafe impl` and add the body).
- Same `compile_fail,E0597` doctest.
- Same `Send`/`Sync`/variance test scaffolding.

## Diffstat vs current PR #92 (post-fixup)

Net **−34 lines** in the buffa crate; the codegen template gets simpler
by losing the SAFETY doc-attr and one source line; the trait gains one
method.

## Decision needed

If you prefer this shape, I'll:
- Mark this ready, transfer the PR description / `Closes #82` over.
- Close #92 with a note pointing here.

If you prefer #92's shape (e.g. for symmetry with `unsafe trait
DefaultInstance` from before #68, or because the explicit pointer cast
better signals what's happening), I'll close this and we land #92 as-is.

References:
- [Original PR #92](#92)
- Issue [#82](#82)
- Prior art for the same pattern: PR
[#68](#68)
(`DefaultViewInstance` unsafe → safe via covariance)

---------

Co-authored-by: Tejas Dharani <tejas.dharani10@gmail.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OwnedView has no reborrow accessor for sub-lifetime views

2 participants