🔪 Partial Borrows
Zero-overhead "partial borrows", borrows of selected fields only, like &<mut field1, mut field2>MyStruct. It lets you split structs into non-overlapping sets of mutably borrowed fields, similar to slice::split_at_mut, but more flexible and tailored for structs.
📌 TL;DR (Full documentation below)
If you prefer a concise guide over reading the entire README, here's a quick setup demonstrating the core concepts. Important lines are marked with "⚠️". Copy the following code into your lib.rs or main.rs. Adjust the path in the #[module(...)] attribute if using in a module:
use Vec;
use PartialBorrow;
use partial_borrow as p;
use *;
// ============
// === Data ===
// ============
type NodeId = usize;
type EdgeId = usize;
// ⚠️
// ⚠️ USE HERE THE PATH TO MODULE OF THIS FILE
// =============
// === Utils ===
// =============
// Requires mutable access to the `graph.edges` field.
// Requires mutable access to all `graph` fields.
// =============
// === Tests ===
// =============
😵💫 What problem does it solve?
Consider a rendering engine requiring storage for geometries, materials, meshes, and scenes. These entities often form a reference graph (e.g., two meshes can use the same material). To handle this, you can either:
- Use
Rc<RefCell<...>>/Arc<RefCell<...>>for shared ownership, which risks runtime errors. - Store the entities in registries and use their indices as references.
We opt for the latter approach and create a root registry called Ctx:
// === Data ===
// === Registries ===
// === Root Registry ===
Some functions require mutable access to only part of the root registry. Should they take a mutable reference to the entire Ctx struct, or should each field be passed separately? Passing the entire Ctx is inflexible and impractical. Consider the following code:
At first glance, this might seem reasonable, but it 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
Passing each field separately compiles, but becomes cumbersome and error-prone as the number of fields grows:
In real-world applications, this problem often affects API design, making code hard to maintain and understand. This issue was described multiple times over the years, some of the most notable discussions include:
- Rust Internals "Notes on partial borrow".
- The Rustonomicon "Splitting Borrows".
- Niko Matsakis Blog Post "After NLL: Interprocedural conflicts".
- Afternoon Rusting "Multiple Mutable References".
- Partial borrows Rust RFC.
- HackMD "My thoughts on (and need for) partial borrows".
- Dozens of threads on different platforms.
🤩 Partial borrows for the rescue!
This crate provides the partial_borrow macro, which we recommend importing under a shorter alias for concise syntax:
// CURRENT FILE: src/data.rs
use PartialBorrow;
use partial_borrow as p;
use *;
// Current module, see explanation below.
The macro allows you to parameterize borrows similarly to how you parameterize types. It implements the syntax proposed in Rust Internals "Notes on partial borrow", extended with utilities for increased expressiveness:
-
Field References: You can parameterize the reference with field names.
// Immutable reference to `geometry` and mutable reference // to `material`. -
Field Selectors: Use
*to include all fields and!to exclude fields. Later selectors override previous ones.// Immutable reference to all fields except `geometry`. // Immutable reference to `material` and mutable reference // to all other fields. // Mutable reference to all fields. -
Lifetime Annotations: You can specify lifetimes for each reference. If a lifetime is not provided, it defaults to
'_.// Reference to `mesh` with lifetime `'c` and references to // other fields with lifetime `'b`. The inferred lifetime // dependencies are `'a: 'b` and `'a: 'c`. -
Default Lifetime: Provide an alternative default lifetime as the first argument.
// Alias for immutable references to `geometry` and `material` // with lifetime `'t`, and to `mesh` with lifetime `'m`. type GlyphCtx<'t, 'm> = p!; -
Flexible Macro Expansion: Please note that
p!(&<...>MyStruct)always expands to&mut p!(<...>MyStruct), which expands to&mut MyStructRef<...>, a generated struct containing references to fields. This allows for concise type alias syntax.type RenderCtx<'t> = p!; type GlyphCtx<'t> = p!; type GlyphRenderCtx<'t> = ;
Let's apply these concepts to our rendering engine example:
// CURRENT FILE: src/data.rs
use PartialBorrow;
use partial_borrow as p;
use *;
// === Data ===
// === Registries ===
// === Root Registry ===
// Current module, see explanation below.
// Take a ref to `mesh` and mut refs to `geometry` and `material`.
🔋 Batteries Included
Consider the following struct to demonstrate the key tools provided by the macro:
// Current module, see explanation below.
The Ctx struct is equipped with the following methods:
The partially borrowed struct provides borrowing and splitting capabilities:
The partially borrowed struct also provides methods for concatenating partial borrows:
/// The `Union` type is particularly useful when defining
/// type aliases.
type RenderCtx<'t> = p!;
type GlyphCtx<'t> = p!;
type GlyphRenderCtx<'t> = ;
Please note, that while the union operation might seem useful, in most cases it is better to re-structure your code to avoid it. For example, let's consider the previous implementation of render_pass1:
It could also be implemented using explicit split and union, but it would make the code less readable:
👓 #[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, currently, 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;
🛠 How It Works Under the Hood
This macro performs straightforward transformations. Consider the Ctx struct from the example above:
The macro defines a CtxRef struct:
Each type parameter is instantiated with one of &, &mut, or Hidden<T>, a type used to safely hide fields that are not part of the current borrow:
;
The partial_borrow, partial_borrow_rest, and split methods are implemented using inlined pointer casts, with safety guarantees enforced by the type system:
Finally, a helper macro with the same name as the struct is generated and is used by the partial_borrow macro.
⚠️ Limitations
Currently, the macro works only with non-parametrized structures. For parametrized structures, please create an issue or submit a pull request.