10 releases (3 stable)
Uses new Rust 2024
1.0.2 | Aug 21, 2025 |
---|---|
0.3.0 | Aug 11, 2025 |
0.2.2 | Aug 9, 2025 |
0.1.2 | Aug 7, 2025 |
#261 in Procedural macros
931 downloads per month
440KB
561 lines

A procedural attribute macro to reduce boilerplate when writing functions that partially borrow a struct.
Overview
This crate attempts to make partial or "split" borrows[^1][^2][^3][^4][^5][^6] more ergonomic in Rust – a problem where functions sometimes must borrow individual fields, rather than a single mutable reference to a struct, in order to adhere to borrowing rules, leading to verbose call sites.
With a struct definition like
#[clasma]
struct Mystruct { ... }
adding #[clasma(&<...> Mystruct)]
to a function definition fn foo(..) {}
- extends
foo
's arguments with borrows of some ofMystruct
's fields specified in&<...>
and - generates a macro
foo!
– essentially callable just likefoo
's original signature but taking an additional an instancemystruct
ofMystruct
to partially borrow
#[clasma(&<...> Mystruct)]
fn foo( ... ) { ... }
foo!( ... , mystruct);
expands to
fn foo( ..., <field>: &<field_type>, <field>: &<field_type>... ) { ... }
foo( ... , &mystruct.<field>, &mystruct.<field>);
where, again, the fields are specified between &<...>
.
[^1]: Rust Internals "Notes on partial borrow". [^2]: The Rustonomicon "Splitting Borrows". [^3]: Niko Matsakis Blog Post "After NLL: Interprocedural conflicts". [^4]: Afternoon Rusting "Multiple Mutable References". [^5]: Partial borrows Rust RFC. [^6]: HackMD "My thoughts on (and need for) partial borrows".
Installation
[!IMPORTANT]
clasma
requires#[feature(decl_macro)]
This is in order to ensure, that
path::to::foo!
expands topath::to::foo
as opposed to justfoo
.macro_rules!
macros are unhygienic in this regard.
cargo add clasma
Motivating Example
Imagine a Tourist
with a list of travel destinations, that counts the number of times he visits one of his destinations.
struct Tourist {
n_visits: u32,
destinations: Vec<String>
}
Before
Without partial borrowing, this code does not compile.
impl Tourist {
fn visit(&mut self, dest: &String) {
if self.destinations.contains(dest) {
self.n_visits += 1;
}
}
fn visit_all(&mut self) {
for dest in &self.destinations { // <- immutable borrow occurs here
self.visit(dest); // ERROR: `*self` is also borrowed as immutable
}
println!("Visited {} countries", self.destinations.len());
}
}
The issue is that visit
unnecessarily borrows self.destinations
mutably when it only needs an immutable borrow. A workaround would be to borrow each struct field separately on every call to visit
:
impl Tourist {
fn visit(destinations: &Vec<String>, n_visits: &mut u32, dest: &String) {
if destinations.contains(dest) {
*n_visits += 1;
}
}
fn visit_all(&mut self) {
for dest in &self.destinations {
Self::visit(&self.destinations, &mut self.n_visits, dest);
}
println!("Visited {} countries", self.destinations.len());
}
}
However, one can imagine how tedious it would get, as the number of fields increases.
With clasma
The clasma
macro handles borrowing and argument passing.
#[clasma]
struct Tourist { ... }
#[clasma]
impl Tourist {
#[clasma(&<destinations, mut n_visits> Tourist)]
fn visit(dest: &String) {
if destinations.contains(dest) {
*n_visits += 1;
}
}
// ...
fn visit_all(&mut self) {
for dest in &self.destinations {
visit!(self, dest); // Now calls to `visit` become more concise
}
println!("Visited {} countries", self.destinations.len());
}
}
Usage
Consider the following struct:
#[clasma]
struct Mystruct {
a: A,
b: B,
c: C,
}
let mut mystruct = Mystruct {
a: A::new(),
b: B::new(),
c: C::new(),
};
The #[clasma(&<...> Mystruct)]
attribute proc-macro expands foo
's signature with the fields specified between &<...>
. For example,
#[clasma(&<mut a, b> Mystruct)]
fn foo(some_arg: u8) {
// ...
}
expands to
fn foo(some_arg: u8, a: &mut A, b: &B) {
// ...
}
Additionally, #[clasma(&<...> Mystruct)]
generates a macro foo!
. The first arguments to foo!
are the arguments of foo
's original signature (some_arg: u8
) and the final argument is an instance of Mystruct
. foo!(..., mystruct)
borrows each of mystruct
's fields individually, passing them into foo
's corresponding arguments.
foo!(3, mystruct);
expands to
foo(3, &mut mystruct.a, &mystruct.b);
One can also optionally provide generic type parameters inside angle brackets <...>
:
#[clasma(&<a, b> Mystruct)]
fn foo<T>(some_arg: T) {
// ...
}
foo!(<&str>, "hello", mystruct);
expands to
fn foo<T>(some_arg: T, a: &A, b: &B) {
// ...
}
foo::<&str>("hello", &mystruct.a, &mystruct.b);
#[clasma(&<...> Mystruct)]
supports adding lifetime annotations to partially borrowed fields in &<...>
, and one can likewise call foo!
with lifetime parameters:
const mystruct: Mystruct = Mystruct { ... };
#[clasma(&<'t a, 't b> Mystruct)]
fn foo<'t: 'static, T>(other_arg: &'t u8) {
// ...
}
foo!(<'static>, &3, mystruct);
expands to
const mystruct: Mystruct = Mystruct { ... };
fn foo<'t: 'static, T>(other_arg: &'t u8, a: &'t A, b: &'t B) {
// ...
}
foo::<'static>(&3, &mystruct.a, &mystruct.b);
In addition to enumerating field names in &<...>
manually, one can also use a wildcard *
to specify all fields, and !
to exclude a field.
For example, &<'t mut a, 's *, mut b, !c> Mystruct
is equivalent to &<'s a, 's b> Mystruct
.
Generic structs
#[clasma]
struct Mystruct<T,U> {
a: Vec<T>,
b: Vec<(T,U)>,
c: U,
}
#[clasma(&<*> Mystruct<T, bool>)]
fn foo<U>(some_arg: U) {
// ...
}
let mystruct = Mystruct {
a: vec!["hi"],
b: vec![("bye", false)],
c: true,
}
foo!(<&str>, "hello", mystruct);
expands to
struct Mystruct<T,U> { ... }
fn foo<U>(some_arg: T, a: &Vec<U>, b: &Vec<(U,bool)>, c: &bool) {
// ...
}
let mystruct = Mystruct {
a: vec!["hi"],
b: vec![("bye", false)],
c: true,
}
foo::<&str>("hello", &mystruct.a, &mystruct.b, &mystruct.c);
Propagating from scope
In addition to partially borrowing fields from a struct instance, foo!
also accepts ..
in place of the struct instance. The arguments to foo
previously supplied by mystruct
, are now read directly from the local scope at the call site.
#[clasma(&<a, mut c> Mystruct)]
fn foo<T>(some_arg: T) {
// ...
}
#[clasma(&<*> Mystruct)]
fn bar() {
let c = &mut C::new();
foo!(<u8>, 3, ..); // supplies the fields of `Mystruct` from local scope
}
expands to
fn foo<T>(some_arg: T, a: &A, c: &mut C) {
// ...
}
fn bar(a: &A, b: &B, c: &C) {
let c = &mut C::new();
foo::<u8>(3, a, c);
}
Partially borrowing several structs
Presuming that two structs have non-overlapping fields, one can partially borrow both structs with #[clasma(&<...> Struct1, &<...> Struct2)]
:
#[clasma]
struct Other {
o1: A,
o2: B,
o3: C,
}
#[clasma(&<a, mut c> Mystruct, &<mut o2, o3> Other)]
fn foo<T>(some_arg: T) {
// ...
}
let a = &A();
let c = &mut C();
let mut other = Other {
o1: A::new(),
o2: B::new(),
o3: C::new(),
};
foo!(<u8>, 3, .., other); // supplies the fields of `Mystruct` locally and fields of `Other` from `other`
expands to
struct Other { ... }
fn foo<T>(some_arg: T, a: &A, c: &mut C, o2: &mut B, o3: &C) {
// ...
}
let a = &A();
let c = &mut C();
let mut other = Other {
o1: A::new(),
o2: B::new(),
o3: C::new(),
};
foo::<u8>(3, a, c, &mut other.o2, &other.o3);
impl
blocks
For impl
blocks, a #[clasma]
attribute must go on top of the impl
. One can then annotate inner functions with #[clasma(&<...> Mystruct)]
like usual:
#[clasma]
impl Mystruct {
// ...
#[clasma(&<mut a, b) Mystruct]
fn foo(some_arg: u8) {
// ...
}
#[clasma(&<mut *, b)]
fn bar(other_arg: u8) {
foo!(other_arg, ..);
}
}
foo!(3, mystruct);
expands to
impl Mystruct {
// ...
fn foo(some_arg: u8, a: &mut A, b: &B) {
// ...
}
#[clasma(&<mut *, b)]
fn bar(other_arg: u8, a: &mut A, b: &B, c: &mut C) {
Mystruct::foo(other_arg, a, b)
}
}
Mystruct::foo(3, &mut mystruct.a, &mystruct.b);
If both the type and the function in the impl
block are generic, one can provide the arguments separated by ::
like so:
#[clasma]
impl<T> Mystruct<T> {
// ...
#[clasma(&<mut a, b> Mystruct<T>)]
fn foo<U>(a: &mut A, b: &B, some_arg: U, other_arg: T) {
// ...
}
}
foo!(<&str>::<u8>, 3, "hello", mystruct);
// expands to:
// Mystruct::<&str>::foo::<u8>(3, "hello", &mut mystruct.a, &mystruct.b);
If only the type is generic, one can omit the second pair of angle brackets and pass the arguments like this:
foo!(<T>::, ...)