πͺ struct-split
Efficiently split struct fields into distinct subsets of references, ensuring zero overhead and strict borrow checker compliance (non-overlapping mutable references). Itβs similar to slice::split_at_mut, but tailored for structs.
π΅βπ« Problem
Suppose youβre building a rendering engine with registries for geometry, materials, and scenes. Entities reference each other by ID (usize
), stored within various registries:
Some functions require mutable access to only part of this structure. Should they take a mutable reference to the entire Ctx struct, or should each field be passed separately? The former approach is inflexible and impractical. Consider the following code:
At first glance, this may seem reasonable. However, using it like this:
will be rejected by the compiler:
Cannot borrow `*ctx` as mutable because it is also borrowed as immutable:
| |
| immutable borrow occurs here
| immutable borrow later used here
| for mesh in &scene.meshes
The approach of passing each field separately is functional but cumbersome and error-prone, especially as the number of fields grows:
In real-world use, this problem commonly impacts API design, making code hard to maintain and understand. This issue is also explored in the following sources:
- The Rustonomicon "Splitting Borrows".
- Afternoon Rusting "Multiple Mutable References".
- Rust Internals "Notes on partial borrow".
- Niko Matsakis Blog Post "After NLL: Interprocedural conflicts".
- Partial borrows Rust RFC.
- HackMD "My thoughts on (and need for) partial borrows".
- Dozens of threads on different platforms.
π€© Solution
With struct-split
, you can divide Ctx
into subsets of field references while keeping the types concise, readable, and intuitive.
use Split;
// Take immutable reference to `mesh` and mutable references to both `geometry`
// and `material`.
π #[module(...)]
Attribute
In the example above, we used the #[module(...)]
attribute, which specifies the path to the module where the macro is invoked. This attribute is necessary because, as of now, Rust does not allow procedural macros to automatically detect the path of the module they are used in. This limitation applies to both stable and unstable Rust versions.
If you intend to use the generated macro from another crate, avoid using the crate::
prefix in the #[module(...)]
attribute. Instead, refer to your current crate by its name, for example: #[module(my_crate::data)]
. However, Rust does not permit referring to the current crate by name by default. To enable this, add the following line to your lib.rs
file:
extern crate self as my_crate;
π Generated Macro Syntax
A macro with the same name as the target struct is generated, allowing flexible reference specifications. The syntax follows these rules:
- Lifetime: The first argument can be an optional lifetime, which will be used for all references. If no lifetime is provided, '_ is used as the default.
- Mutability: Each field name can be prefixed with mut for a mutable reference or ref for an immutable reference. If no prefix is specified, the reference is immutable by default.
- Symbols:
*
can be used to include all fields.!
can be used to exclude a field (providing neither an immutable nor mutable reference).
- Override Capability: Symbols can override previous specifications, allowing flexible configurations. For example,
Ctx![mut *, geometry, !scene]
will provide a mutable reference to all fields exceptgeometry
andscene
, with geometry having an immutable reference and scene being completely inaccessible.
π How it works under the hood
This macro performs a set of straightforward transformations. Consider the struct from the example above:
The macro defines a CtxRef
struct:
The Value
type adapts to either a reference, a mutable reference, or an inaccessible private mutable pointer, based on parameterization:
;
pub type Value<'t, L, T> = Value;
The macro generates as_ref_mut
and as_refs
methods for flexible reference creation:
The CtxRef struct provides fit
, fit_rest
, and split
methods:
An extract_$field
method is generated for each field:
Finally, the macro generates the Ctx!
macro itself.
β οΈ Limitations
Currently, the macro works only with non-parametrized structures. For parameterized structures, please create an issue or submit a PR.