Skip to content
Open
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
30 changes: 30 additions & 0 deletions buffa-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,36 @@ impl Config {
self
}

/// Store the matching message-typed oneof variants inline instead of
/// wrapping them in `Box<T>`.
///
/// By default every message/group oneof variant is boxed so that recursive
/// types compile. For non-recursive variants the `Box` is pure overhead (an
/// allocation per construction); this opts the matching variants out.
///
/// Each path is a fully-qualified proto variant path prefix, e.g.
/// `".my.pkg.MyMessage.body.small"` for one variant or `".my.pkg"` for a
/// package (same matching as [`use_bytes_type_in`](Self::use_bytes_type_in)).
/// Opting a *recursive* variant out is rejected at codegen time, since the
/// resulting type would be unsized.
#[must_use]
pub fn unbox_oneof_in(mut self, paths: &[impl AsRef<str>]) -> Self {
self.codegen_config
.unboxed_oneof_fields
.extend(paths.iter().map(|p| p.as_ref().to_string()));
self
}

/// Store every non-recursive message-typed oneof variant inline instead of
/// boxing it. Convenience for `.unbox_oneof_in(&["."])`.
#[must_use]
pub fn unbox_oneof(mut self) -> Self {
self.codegen_config
.unboxed_oneof_fields
.push(".".to_string());
self
}

/// Map `string` fields to a [`StringRepr`] other than `String` for the
/// given proto path prefixes. The string counterpart to
/// [`use_bytes_type_in`](Self::use_bytes_type_in).
Expand Down
13 changes: 13 additions & 0 deletions buffa-codegen/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,19 @@ impl<'a> CodeGenContext<'a> {
.any(|prefix| matches_proto_prefix(prefix, field_fqn))
}

/// Check whether a message-typed oneof variant at the given proto path was
/// opted out of `Box` wrapping via `config.unboxed_oneof_fields`.
///
/// `variant_fqn` is the fully-qualified variant path, e.g.
/// `".my.pkg.MyMessage.body.small"`. Matched with the same
/// proto-segment-aware prefix logic as [`use_bytes_type`](Self::use_bytes_type).
pub fn oneof_unboxed(&self, variant_fqn: &str) -> bool {
self.config
.unboxed_oneof_fields
.iter()
.any(|prefix| matches_proto_prefix(prefix, variant_fqn))
}

/// Resolve the [`StringRepr`](crate::StringRepr) for a `string` field at the
/// given proto path.
///
Expand Down
28 changes: 24 additions & 4 deletions buffa-codegen/src/impl_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2329,10 +2329,24 @@ fn oneof_merge_arm(
preserve_unknown_fields: bool,
use_bytes: bool,
string_repr: crate::StringRepr,
boxed: bool,
) -> TokenStream {
let wire_type = wire_type_token(ty);
let wire_byte = wire_type_byte(ty);
let wire_check = wire_type_check(field_number, &wire_type, wire_byte);
// Message/group variants merge into the existing value. When boxed, the
// binding is `&mut Box<M>` (deref once); when stored inline it is `&mut M`,
// and the freshly decoded value is moved in without a `Box`.
let existing_ref = if boxed {
quote! { &mut **existing }
} else {
quote! { existing }
};
let wrapped_val = if boxed {
quote! { ::buffa::alloc::boxed::Box::new(val) }
} else {
quote! { val }
};
match ty {
Type::TYPE_STRING => {
let decoded = if string_repr.is_default() {
Expand Down Expand Up @@ -2405,12 +2419,12 @@ fn oneof_merge_arm(
if let ::core::option::Option::Some(
#enum_ident::#variant_ident(ref mut existing)
) = self.#field_ident {
::buffa::Message::merge_length_delimited(&mut **existing, buf, depth)?;
::buffa::Message::merge_length_delimited(#existing_ref, buf, depth)?;
} else {
let mut val = ::core::default::Default::default();
::buffa::Message::merge_length_delimited(&mut val, buf, depth)?;
self.#field_ident = ::core::option::Option::Some(
#enum_ident::#variant_ident(::buffa::alloc::boxed::Box::new(val))
#enum_ident::#variant_ident(#wrapped_val)
);
}
}
Expand All @@ -2421,12 +2435,12 @@ fn oneof_merge_arm(
if let ::core::option::Option::Some(
#enum_ident::#variant_ident(ref mut existing)
) = self.#field_ident {
::buffa::Message::merge_group(&mut **existing, buf, depth, #field_number)?;
::buffa::Message::merge_group(#existing_ref, buf, depth, #field_number)?;
} else {
let mut val = ::core::default::Default::default();
::buffa::Message::merge_group(&mut val, buf, depth, #field_number)?;
self.#field_ident = ::core::option::Option::Some(
#enum_ident::#variant_ident(::buffa::alloc::boxed::Box::new(val))
#enum_ident::#variant_ident(#wrapped_val)
);
}
}
Expand Down Expand Up @@ -2489,6 +2503,11 @@ fn generate_oneof_impls(
let field_features = crate::features::resolve_field(ctx, field, features);
let use_bytes = ty == Type::TYPE_BYTES && field_uses_bytes(ctx, proto_fqn, field_name);
let string_repr = field_string_repr(ctx, proto_fqn, field_name);
let boxed = crate::oneof::variant_boxed(
ctx,
ty,
&format!(".{proto_fqn}.{oneof_name}.{field_name}"),
);
merge_arm_list.push(oneof_merge_arm(
&field_ident,
&qualified_enum,
Expand All @@ -2499,6 +2518,7 @@ fn generate_oneof_impls(
preserve_unknown_fields,
use_bytes,
string_repr,
boxed,
));
}

Expand Down
41 changes: 34 additions & 7 deletions buffa-codegen/src/impl_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ use crate::impl_message::{
is_required_field, is_supported_field_type,
};
use crate::message::{is_closed_enum, is_map_field, make_field_ident};
use crate::oneof::is_boxed_variant;
use crate::CodeGenError;

/// Wrap a text-decoded owned `String` expression in the conversion to the
Expand Down Expand Up @@ -181,7 +180,15 @@ pub(crate) fn generate_text_impl(
let oneof_encode: Vec<_> = oneof_groups
.iter()
.map(|(name, enum_ident, fields)| {
oneof_encode_stmt(ctx, enum_ident, name, fields, oneof_prefix, features)
oneof_encode_stmt(
ctx,
enum_ident,
name,
fields,
oneof_prefix,
proto_fqn,
features,
)
})
.collect::<Result<_, _>>()?;
let map_encode: Vec<_> = map_fields
Expand Down Expand Up @@ -762,6 +769,7 @@ fn oneof_encode_stmt(
oneof_name: &str,
fields: &[&FieldDescriptorProto],
oneof_prefix: &TokenStream,
proto_fqn: &str,
parent_features: &ResolvedFeatures,
) -> Result<TokenStream, CodeGenError> {
let field_ident = make_field_ident(oneof_name);
Expand All @@ -777,7 +785,11 @@ fn oneof_encode_stmt(
let variant = crate::oneof::oneof_variant_ident(proto_name);
let ty = effective_type(ctx, field, &features);
let (name_lit, _) = text_field_name(proto_name, field, ty);
let boxed = is_boxed_variant(ty);
let boxed = crate::oneof::variant_boxed(
ctx,
ty,
&format!(".{proto_fqn}.{oneof_name}.{proto_name}"),
);

// Box<M> auto-derefs through `&**__v` → `&M`. For string/bytes,
// `__v: &String` / `&Vec<u8>` / `&bytes::Bytes` deref-coerces.
Expand Down Expand Up @@ -841,20 +853,35 @@ fn oneof_merge_arms(
let (_, name_pat) = text_field_name(proto_name, field, ty);
let use_bytes = ty == Type::TYPE_BYTES && field_uses_bytes(ctx, proto_fqn, proto_name);

// Message/group variants are boxed. Merge-into-existing matches
// binary oneof semantics (oneof_merge_arm in impl_message.rs).
// Message/group variants are boxed unless opted out. Merge-into-existing
// matches binary oneof semantics (oneof_merge_arm in impl_message.rs).
let boxed = crate::oneof::variant_boxed(
ctx,
ty,
&format!(".{proto_fqn}.{oneof_name}.{proto_name}"),
);
let assign = match ty {
Type::TYPE_MESSAGE | Type::TYPE_GROUP => {
let existing_ref = if boxed {
quote! { &mut **__existing }
} else {
quote! { __existing }
};
let wrapped = if boxed {
quote! { ::buffa::alloc::boxed::Box::new(__m) }
} else {
quote! { __m }
};
quote! {
if let ::core::option::Option::Some(
#qualified::#variant(ref mut __existing)
) = self.#field_ident {
dec.merge_message(&mut **__existing)?;
dec.merge_message(#existing_ref)?;
} else {
let mut __m = ::core::default::Default::default();
dec.merge_message(&mut __m)?;
self.#field_ident = ::core::option::Option::Some(
#qualified::#variant(::buffa::alloc::boxed::Box::new(__m))
#qualified::#variant(#wrapped)
);
}
}
Expand Down
11 changes: 11 additions & 0 deletions buffa-codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,16 @@ pub struct CodeGenConfig {
/// `string` variants. Map keys and values always stay `String`, mirroring
/// the bytes path (where map values always stay `Vec<u8>`).
pub string_fields: Vec<(String, StringRepr)>,
/// Fully-qualified proto paths whose message-typed oneof variants should
/// **not** be wrapped in `Box<T>`. By default every message/group oneof
/// variant is boxed (so recursive types compile); entries here opt matching
/// variants out, storing the message inline in the enum.
///
/// Each entry is a proto path prefix matched with the same
/// proto-segment-aware logic as [`bytes_fields`](Self::bytes_fields)
/// (`"."` matches every variant). Opting a *recursive* variant out is
/// rejected at codegen time, since the resulting type would be unsized.
pub unboxed_oneof_fields: Vec<String>,
/// Honor `features.utf8_validation = NONE` by emitting `Vec<u8>` / `&[u8]`
/// for such string fields instead of `String` / `&str`.
///
Expand Down Expand Up @@ -484,6 +494,7 @@ impl Default for CodeGenConfig {
extern_paths: Vec::new(),
bytes_fields: Vec::new(),
string_fields: Vec::new(),
unboxed_oneof_fields: Vec::new(),
strict_utf8_mapping: false,
allow_message_set: false,
generate_text: false,
Expand Down
6 changes: 5 additions & 1 deletion buffa-codegen/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1278,7 +1278,11 @@ fn custom_deser_oneof_group(
proto_name,
field_type,
null_forward: crate::oneof::null_is_valid_value(field),
is_boxed: crate::oneof::is_boxed_variant(field_type),
is_boxed: crate::oneof::variant_boxed(
ctx,
field_type,
&format!(".{proto_fqn}.{oneof_name}.{proto_name}"),
),
enum_ident: &qualified_enum,
result_var: &var_ident,
oneof_name,
Expand Down
Loading
Loading