🔪 Partial Borrow Implementation
Zero-overhead, safe implementation of partial borrows. This crate allows you to borrow selected fields from a struct and split structs into non-overlapping sets of borrowed fields. The splitting functionality is similar to slice::split_at_mut, but tailored for structs.
😵💫 Problem
Suppose you're building a rendering engine that needs to store geometries, materials, meshes, and scenes. These entities form a reference graph (e.g., two meshes can use the same material). To handle this, you can either:
- Use
Rc/Arcto share ownership, which bypasses Rust's borrow checker and can make your code error-prone at runtime. - 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 ===
// === Global 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 seems reasonable. However, 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 works but becomes cumbersome and error-prone as the number of fields grows:
In real-world applications, this problem affects API design, making code hard to maintain and understand. This issue is also explored in the following sources:
- 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.
🤩 Solution: Partial Borrow
This crate provides the partial_borrow macro, which we recommend importing under a shorter alias for concise syntax:
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!;
Let's apply these concepts to our rendering engine example:
use PartialBorrow;
use partial_borrow as p;
use *;
// Current module, see explanation below.
// Take a ref to `mesh` and mut ref to `geometry` and `material`.
🛠 Batteries Included
Consider the following struct to demonstrate the key tools provided by the macro:
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>:
;
The Hidden<T> type is used to safely hide fields that are not part of the current borrow.
The macro generates as_refs_mut and as_refs methods for flexible reference creation:
The partial_borrow, partial_borrow_rest, and split methods are implemented using inlined pointer casts, with safety guarantees enforced by the type system:
An extract_$field method is generated for each field:
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.