Skip to content

Add HasMessageView: link owned messages to their generated view types#158

Merged
iainmcgin merged 1 commit into
mainfrom
has-message-view
May 28, 2026
Merged

Add HasMessageView: link owned messages to their generated view types#158
iainmcgin merged 1 commit into
mainfrom
has-message-view

Conversation

@iainmcgin
Copy link
Copy Markdown
Collaborator

@iainmcgin iainmcgin commented May 28, 2026

Summary

Adds buffa::HasMessageView, a small trait that links an owned message type to its generated zero-copy view types, completing the wrapper surface introduced in #154:

pub trait HasMessageView: Message + Sized {
    type View<'a>: MessageView<'a, Owned = Self> + Send + Sync;
    type ViewHandle: From<OwnedView<Self::View<'static>>>
        + AsRef<OwnedView<Self::View<'static>>>
        + Send + Sync + 'static;

    fn decode_view_handle(bytes: Bytes) -> Result<Self::ViewHandle, DecodeError> {}
    fn decode_view_handle_with_options() -> Result<Self::ViewHandle, DecodeError> {}
}

For each message Foo (when views are generated), codegen emits impl HasMessageView for Foo with View<'a> = FooView<'a> and ViewHandle = FooOwnedView, and the generated FooOwnedView wrapper additionally implements AsRef<OwnedView<FooView<'static>>>. Together these let code that is generic over an owned message decode a request body into M::View<'_>, hold M::ViewHandle items, and reach reborrow() / bytes() / to_owned_message() through the handle — without per-message glue on the consumer's side. This is the hook an RPC framework needs to accept M and work with its view types generically; it has been validated end-to-end against a connect-rust prototype, where it replaces an equivalent local shim.

Example

The provided decode_view_handle (concretely or as M::decode_view_handle in generic code) goes straight from wire bytes to the message's owned-view handle, where field access reads much like the pre-#154 view.name — accessor methods instead of Deref fields, with every borrow tied to the handle:

// Decode to the message's owned-view handle ('static + Send):
let person = Person::decode_view_handle(body)?;        // PersonOwnedView

// Zero-copy field access; each borrow is tied to `person` and cannot
// outlive it:
println!("{} ({})", person.name(), person.id());

// A borrowed field therefore can't be sent somewhere that outlives the
// handle (tokio::spawn, channels, storage). Detach just the data you need…
let name = person.name().to_owned();
tokio::spawn(async move { index(name).await });

// …or move the whole handle into the task (it owns its buffer), or convert
// with `person.to_owned_message()` when the full owned struct is wanted.
tokio::spawn(async move { audit(person).await });

The difference from the pre-#154 ergonomics is deliberate: the borrow checker now enforces that field borrows stay within the handle's lifetime instead of letting them escape as 'static.

Notes

  • The associated types carry only structural bounds; the wrapper's per-field accessor methods remain inherent on the concrete type.
  • Generic code that reborrows adds M::View<'static>: ViewReborrow at the use site — a trait-level where clause for it currently trips a GAT normalization error (documented on the trait).
  • The impls are emitted alongside the view/wrapper and share their feature gating; map-entry synthetic messages are skipped, matching the wrapper.
  • Checked-in WKT and descriptor generated code is regenerated.

Testing

  • New codegen assertions for the emitted impls.
  • buffa-test view_family module: a function generic over M: HasMessageView exercising decode → AsRef → reborrow → bytes → owned round-trip (flat message and oneof), plus associated-type and Send/Sync assertions.
  • task lint, full workspace tests (all features), check-nostd, and rustdoc link checks all pass.

For each message Foo (when views are generated), generated code now
implements buffa::HasMessageView, naming the borrowed view
(Foo::View<'a> = FooView<'a>) and the 'static handle
(Foo::ViewHandle = FooOwnedView), with provided decode_view_handle
helpers. The generated FooOwnedView wrapper additionally implements
AsRef<OwnedView<FooView<'static>>> so generic code can reach
reborrow(), bytes(), and to_owned_message() through the handle without
naming concrete types.

This gives downstream frameworks a way to be generic over an owned
message and work with its view types — decode a request body into a
borrowed view, hold owned handles as stream items — without emitting
per-message glue of their own. Code that reborrows generically adds
M::View<'static>: ViewReborrow at the use site (a trait-level where
clause currently trips a GAT normalization error).
@github-actions
Copy link
Copy Markdown

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

@iainmcgin iainmcgin requested a review from rpb-ant May 28, 2026 22:24
@iainmcgin iainmcgin marked this pull request as ready for review May 28, 2026 22:24
@iainmcgin iainmcgin merged commit 920d7d8 into main May 28, 2026
7 checks passed
@iainmcgin iainmcgin deleted the has-message-view branch May 28, 2026 22:26
@github-actions github-actions Bot locked and limited conversation to collaborators May 28, 2026
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.

2 participants