default_value_ref in buffa-descriptor/src/reflect/dynamic.rs answers get() on an unset singular message field by building a value on every call:
FieldKind::Singular(SingularKind::Message(midx)) => ValueRef::Message(ReflectCow::Owned(
alloc::boxed::Box::new(DynamicMessage::new(Arc::clone(pool), midx)),
)),
so each read of an unset message-typed field costs a heap allocation, an Arc clone of the pool (an atomic RMW on a line shared across threads — see the companion issue on the pool's count line), and the matching drop, where the list and map arms of the same function hand out 'static empties for free. A reflective reader that probes optional sub-messages (has-then-get patterns, or readers that treat unset as the empty message per proto3 semantics) pays this per field per message per request.
Options that keep get()'s signature:
- Cache one empty
DynamicMessage per MessageIndex in the pool (built lazily, e.g. a OnceLock<Box<DynamicMessage>> slot per message, or a side Vec filled at pool build) and return ValueRef::Message(ReflectCow::Borrowed(&empty)). The empty message's own pool field is the one wrinkle — an empty message never dereferences it for field storage, so it could hold a Weak or the pool could store the empties without a back-reference; whichever fits the existing invariants.
- Or return a dedicated
ValueRef::EmptyMessage(MessageIndex)-style variant and let ReflectMessage consumers treat it as the default instance, avoiding the self-referential question altogether at the cost of a new variant.
The first keeps every caller working unchanged.
default_value_refinbuffa-descriptor/src/reflect/dynamic.rsanswersget()on an unset singular message field by building a value on every call:so each read of an unset message-typed field costs a heap allocation, an
Arcclone of the pool (an atomic RMW on a line shared across threads — see the companion issue on the pool's count line), and the matching drop, where the list and map arms of the same function hand out'staticempties for free. A reflective reader that probes optional sub-messages (has-then-getpatterns, or readers that treat unset as the empty message per proto3 semantics) pays this per field per message per request.Options that keep
get()'s signature:DynamicMessageperMessageIndexin the pool (built lazily, e.g. aOnceLock<Box<DynamicMessage>>slot per message, or a sideVecfilled at pool build) and returnValueRef::Message(ReflectCow::Borrowed(&empty)). The empty message's ownpoolfield is the one wrinkle — an empty message never dereferences it for field storage, so it could hold aWeakor the pool could store the empties without a back-reference; whichever fits the existing invariants.ValueRef::EmptyMessage(MessageIndex)-style variant and letReflectMessageconsumers treat it as the default instance, avoiding the self-referential question altogether at the cost of a new variant.The first keeps every caller working unchanged.