Welcome to LWN.net
The following subscription-only content has been made available to you by an LWN subscriber. Thousands of subscribers depend on LWN for the best news from the Linux and free software communities. If you enjoy this article, please consider subscribing to LWN. Thank you for visiting LWN.net!
Rust has a number of kinds of smart pointers, both in the standard library and defined by users. Still, some operations that are possible with built-in references are not possible to perform with user-defined smart pointers. Tyler Mandry, lead of the Rust project's language team, spoke at RustConf 2026 about the lengthy effort to change that, and make smart pointers just as flexible as built-in references.
He has been working on this problem all through 2026, Mandry said, alongside several other interested members of the language team, under the project name "Beyond the &". It took a good deal of thought, but the design that they have arrived at should allow a substantially simpler mental model of how pointers and references work. To illustrate the problem that this design solves, he put up an example of a simplified Rust program that calculates some dynamic content and caches it in a hash map. The example uses Rust's map-entry API, which provides a function called or_insert_with() that takes a callback to populate the hash-map entry if it is empty.
struct RenderState {
cache: HashMap<String, Arc<String>>,
template: String,
}
fn render_page(state: &mut RenderState, name: &str) -> Arc<String> {
// Mutably borrow `state.cache` using the Entry API.
let entry = state.cache.entry(name.to_string());
entry.or_insert_with(|| {
// Create the entry if it doesn't exist.
Arc::new(state.template.replace("$name", name))
}).clone()
}
This code looks up a cache key (name) in the hash map, and either returns a copy of the cached value, or calculates it by substituting name into a template if it doesn't exist. The shared state is accessed using a built-in mutable reference, &mut RenderState. The example works, but it can't safely be shared between threads. Adding a mutex, to allow the cache to be shared, causes the borrow checker to reject the program:
fn render_page(state: &Mutex<RenderState>, name: &str) -> Arc<String> {
let state: MutexGuard <' _ , RenderState> = state.lock().unwrap();
// The borrow checker complains that "state" is mutably borrowed here:
let entry = state.cache.entry(name.to_string());
entry.or_insert_with(|| {
// ... but then used here while it is still borrowed:
Arc::new(state.template.replace("$name", name))
}).clone()
}
The original code worked, Mandry said, because the borrow checker was able to track that state.cache and state.template were separate fields, so it was safe to access them at the same time. With the addition of the mutex, the borrow checker just sees opaque accesses to a MutexGuard structure, and can no longer tell that the accesses are disjoint — it has to consider the case where the mutable borrow is used to edit the state at the same time that the callback reads from it, causing a potentially invalid data race. A simple fix in this case is to dereference the MutexGuard once, and reborrow the structure behind it:
let state: &mut RenderState = &mut *state.lock().unwrap();
This constructs a new built-in reference, just like the original code used, so the borrow checker can once again see that the accesses don't interfere with each other. That works, but it's not particularly intuitive. Mandry said that complexity like this contributes to Rust's difficult learning curve. It would be better if user-defined pointer types such as MutexGuard behaved more like built-in references.
Pointer types are everywhere, he continued. And all of them have slightly different semantics. Some pointers can't be safely dereferenced (raw pointers), or can be written to but not read from (MaybeUninit), etc. This demonstrates Rust's versatility, but it also makes it hard to come up with a design that works for the many possible pointer types.
The solution the language team settled on, based on the work of Nadrieril and Benno Lossin, was to expose the compiler's internal notion of a "place" to user code. A place is Rust's equivalent of C's left-hand sides (lvalues): a location that values can be read from or written to. The difference between a place and a pointer is that a place is an abstract expression that exists at compile time, such as state.cache, and a pointer is one way to represent a place at run time.
The idea is to create a new handle type for every smart pointer. Often the handle will simply be a wrapper around an unsafe pointer to the same location. Then, the compiler will automatically create handles to represent places referenced by the program. That allows user code to implement traits on a handle type that will affect how the borrow checker interacts with handles which correspond to a custom smart pointer, with a higher degree of flexibility than the existing Deref and DerefMut traits. As an example of what that would look like, Mandry showed how a library author might go about teaching the borrow checker how to handle writes to places that are accessed via a NonNull pointer:
unsafe impl<T> WritePlace for NonNullHandle<T> {
const SAFE: bool = false;
unsafe fn write_place(self, value: T) {
unsafe { self.0.write(value) }
}
}
The WritePlace trait would be used to tell the borrow checker that a particular kind of handle (in this case, a handle referencing a NonNull pointer) can be written to, and how. When SAFE is set to false, the borrow checker treats writing to the associated place as an unsafe operation. The actual write is forwarded through to the raw pointer that NonNull wraps. The whole trait implementation is unsafe because an incorrect implementation of the trait could cause the borrow checker to make a mistake and result in unsound behavior.
A corresponding ReadPlace trait encodes how to read from a handle. More interesting are ProjectPlace and BorrowPlace. The former tells the borrow checker how to turn a place containing a structure into a place containing one of its fields. For example, how to turn (the handle for) a MutexGuard<RenderState> into a MutexGuard<String> when the programmer writes state.template. The latter tells the borrow checker how to create a new smart pointer that borrows a given place, the same way that & works for built-in references.
The language team is still debating what the syntax for creating a new smart pointer with BorrowPlace should be. While it could use the same & symbol, that might be confusing and make it harder to use type inference. One proposal is to use an @ symbol instead, but people aren't entirely happy with that either. No matter what syntax is eventually decided on, the BorrowPlace trait encodes all of the information that the borrow checker needs to know in order to safely handle the pointer type. This means that currently built-in behavior can be defined via the same mechanism. For example, this is what an implementation for the built-in reference type &T would look like:
unsafe impl<'a, T> BorrowPlace<&'a T> for RefHandle<'a, T> {
// Tell the borrow checker that multiple references can be made to the
// same place at the same time:
const ACCESS: AccessKind = AccessKind::Shared;
// And that the borrow lasts for the lifetime 'a:
type Timing = Lifetime<'a>;
// And that creating references is a safe operation:
const SAFE: bool = true;
unsafe fn borrow(self) -> &'a T {
unsafe { &*self.ptr.as_ptr() }
}
}
That gives users an example of how to write their own smart pointers that act
like references, and gives the standard library maintainers a place to document
existing counterintuitive built-in behaviors.
With a similar implementation of a handle type for MutexGuard,
the earlier render_page() example
works without errors. Mandry called it "a small code change for this example,
but a big semantic shift.
"
Tying smart pointers more closely to the compiler's existing internals has other benefits, however. Currently, it is possible to use pattern matching on a value behind a reference, but not on a value behind a smart pointer — at least without dereferencing the pointer and reborrowing the value behind it. The details exposed by BorrowPlace would be enough to let the compiler safely implement those pattern matches.
One of the criteria that the language team looks for in new features is
composability, Mandry said: how well the prospective feature integrates into
the existing structures of the language, and how well multiple uses of the
feature compose with each other. Handles and places "compose beautifully
"
because it is just exposing some details of how the compiler already
understands and processes the language.
Despite that, exposing places and handles to library code is still a
prototype. Mandry asked for help ensuring that the design would work for
everyone's use cases; he asked the audience to look at
the design documentation and add their own examples of smart pointers with
weird semantics. "If you have an abstraction that you want to have more
deeply integrated into the language, please help us out by trying this, and
telling us about any roadblocks.
"
The next part of the design that he intends to flesh out is errors and
diagnostic messages. Ideally, users should never see errors mentioning the newly
added traits; those would remain internal, and the error messages would explain
the problem directly, as they do for built-in references. Even though there is
much more work to do before it can become a stable part of the language,
Mandry is optimistic about this design. "My hope is that it will let
libraries make Rust even more powerful and friendly.
"
One audience member wanted to know whether this design would also support destructive pattern matching (a combined operation that takes ownership of a value while matching it against possible patterns, pulling it apart into its component pieces). Mandry said that it would, as long as the developer implemented a VariantPlace trait for the appropriate handle. There are six to eight operations to implement to support every operation on a handle type, and that's one of them, he said.
Another audience member asked whether the
design would also work for enumerations in general. Mandry paused, stared out
into space for a moment, and then let out a hesitant and elongated yes.
Projecting a field from an enumeration in general will probably not be possible,
he elaborated, but there has been some exploratory work in that direction. For
example, should it be possible to project a field from inside an
Option
to obtain an Option of the field value? "I don't know the answer,
but it's an interesting question,
" Mandry remarked, just before time for the
session ran out.
[ Thanks to the Linux Foundation, LWN's travel sponsor, for assistance in traveling to Montreal for RustConf. ]
| Index entries for this article | |
|---|---|
| Conference | RustConf/2026 |