Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 22 additions & 17 deletions buffa-codegen/src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,22 @@ fn closed_enum_view_unknown_route(preserve_unknown_fields: bool) -> TokenStream

/// Convert a borrowed bytes view to the owned field type.
///
/// Emits `bytes::Bytes::copy_from_slice(expr)` when `use_bytes_type()`
/// is active for this field (the borrow isn't `'static` so `from` won't
/// work), otherwise `(expr).to_vec()`.
/// When `use_bytes_type()` is active for this field, emits
/// `::buffa::view::bytes_from_source(__buffa_src, expr)` so
/// `to_owned_from_source(Some(buf))` produces a zero-copy `Bytes::slice_ref`;
/// `None` falls back to `copy_from_slice`. Otherwise emits `(expr).to_vec()`.
///
/// `expr` may be `&[u8]` (singular/optional) or `&&[u8]` (repeated-iter,
/// oneof match-ergonomics). Both branches accept either: `.to_vec()` via
/// method auto-deref, `copy_from_slice` via argument auto-deref.
/// method auto-deref, `bytes_from_source`'s `&[u8]` arg via auto-deref.
fn bytes_to_owned(
ctx: &CodeGenContext,
proto_fqn: &str,
field_name: &str,
expr: TokenStream,
) -> TokenStream {
if field_uses_bytes(ctx, proto_fqn, field_name) {
quote! { ::bytes::Bytes::copy_from_slice(#expr) }
quote! { ::buffa::view::bytes_from_source(__buffa_src, #expr) }
} else {
quote! { (#expr).to_vec() }
}
Expand Down Expand Up @@ -292,19 +293,21 @@ pub(crate) fn generate_view_with_nesting(
Self::_decode_depth(buf, depth)
}

/// Convert this view to the owned message type.
// redundant_closure: bytes_to_owned() emits `|b| Bytes::copy_from_slice(b)`
// for optional bytes — eta-reducible, but the non-bytes branch
// `|b| (b).to_vec()` is NOT (no fn path for the method), so the
// helper can't uniformly emit a fn path.
fn to_owned_message(&self) -> #owned_path {
self.to_owned_from_source(None)
}

// useless_conversion: __buffa_unknown_fields uses `.into()` to
// unify the `UnknownFields` (no-wrapper) and `__<Name>ExtJson`
// (generate_json wrapper) cases; no-op in the former.
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
#[allow(clippy::needless_update)]
fn to_owned_message(&self) -> #owned_path {
#[allow(clippy::useless_conversion, clippy::needless_update)]
fn to_owned_from_source(
&self,
__buffa_src: ::core::option::Option<&::buffa::bytes::Bytes>,
) -> #owned_path {
#[allow(unused_imports)]
use ::buffa::alloc::string::ToString as _;
let _ = __buffa_src;
#owned_path {
#(#owned_fields)*
..::core::default::Default::default()
Expand Down Expand Up @@ -1380,7 +1383,9 @@ fn singular_to_owned(
let owned_ty = crate::message::rust_path_to_tokens(&owned_path);
quote! {
match self.#ident.as_option() {
Some(v) => ::buffa::MessageField::<#owned_ty>::some(v.to_owned_message()),
Some(v) => ::buffa::MessageField::<#owned_ty>::some(
v.to_owned_from_source(__buffa_src),
),
None => ::buffa::MessageField::none(),
}
}
Expand All @@ -1404,7 +1409,7 @@ fn repeated_to_owned(
quote! { self.#ident.iter().map(|b| #conv).collect() }
}
Type::TYPE_MESSAGE | Type::TYPE_GROUP => {
quote! { self.#ident.iter().map(|v| v.to_owned_message()).collect() }
quote! { self.#ident.iter().map(|v| v.to_owned_from_source(__buffa_src)).collect() }
}
_ => quote! { self.#ident.to_vec() },
})
Expand Down Expand Up @@ -1432,7 +1437,7 @@ fn map_to_owned_expr(
Type::TYPE_MESSAGE => {
// Verify the owned path resolves (catches missing imports at codegen time).
let _owned_path = resolve_owned_path(scope, val_fd)?;
quote! { v.to_owned_message() }
quote! { v.to_owned_from_source(__buffa_src) }
}
_ => quote! { *v },
};
Expand All @@ -1449,7 +1454,7 @@ fn oneof_variant_to_owned(scope: MessageScope<'_>, ty: Type, field_name: &str) -
// match-ergonomics on &ViewEnum → v: &&[u8]. bytes_to_owned handles it.
Type::TYPE_BYTES => bytes_to_owned(ctx, proto_fqn, field_name, quote! { v }),
Type::TYPE_MESSAGE | Type::TYPE_GROUP => {
quote! { ::buffa::alloc::boxed::Box::new(v.to_owned_message()) }
quote! { ::buffa::alloc::boxed::Box::new(v.to_owned_from_source(__buffa_src)) }
}
_ => quote! { *v },
}
Expand Down
6 changes: 6 additions & 0 deletions buffa-test/protos/basic.proto
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ message BytesContexts {
map<string, bytes> by_key = 6;
}

// Nested-message bytes_fields: locks in to_owned_from_source recursion
// (the parent must thread __buffa_src into inner.to_owned_from_source).
message BytesNested {
BytesContexts inner = 1;
}

// Message to test all numeric scalar types.
message AllScalars {
int32 f_int32 = 1;
Expand Down
74 changes: 70 additions & 4 deletions buffa-test/src/tests/bytes_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ fn test_bytes_type_repeated_view_to_owned() {
assert_eq!(borrowed, vec![&b"a"[..], &b"bc"[..], &b""[..]]);

// to_owned_message: Vec<&[u8]> → Vec<bytes::Bytes>.
// Generated: self.many.iter().map(|b| ::bytes::Bytes::copy_from_slice(*b)).collect()
// where b: &&[u8] so *b: &[u8].
// Generated: self.many.iter().map(|b| bytes_from_source(__buffa_src, b)).collect()
// where b: &&[u8]; the &[u8] arg auto-derefs.
let owned: BytesContexts = view.to_owned_message();
assert_eq!(owned.many.len(), 3);
assert_eq!(&owned.many[0][..], b"a");
Expand All @@ -127,8 +127,8 @@ fn test_bytes_type_oneof_view_to_owned() {

// to_owned_message: view oneof ChoiceView::Raw(&[u8]) → owned Choice::Raw(Bytes).
// Generated: self.choice.as_ref().map(|v| match v {
// ChoiceView::Raw(v) => Choice::Raw(::bytes::Bytes::copy_from_slice(*v)), ... })
// Match ergonomics: v in the arm is &&[u8], *v is &[u8].
// ChoiceView::Raw(v) => Choice::Raw(bytes_from_source(__buffa_src, v)), ... })
// Match ergonomics: v in the arm is &&[u8]; the &[u8] arg auto-derefs.
let owned: BytesContexts = view.to_owned_message();
match &owned.choice {
Some(ChoiceOneof::Raw(b)) => assert_eq!(&b[..], &[0x00, 0xFF, 0x7F]),
Expand Down Expand Up @@ -162,6 +162,72 @@ fn test_bytes_type_optional_view_to_owned() {
}
}

#[test]
fn test_bytes_type_view_to_owned_from_source_zero_copy() {
// Issue #52: to_owned_from_source(Some(&buf)) must slice_ref into the
// source buffer for singular/optional/repeated/oneof bytes_fields.
use buffa::MessageView;
let msg = BytesContexts {
many: vec![bytes::Bytes::from_static(b"aaaa"), bytes::Bytes::new()],
maybe: Some(bytes::Bytes::from_static(b"bbbb")),
choice: Some(ChoiceOneof::Raw(bytes::Bytes::from_static(b"cccc"))),
..Default::default()
};
let buf = bytes::Bytes::from(msg.encode_to_vec());
let in_buf = |p: *const u8| {
let r = buf.as_ptr() as usize..buf.as_ptr() as usize + buf.len();
r.contains(&(p as usize))
};

let view = BytesContextsView::decode_view(&buf).expect("decode_view");
let owned = view.to_owned_from_source(Some(&buf));

assert_eq!(&owned.many[0][..], b"aaaa");
assert!(
in_buf(owned.many[0].as_ptr()),
"repeated[0] should slice_ref"
);
assert!(owned.many[1].is_empty());
assert_eq!(owned.maybe.as_deref(), Some(&b"bbbb"[..]));
assert!(
in_buf(owned.maybe.as_ref().unwrap().as_ptr()),
"optional should slice_ref"
);
match &owned.choice {
Some(ChoiceOneof::Raw(b)) => {
assert_eq!(&b[..], b"cccc");
assert!(in_buf(b.as_ptr()), "oneof should slice_ref");
}
other => panic!("expected Choice::Raw, got {other:?}"),
}
assert_eq!(owned.encode_to_vec(), buf);
}

#[test]
fn test_bytes_type_nested_to_owned_from_source_zero_copy() {
// Issue #52: __buffa_src must thread through nested-message recursion.
use crate::basic_bytes::__buffa::view::BytesNestedView;
use crate::basic_bytes::BytesNested;
use buffa::MessageView;
let msg = BytesNested {
inner: buffa::MessageField::some(BytesContexts {
singular: bytes::Bytes::from_static(b"nested-payload"),
..Default::default()
}),
..Default::default()
};
let buf = bytes::Bytes::from(msg.encode_to_vec());
let view = BytesNestedView::decode_view(&buf).expect("decode_view");
let owned = view.to_owned_from_source(Some(&buf));
let inner_bytes = &owned.inner.singular;
assert_eq!(&inner_bytes[..], b"nested-payload");
let r = buf.as_ptr() as usize..buf.as_ptr() as usize + buf.len();
assert!(
r.contains(&(inner_bytes.as_ptr() as usize)),
"nested bytes field should slice_ref into parent buf"
);
}

// ── JSON: use_bytes_type() + generate_json(true) ─────────────────────────
//
// The bytes_variant build enables both. Runtime support comes from:
Expand Down
38 changes: 38 additions & 0 deletions buffa-types/src/any_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,44 @@ mod tests {
use crate::google::protobuf::Timestamp;
use buffa::Message as _;

#[test]
fn any_view_to_owned_from_source_is_zero_copy() {
use crate::google::protobuf::__buffa::view::AnyView;
use buffa::view::{MessageView as _, OwnedView};

let src = Any {
type_url: "type.googleapis.com/x".into(),
value: bytes::Bytes::from_static(&[1u8; 256]),
..Default::default()
};
let buf = bytes::Bytes::from(src.encode_to_vec());

// Direct trait path: to_owned_from_source(Some(&buf)) → slice_ref.
let view = AnyView::decode_view(&buf).unwrap();
let owned = view.to_owned_from_source(Some(&buf));
assert_eq!(owned.value, src.value);
let value_ptr = owned.value.as_ptr() as usize;
let buf_range = (buf.as_ptr() as usize)..(buf.as_ptr() as usize + buf.len());
assert!(
buf_range.contains(&value_ptr),
"owned.value should point into buf (slice_ref), got {value_ptr:#x} outside {buf_range:#x?}"
);

// OwnedView path: the inherent OwnedView::to_owned_message routes
// through to_owned_from_source(Some(&self.bytes)). This is also the
// regression pin for inherent-method resolution shadowing the
// (full-copy) trait method reachable via Deref.
let ov = OwnedView::<AnyView<'static>>::decode(buf.clone()).unwrap();
let owned2 = ov.to_owned_message();
assert_eq!(owned2.value, src.value);
assert!(buf_range.contains(&(owned2.value.as_ptr() as usize)));

// No-source path still copies (correct, distinct allocation).
let copied = view.to_owned_message();
assert_eq!(copied.value, src.value);
assert!(!buf_range.contains(&(copied.value.as_ptr() as usize)));
}

#[test]
fn pack_and_unpack() {
let ts = Timestamp {
Expand Down
13 changes: 9 additions & 4 deletions buffa-types/src/generated/google.protobuf.any.__view.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions buffa-types/src/generated/google.protobuf.duration.__view.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions buffa-types/src/generated/google.protobuf.empty.__view.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions buffa-types/src/generated/google.protobuf.field_mask.__view.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading