Upcoming Rust language features for kernel development
[LWN subscriber-only content]
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!
The Rust for Linux project has been good for Rust, Tyler Mandry, one of the co-leads of Rust's language-design team, said. He gave a talk at Kangrejos 2025 covering upcoming Rust language features and thanking the Rust for Linux developers for helping drive them forward. Afterward, Benno Lossin and Xiangfei Ding went into more detail about their work on the three most important language features for kernel development: field projections, in-place initialization, and arbitrary self types.
Many people have remarked that the development of new language features in Rust
can be quite slow, Mandry said. Partly, that can be attributed to the care the
Rust language team takes to avoid enshrining bad designs. But the biggest reason
is "alignment in attention
". The Rust project is driven by volunteers,
which means that if there are not people focusing on pushing a given feature or
group of related features forward, they languish. The Rust for Linux project has
actually been really helpful for addressing that, Mandry explained, because it
is something that a lot of people are excited about, and that focuses effort
onto the few specific things that the Linux kernel needs.
![Tyler Mandry [Tyler Mandry]](https://static.lwn.net/images/2025/tyler-mandry-kangrejos-small.png)
Mandry then went through a whirlwind list of upcoming language features,
including
types without known size information,
reference-counting improvements,
user-defined function modifiers of the same kind as const, and more.
At the end, he asked which of those were most
important to Rust for Linux, and how the assembled kernel developers would
prioritize them. Beyond the three features to be discussed later,
Lossin said that the project definitely wanted the ability to
write functions that can be evaluated at compile time (called
const
functions in Rust) in trait definitions.
Danilo Krummrich asked for
specialization, which immediately
prompted an "Oh no!
" from Lossin, due to the feature's nearly decade-long
history of causing problems for Rust's type system. Specialization would allow
two overlapping implementations for a single trait to exist, with the compiler
picking the more specific one. Matthew Maurer asked for some ability to control
what the compiler does on integer overflow.
Ultimately, Miguel Ojeda told Mandry that the priority should be on stabilizing the unstable language features that Rust for Linux currently uses, followed by language features that would change how the project structures its code, followed by everything else. The next two talks went into much more detail about the current status and future plans for some of those key language features.
Field projections
Field
projection refers to the idea of taking a pointer to a structure, and turning it
into a pointer to a field of the structure. Rust does already have this for the
built-in reference and pointer types, but it can't always be made to work for
user-defined smart-pointer types. Since the Rust for Linux developers would like
to have custom smart pointers to handle
untrusted data, reference counting,
external locking, and related kernel complications, they would benefit from a
general language feature allowing field projections for all pointer types using
the same syntax.
Lossin spoke about his work on the problem, which has been ongoing
since Kangrejos 2022. There has been "lots of progress
" so far, but the
work is still in the design stage, with a few details left to work out.
The built-in field projections all have the same kind of type signature, Lossin explained. For example, the code for converting a reference to an object into a reference to one of its fields and the code for converting a raw pointer to an object into a raw pointer to one of its fields look different, but have similar signatures:
fn project_reference(r: &MyStruct) -> &Field { &r.field } unsafe fn project_pointer(r: *mut MyStruct) -> *mut Field { unsafe { &raw mut (*r).field } } // The equivalent C code would look like this: struct field *project(struct my *r) { return &(r->field); }
This example uses the relatively recent raw borrow syntax.
The
Pin type throws a bit of a wrench into things.
The Rust
compiler is, by default, free to move structures around for performance reasons.
That doesn't work when the structure is being referenced from the C side, so the
Pin type is used to mark structures that shouldn't be moved.
Projecting a
Pin<MyStruct> Pin<&mut MyStruct>
[Lossin sent LWN a correction: Pin is always used to wrap a pointer
type, not a structure directly]
might produce either a Pin<&mut Field> or a
plain &mut Field depending on whether the field is also of a type that
shouldn't be moved or not. So the most general possible signature for the field projection operation
would be something like this, Lossin said:
Container<'a, Struct> -> Output<'a, Field>
That is, given some pointer type that wraps a structure and must be valid for lifetime a, projecting a field gives a (possibly different) output pointer type wrapping a field of that structure, valid for the same lifetime. Lossin then gave an example of how supporting this could make fully implementing read-copy-update (RCU) support in the kernel's Rust bindings a lot easier.
![Benno Lossin [Benno Lossin]](https://static.lwn.net/images/2025/benno-lossin-kangrejos-small.png)
The RCU mechanism protects readers from concurrent writers, he explained, but it doesn't protect writers from each other. It's somewhat common in the kernel, therefore, to have a mutex protecting some data, with a frequently accessed field of that data being protected by RCU. That way, readers rely on the RCU lock (which is cheap), and writers synchronize with each other using the mutex. Translating that interface to Rust poses problems: Rust doesn't allow any access to the content inside a Mutex without locking it first, so the straightforward translation of this pattern wouldn't work. It would force Rust readers to lock the mutex in order to read the RCU field, which would be an unacceptable performance hit.
With generalized field projection in the language, though, the Rust for Linux developers could write bindings that permit projecting a &Mutex<MyStruct> into an &Rcu<Field> without holding the lock. In driver code, attempting to read from the RCU-protected field would look like a normal access, the same way it is in C — but the compiler would still check that the other, non-RCU-protected data isn't touched without holding the mutex.
Lossin ended by asking the assembled developers to keep an eye on the tracking issue for the feature, and provide feedback on it. Daniel Almeida asked whether testing the feature outside the mainline kernel was really helpful; Ojeda affirmed that it was, because that makes it easier to go to the Rust team and make a case to stabilize the feature. The Rust for Linux project is trying not to use any new unstable features (and to compile with a version of Rust equal to or older than the version packaged on Debian stable), so the feature needs to be completed and make it into Debian 14 (expected in 2027) before it will be widely usable in kernel code.
Andreas Hindborg asked: "Can we have this yesterday, please?
", to general
amusement. The kernel's Rust bindings already feature a plethora of custom
pointers encoding various invariants; this feature, whenever it becomes
available to kernel code, may make them a good deal easier to use in driver code.
Arbitrary self types
Ding gave an update immediately afterward about another ergonomic language feature for custom pointers: arbitrary self types. In Rust, a method on a type can have a first argument that is an object of the type or that is a reference to one. Such a method can be called with the .method() syntax, instead of the more general Type::function() syntax. But the proliferation of smart pointers in kernel Rust code means that the programmer frequently does not have a plain reference; often, they instead have a Pin, an Arc, or some other smart-pointer type.
The arbitrary self types proposal that Ding has been working on would let programmers write methods that take smart pointers, instead of normal references:
impl MyStruct { fn method(self: Pin<&mut MyStruct>) {} }
Unfortunately, adding this to the compiler has not proved to be straightforward. The interaction with Rust's existing Deref trait, which makes custom smart pointers possible in the first place, complicates the implementation because not all of the type information is available while searching for matching methods. Currently, if the user has a Pin<&mut MyStruct> and they call a method on it, the compiler will first look for a matching method for Pin. If one isn't found, it will try to dereference the type, producing a &mut MyStruct. That type is checked for matching methods, and then is dereferenced one final time, producing a MyStruct. That type will finally have a matching method, or else the compiler will emit a type error.
![Xiangfei Ding [Xiangfei Ding]](https://static.lwn.net/images/2025/xiangfei-ding-kangrejos-small.png)
By the time that procedure begins checking functions associated with MyStruct, it will have already discarded information about the wrapping types, which an implementation of arbitrary self types needs. Ding spent a few minutes explaining the approaches for rectifying the problem that he had tried and discarded, before focusing on the current approach. He has added another trait — tentatively called Receiver — that is used to mark types that can be used with arbitrary self types. Then the compiler can try following the chain of Receiver implementations before following the chain of Deref implementations. That does mean that a pointer type will have to opt into being used as an arbitrary self type, but Ding didn't see that as a downside. Letting the author of a pointer type decide when it should support the new feature eliminates a lot of concerns around accidentally introducing backward compatibility problems. For the kernel, it doesn't really impose a barrier, because the Rust developers can just add Receiver implementations as they run across cases that require them.
Ojeda asked how long Ding thought it would take to finalize the arbitrary self types feature; in particular, would it be ready within a year? Ding agreed that a year was possible, although he would need support from the Rust language team in order to make that happen. He wants to run Crater, the tool that the Rust community uses to check whether compiler changes break any published Rust libraries, against his change before submitting the code. Ojeda offered help with obtaining a large build machine to do that, since Ding has had trouble previously with the memory requirements to compile some packages during a Crater run.
In-place initialization
The other topic that Ding wanted to cover was his work on in-place initialization. Like the other new language features being discussed, this doesn't really enable new use cases, but it does make common kernel code cleaner. Currently, Rust code in the kernel uses the pin_init!() macro to create structures that are fixed in place after initialization (by being wrapped in Pin).
There's nothing wrong with pin_init!(): "We love pin_init!()! We
want to make a language feature out of it.
" Adopting a language feature for
in-place initialization would also help with a handful of sharp edges outside
kernel code; it could make creating large
Future values on the heap
more ergonomic, and let some traits become
dyn-compatible. The exact design of
this language feature was more up in the air; Ding covered three different
proposals for how it could work.
The simplest, proposed by Alice Ryhl and Lossin, would be to add a new keyword, init, before a structure-initializing expression in order to ask the compiler to automatically write an implementation of the kernel's PinInit trait. That has the nice benefit of being a fairly minimal change to the language, although it would lock in the use of the PinInit trait in its current form.
Another solution, proposed by Taylor Cramer, would introduce a new type of reference into the language. Rust's existing references can either be read from (&) or read from and written to (&mut). This proposal would add a third type, &out, that can only be written to, not read from. The only way to use an &out reference would be to either write to it, or use projection to break it apart into multiple &out references to various fields. Under this scheme, in-place initialization would look like allocating space on the heap, and then returning an &out reference. The calling code could then fill it in however it wants to, potentially passing off sub-parts to other functions. The compiler would track that the &out references are all used before allowing the code to obtain a normal &mut reference to the heap allocation.
That proposal was considerably less polished than Ryhl and Lossin's approach, however. Ding later told me that he, Mandry, and other compiler contributors at Kangrejos were actually working on figuring out how it would interact with some of the Rust compiler's internals in between talks that day. By the end of the conference, they had a rough idea of how it could be implemented, so a more detailed version of the out-pointer proposal may be forthcoming shortly.
The final design, taking inspiration from C++, would be a form of guaranteed optimization, where constructing a new value and then immediately moving it to the heap causes it to be constructed on the heap in the first place. Ding was less sure about the details of the final proposal; he suggested that the best way forward might be to implement both the PinInit proposal and the out-reference proposal, and see how well each approach works in practice.
Regardless of which approach ends up being chosen, it seems clear that Mandry's point about the Rust for Linux project driving language improvement is correct. While these features are in the early stages, adopting them could significantly simplify code involving user-defined smart pointers, both within and outside the kernel.
Update: Since the talks described in this article, the work on field projection has received an update. Lossin wrote in to inform LWN that all fields of all structures are now considered structurally pinned, so projecting a Pin will now always produce a Pin<&mut Field> or similar value.
Posted Oct 8, 2025 17:39 UTC (Wed)
by NYKevin (subscriber, #129325)
[Link] (2 responses)
To the best of my understanding, this is true but incomplete. Rust generally assumes that it is safe to allow *the developer* to move structures around (for whatever reason they see fit). But there are some structures where doing this would invalidate some invariant or cause other problems. Normally, that would be the developer's problem, but in some cases, those invariants are required to avoid UB, so safe Rust must not allow a move in that case. FFI, as described in the article, is a good example of this. Another is self-referential structures (structures that hold pointers into themselves). The Pin type imposes an exception on this general rule - anything* behind a pinned pointer (a Pin<Ptr> where Ptr: Deref) is considered immovable.
Aside for C++ developers: Because Rust always implements move by memcpy, this is nearly equivalent to saying that all Rust types are trivially movable unless protected by Pin. However, Rust does not run the destructor on the moved-from value (unlike C++, which does), so the vast majority of RAII types can be trivially movable in Rust but not in C++. Pin is only needed for the rare case where the move constructor would do something other than transferring ownership of a resource to the new instance.
What makes Pin especially weird is the fact that it is not a language-level feature at all. It is a standard library feature, implemented entirely in terms of Rust's visibility and borrow-checking rules. In order to accomplish that, it wraps the pointer and makes it unsafe to obtain &mut T (where T is the pointee type), only allowing safe access to &T. Blocking &mut T is necessary because of functions like std::mem::swap() (roughly equivalent to C++'s std::swap()). But that would prevent safe Rust from mutating the pointee at all, so now you have to write a bunch of boilerplate to individually allow specific field mutations (even if the field is private, you probably still want a pub(crate) mutator for internal use). This boilerplate frequently contains a large amount of unsafe Rust to selectively unwrap the Pin, and that is generally considered problematic.
If I'm reading between the lines correctly, this field projection proposal would let you put all of that unsafe boilerplate inside of your smart pointer implementation, so you don't have to keep rewriting it for each new pinnable (non-Unpin) type. That sounds like a pretty big win to me, but I imagine the RCU benefits are probably a bigger deal, since you can macro-generate the Pin boilerplate if you really want to.
***
* Technically, there's an exception to the exception - (pointee) types that implement Unpin are immune to pinning and can be moved regardless. But Unpin is mostly just a convenience trait for generics (it allows them to use pinning pointers without having to worry about whether that's needlessly restrictive for any given pointee type), so for the purposes of this discussion, I'm going to ignore it.
Posted Oct 8, 2025 17:51 UTC (Wed)
by daroc (editor, #160859)
[Link]
In any case, I believe you're correct. If the pin-projection proposal is adopted, a lot of boiler-plate around accessing fields of pinned objects could go away. Instead you'd just use the same field-projection operator as with references, pointers, reference-counted pointers, etc.
Posted Oct 9, 2025 7:19 UTC (Thu)
by lossin (subscriber, #177724)
[Link]
Indeed, the field projection proposal is to have an operator that makes all the unsafe boilerplate obsolete. The wins around Pin are very big, as almost every type in the kernel will end up being pinned. That's because pinning is infectious -- as soon as one of the (transitive) fields of your struct requires it, the entire struct needs to be pinned. Field projections will also improve ergonomics for raw pointers, handling uninitialized data as well as untrusted data and many more user declared types.
The RCU abstractions are a different beast, as without field projections, they probably aren't possible in a safe, ergonomic and performant way. Without it, we'd probably need two different APIs, one unsafe and performant, the other safe & ergonomic. But do note that we could also macro-generate the unsafe boilerplate similar to how it's possible with Pin. The issue is, that means yet another derive macro that one needs to think about (and rather bad ergonomics).
> * Technically, there's an exception to the exception - (pointee) types that implement Unpin are immune to pinning and can be moved regardless. But Unpin is mostly just a convenience trait for generics (it allows them to use pinning pointers without having to worry about whether that's needlessly restrictive for any given pointee type), so for the purposes of this discussion, I'm going to ignore it.
In the current version of the pin-ergonomics proposal, Unpin is going to play a more important role. It will allow coercions between Pin<&mut T> and &mut T if T: Unpin. Then we can just always allow pin projections, so going from Pin<&mut Struct> to Pin<&mut Field> and let the coercions & types handle the not structurally pinned fields.
We still need field projections for pin projections in the kernel: our mutex guard is pinning the data and thus it also needs to provide this kind of projection.
Posted Oct 8, 2025 18:48 UTC (Wed)
by prittner (subscriber, #110534)
[Link] (1 responses)
This is a sorely needed feature for folks who work with large types. The cost of having to reserve lots of stack space, initialize the object, and then copy it to the heap can be significant; in some cases, it's impossible to construct the type on the stack at all due to its size (unless you also blow up your stack size). Inlining can help, but once you have to use dynamic dispatch that goes out the window.
Posted Oct 9, 2025 7:30 UTC (Thu)
by lossin (subscriber, #177724)
[Link]
Very much so. The Asahi GPU driver developed a similar macro-based solution to this problem around the same time we added the pin-init [1] crate that we use to this day. They regularly had structs with hundreds of fields that obviously overflowed the small kernel stack. IIRC, their use-case also wasn't inlined (at least not everywhere), which made it rather painful.
This use-case is part of the core motivation for developing the language feature.
Pin is a bit more complicated than described
Pin is a bit more complicated than described
Pin is a bit more complicated than described
>
> If I'm reading between the lines correctly, this field projection proposal would let you put all of that unsafe boilerplate inside of your smart pointer implementation, so you don't have to keep rewriting it for each new pinnable (non-Unpin) type. That sounds like a pretty big win to me, but I imagine the RCU benefits are probably a bigger deal, since you can macro-generate the Pin boilerplate if you really want to.
Heap initialization is important
Heap initialization is important