Expand description
Rich support to safely create instances of Dynamically Sized Types.
This crate lets you allocate variable data inline at the end of a struct. If you have a struct that gets allocated on the heap and has some variable-length data associated with it (like a string or an array), then you can allocate this data directly inline with the struct. This saves memory by avoiding the need for a pointer and a separate allocation, and saves CPU cycles by eliminating the need for indirection when accessing the data.
Rust supports the notion of Dynamically Sized Types, known as DSTs, which are types that have a size not known at compile time. DSTs are perfect to implement flexible array members. But unfortunately, Rust doesn’t provide an out-of-the-box way to allocate instances of such types. This is where this crate comes in.
You can apply the #[make_dst_factory] attribute to your DST structs, which generates factory
functions that let you easily and safely create instances of your DSTs.
§Why Should You Care?
Dynamically sized types aren’t for everyone. You can’t use them as local variables or put them in arrays or vectors, so they can be inconvenient to use. However, their value lies in situations where you have a lot of heap-allocated objects, as they can substantially reduce the memory footprint of your application. If you’re building graphs, trees, or other dynamic data structures, you can often leverage DSTs to keep your individual nodes smaller and more efficient.
§Where to Use DSTs?
It can be hard or tedious to discover where in your codebase there are opportunities to use DSTs. You can give the following prompt to your favorite AI to have it find candidate structs that can be upgraded to a DST.
Analyze this Rust codebase for DST (Dynamically Sized Type) optimization opportunities using the dst_factory crate pattern.
A DST struct has one trailing unsized field that is co-located with the struct header in a single allocation (behind Arc, Box, or Rc), eliminating one heap indirection. There are three forms:
- String field → trailing str (eliminates the String's heap buffer)
- Vec<T> field → trailing [T] (eliminates the Vec's heap buffer)
- Box<dyn Trait> field → trailing dyn Trait (eliminates the Box's heap allocation and pointer indirection)
A struct is a candidate if ALL of these are true:
1. It has at least one String, Vec<T>, or Box<dyn Trait> field
2. It is used behind Arc<T>, Box<T>, or Rc<T> — NOT stored inline in a Vec, HashMap, array, or by value on the stack
3. The candidate field is not resized, replaced, or swapped after construction. In-place mutation of elements (e.g., writing to [u8] slots, calling &mut self methods on a dyn Trait) is fine — only
operations that change the length or swap the entire value are disqualifying (e.g., push(), pop(), clear(), resize(), reassigning the field to a new String/Vec/Box<dyn>)
A struct with multiple candidate fields is still a valid candidate — the user picks one field as the trailing unsized tail, the others stay as-is. When listing candidates, note ALL eligible tail fields and
recommend which one to pick (typically the largest or most frequently populated).
For each crate, systematically:
1. Find all struct definitions (both pub and private) that have String, Vec<T>, or Box<dyn Trait> fields
2. For each, grep for Arc<StructName>, Box<StructName>, Vec<StructName> to determine usage pattern
3. Check whether candidate fields are resized, replaced, or swapped after construction (in-place element mutation is OK)
4. Report: file path, full struct definition, all candidate tail fields, usage pattern (Arc/Box/Vec/value), mutability assessment, and recommended tail choice
Exclude:
- Structs stored in Vec<T> or HashMap values (DSTs are !Sized)
- Structs where the candidate field is resized or replaced after construction (e.g., push(), clear(), field reassignment)
- Structs not behind Arc/Box/Rc (no allocation to optimize)
- Test-only structs
Output format per candidate:
### StructName
File: path/to/file.rs:line
Arc/Box usage: N sites (list key files)
Candidate tail fields:
- field_name: Type → unsized_type (recommended: yes/no, reason)
- field_name: Type → unsized_type
Resized/replaced after construction: no (cite evidence) / yes (disqualifying method)
Volume: how often constructed per request/operation
Savings: 1 allocation per instance × volume§Examples
Here’s an example using an array as the last field of a struct:
use dst_factory::make_dst_factory;
#[make_dst_factory]
struct User {
age: u32,
signing_key: [u8],
}
// allocate one user with a 4-byte key
let a = User::build(33, [0, 1, 2, 3]);
// allocate another user with a 5-byte key
let b = User::build_from_slice(33, &[0, 1, 2, 3, 4]);
// allocate another user, this time using an iterator
let v = vec![0, 1, 2, 3, 4];
let c = User::build(33, v.iter().copied());
// destructure this user and compare its key to the vector
// this has the advantage of iterating over u8, not &u8 or &mut u8.
let (_age, signing_key) = User::destructure(c);
assert!(signing_key.eq(v.into_iter()));Here’s another example, this time using a string as the last field of a struct:
use dst_factory::make_dst_factory;
#[make_dst_factory]
struct User {
age: u32,
name: str,
}
// allocate one user with a 5-character string
let a = User::build(33, "Alice");
// allocate another user with a 3-character string
let b = User::build(33, "Bob");And finally, here’s an example using a trait object as the last field of a struct:
use dst_factory::make_dst_factory;
// a trait we'll use in our DST
trait NumberProducer {
fn get_number(&self) -> u32;
}
// an implementation of the trait we're going to use
struct FortyTwoProducer;
impl NumberProducer for FortyTwoProducer {
fn get_number(&self) -> u32 {
42
}
}
// another implementation of the trait we're going to use
struct TenProducer;
impl NumberProducer for TenProducer {
fn get_number(&self) -> u32 {
10
}
}
#[make_dst_factory]
struct Node {
count: u32,
producer: dyn NumberProducer,
}
// allocate an instance with one implementation of the trait
let a = Node::build(33, FortyTwoProducer{});
assert_eq!(42, a.producer.get_number());
// allocate an instance with another implementation of the trait
let b = Node::build(33, TenProducer{});
assert_eq!(10, b.producer.get_number());Because DSTs don’t have a known size at compile time, you can’t store them on the stack, and you can’t pass them by value. As a result of these constraints, the factory functions return smart-pointer-wrapped instances of the structs.
§Smart Pointers
The macro generates factory functions for three smart pointer types:
| Pointer | Factory suffix | Use case |
|---|---|---|
Box<T> | (none) | Unique ownership |
Arc<T> | _arc | Shared ownership, thread-safe (atomic refcount) |
Rc<T> | _rc | Shared ownership, single-threaded (non-atomic refcount) |
use dst_factory::make_dst_factory;
use std::sync::Arc;
use std::rc::Rc;
#[make_dst_factory]
struct User {
age: u32,
name: str,
}
// Unique ownership
let boxed: Box<User> = User::build(33, "Alice");
// Thread-safe shared ownership
let shared: Arc<User> = User::build_arc(33, "Bob");
let clone = Arc::clone(&shared);
assert_eq!(&clone.name, "Bob");
// Single-threaded shared ownership
let local: Rc<User> = User::build_rc(33, "Carol");
let clone = Rc::clone(&local);
assert_eq!(&clone.name, "Carol");
// Convert an existing Box<User> into Arc<User> or Rc<User>
let boxed: Box<User> = User::build(33, "Dora");
let shared: Arc<User> = User::into_arc(boxed);
assert_eq!(&shared.name, "Dora");As shown above, in addition to the factory methods, the macro always generates the into_arc and into_rc
associated functions that convert a Box<Self> into Arc<Self> or Rc<Self> while
preserving the inline DST layout.
§Attribute Features
The common use case for the #[make_dst_factory] attribute is to not pass any arguments.
This results in functions called build, build_arc, and build_rc when using a string
or dynamic trait as the last field of the struct, and additionally build_from_slice,
build_arc_from_slice, build_rc_from_slice, and destructure when using an array as the
last field of the struct.
The generated functions are private by default and have the following approximate signatures:
// for slices
fn build<G>(field1, field2, ..., last_field: G) -> Box<Self>
where
G: IntoIterator<Item = last_field_type>,
<G as IntoIterator>::IntoIter: ExactSizeIterator,
fn build_from_slice(field1, field2, ..., last_field: &[last_field_type]) -> Box<Self>
where
last_field_type: Copy + Sized;
fn build_arc<G>(field1, field2, ..., last_field: G) -> Arc<Self>
where
G: IntoIterator<Item = last_field_type>,
<G as IntoIterator>::IntoIter: ExactSizeIterator,
fn build_arc_from_slice(field1, field2, ..., last_field: &[last_field_type]) -> Arc<Self>
where
last_field_type: Copy + Sized;
fn build_rc<G>(field1, field2, ..., last_field: G) -> Rc<Self>
where
G: IntoIterator<Item = last_field_type>,
<G as IntoIterator>::IntoIter: ExactSizeIterator,
fn build_rc_from_slice(field1, field2, ..., last_field: &[last_field_type]) -> Rc<Self>
where
last_field_type: Copy + Sized;
fn destructure(this: Box<Self>) -> (Type1, Type2, ..., SelfIter);
// when zeroable flag is set (slice tails only, requires bytemuck)
fn build_zeroed(field1, field2, ..., len: usize) -> Box<Self>
where
last_field_type: bytemuck::Zeroable;
fn build_arc_zeroed(field1, field2, ..., len: usize) -> Arc<Self>
where
last_field_type: bytemuck::Zeroable;
fn build_rc_zeroed(field1, field2, ..., len: usize) -> Rc<Self>
where
last_field_type: bytemuck::Zeroable;
// for strings
fn build(field1, field2, ..., last_field: impl AsRef<str>) -> Box<Self>;
fn build_arc(field1, field2, ..., last_field: impl AsRef<str>) -> Arc<Self>;
fn build_rc(field1, field2, ..., last_field: impl AsRef<str>) -> Rc<Self>;
// for trait objects
fn build(field1, field2, ..., last_field: G) -> Box<Self>
where
G: TraitName + Sized;
fn build_arc(field1, field2, ..., last_field: G) -> Arc<Self>
where
G: TraitName + Sized;
fn build_rc(field1, field2, ..., last_field: G) -> Rc<Self>
where
G: TraitName + Sized;
// generated for all DSTs
fn into_arc(this: Box<Self>) -> Arc<Self>;
fn into_rc(this: Box<Self>) -> Rc<Self>;The attribute lets you control the name of the generated functions, their
visibility, and whether to generate code for the no_std environment, along with
which traits to automatically implement for your type. The general grammar is:
#[make_dst_factory(
<base_factory_name>
[, destructurer=<destructurer_name>]
[, iterator=<iterator_name>]
[, generic=<generic_name>]
[, <visibility>]
[, no_std]
[, deserialize]
[, clone]
[, debug]
[, eq]
[, ord]
[, hash]
[, zeroable]
)]Some examples:
// Make all generated functions public.
#[make_dst_factory(pub)]
// Custom base name for the generated functions giving `create`, `create_from_slice`, `create_arc`, `create_arc_from_slice`,
// `create_rc`, and `create_rc_from_slice`.
#[make_dst_factory(create)]
// Custom destructurer name.
#[make_dst_factory(create, destructurer = destroy)]
// Public functions with custom name.
#[make_dst_factory(create, pub)]
// Support the `no_std` environment.
#[make_dst_factory(create, no_std)]
// Custom generic type name.
#[make_dst_factory(create, no_std, generic=X)]§Trait Implementations
Rust’s standard #[derive(...)] often doesn’t work for DST structs. The #[make_dst_factory] attribute provides
flags to generate these trait implementations:
| Flag | Trait(s) generated | Notes |
|---|---|---|
clone | Clone for Box<T> | Deep copy via factory function |
debug | Debug for the struct | Named fields or tuple formatting |
eq | PartialEq and Eq for the struct | Compares all fields |
ord | PartialOrd and Ord for the struct | Compares all fields lexicographically |
hash | Hash for the struct | Hashes all fields |
When the last field of the struct is a dyn Trait, the generated where clauses require the trait object to
implement the relevant trait (e.g. dyn MyTrait: Debug). The clone flag is the
exception; it is not supported for dyn Trait since there is no way to clone
a concrete type through a trait object reference.
use dst_factory::make_dst_factory;
#[make_dst_factory(clone, debug, eq, ord, hash)]
struct Message {
id: u32,
text: str,
}
let msg = Message::build(1, "hello");
let cloned = msg.clone();
assert_eq!(msg, cloned);
assert_eq!(format!("{:?}", &*msg), "Message { id: 1, text: \"hello\" }");§Zero-Initialized Buffers
When the last struct field is a [T], the zeroable flag generates build_zeroed, build_arc_zeroed,
and build_rc_zeroed factories that allocate the DST with a zero-initialized slice of a
given length.
use dst_factory::make_dst_factory;
#[make_dst_factory(zeroable)]
struct Buffer {
cursor: usize,
data: [u8],
}
// Allocate a 1MB buffer with zero-initialized payload — no per-element initialization.
let buf = Buffer::build_zeroed(0, 1_000_000);
assert_eq!(buf.data.len(), 1_000_000);
assert!(buf.data.iter().all(|&b| b == 0));§Serde Support
DST structs work naturally with serde’s #[derive(Serialize)], since serialization
only requires a reference. However, #[derive(Deserialize)] does not work with DSTs,
so special support is needed instead.
Passing the deserialize flag in the attribute generates a
Deserialize implementation
for Box<T>.
use dst_factory::make_dst_factory;
use serde::Serialize;
#[derive(Serialize)]
#[make_dst_factory(deserialize)]
struct Message {
id: u32,
text: str,
}
// Serialize
let msg = Message::build(1, "hello");
let json = serde_json::to_string(&*msg).unwrap();
// Deserialize
let restored: Box<Message> = serde_json::from_str(&json).unwrap();
assert_eq!(restored.id, 1);
assert_eq!(&restored.text, "hello");Rust’s orphan rules prevent implementing Deserialize for Arc<T> or Rc<T>.
Instead, the deserialize flag generates helper functions deserialize_arc and
deserialize_rc that can be used with serde’s #[serde(deserialize_with = "...")]
attribute:
use dst_factory::make_dst_factory;
use serde::{Serialize, Deserialize};
use std::sync::Arc;
use std::rc::Rc;
#[derive(Serialize)]
#[make_dst_factory(deserialize)]
struct Message {
id: u32,
text: str,
}
#[derive(Serialize, Deserialize)]
struct Dashboard {
#[serde(deserialize_with = "Message::deserialize_arc")]
shared_msg: Arc<Message>,
#[serde(deserialize_with = "Message::deserialize_rc")]
local_msg: Rc<Message>,
}Deserialization is not supported when the last field of the struct is a dyn Trait
since there is no way to reconstruct the concrete type from serialized data.
§Other Features
You can use the #[make_dst_factory] attribute on structs with the normal Rust
representation or C representation (#[repr(C)]), with any padding and alignment
specification. See the Rust reference on Type Layout
for more details.
§Error Conditions
The #[make_dst_factory] attribute produces a compile-time error if:
- It’s applied to anything other than a regular struct or a tuple struct.
- Its arguments are malformed (e.g., incorrect visibility keyword, too many arguments, etc.).
- The struct has no fields.
- The last field of the struct is not a slice (
[T]), a string (str), or a trait object (dyn Trait). - The resulting struct exceeds the maximum size allowed of
isize::MAX. - The
deserializeorcloneflags are used on a struct whose last field is a trait object. - The
zeroableflag is used on a struct whose last field is not a slice ([T]).
§Acknowledgments
Many thanks to https://github.com/scottmcm for his invaluable help getting the factory methods in top shape.
Attribute Macros§
- make_
dst_ factory - Generate factory functions for dynamically sized types (DST) structs.