From 989b3f38a9be46178950f86bf76b23c3def33978 Mon Sep 17 00:00:00 2001
From: Tomas Sedovic
&pin mut|const place borrowing syntaxI've got some primitive ideas about borrowck, and I probably need to confirm with someone who is familiar with MIR/borrowck before starting to implement.
@@ -78,11 +78,11 @@ Continue Experimentation with Pin Ergonomics Drop::pin_drop(&pin mut self) first.&pin pat pattern syntaxA chained projection operation should naturally decompose, so foo.[Ber Clausen][].[Baz Shkara][] should be the same as writing (foo.[Ber Clausen][]).[Baz Shkara][]. Until now, the different parenthesizing would have allowed different outcomes. This behavior is confusing and also makes many implementation details more complicated than they need to be.
A chained projection operation should naturally decompose, so foo.@bar.@baz should be the same as writing (foo.@bar).@baz. Until now, the different parenthesizing would have allowed different outcomes. This behavior is confusing and also makes many implementation details more complicated than they need to be.
Since projections now decompose, we have no need from a design perspective for multi-level FRTs. So field_of!(Foo, bar.baz) is no longer required to work. Thus we have decided to restrict FRTs to only a single field and get rid of the path. This simplifies the implementation in the compiler and also avoids certain difficult questions such as the locality of FRTs (if we had a path, we would have to walk the path and it is local, if all structs included in the path are local). Now with only a single field, the FRT is local if the struct is.
We also discovered that it is a good idea to make FRTs inhabited (they still are ZSTs), since then it allows the following pattern to work:
-fn project_free_standing<F: Field>(_: Field, r: &F::Base) -> &F::Type { ... }
+
+
+fn project_free_standing<F: Field>(_: Field, r: &F::Base) -> &F::Type { ... }
+
// can now call the function without turbofish:
let my_field = project_free_standing(field_of!(MyStruct, my_field), &my_struct);
@@ -185,18 +188,22 @@ let my_field = project_free_standing(field_of!(MyStruct, my_field), &my_stru
Single Project Operator & Trait via Exclusive Decay
It would be great if we only had to add a single operator and trait and could obtain the same features as we have with two. The current reason for having two operators is to allow both shared and exclusive projections. If we could have another operation that decays an exclusive reference (or custom, exclusive smart-pointer type) into a shared reference (or the custom, shared version of the smart pointer). This decay operation would need borrow checker support in order to have simultaneous projections of one field exclusively and another field shared (and possibly multiple times).
This goes into a similar direction as the reborrowing project goal https://github.com/rust-lang/rust-project-goals/issues/399, however, it needs extra borrow checker support.
+
+
fn add(x: cell::RefMut<'_, i32>, step: i32) {
*x = *x + step;
-}
+}
+
struct Point {
x: i32,
y: i32,
-}
+}
+
fn example(p: cell::RefMut<'_, Point>) {
- let y: cell::Ref<'_, i32> = coerce_shared!(p.[@y][]);
- let y2 = coerce_shared!(p.[@y][]); // can project twice if both are coerced
- add(p.[Devon Peticolas][], *y);
- add(p.[Devon Peticolas][], *y2);
+ let y: cell::Ref<'_, i32> = coerce_shared!(p.@y);
+ let y2 = coerce_shared!(p.@y); // can project twice if both are coerced
+ add(p.@x, *y);
+ add(p.@x, *y2);
assert_eq!(*y, *y2); // can still use them afterwards
}
@@ -223,12 +230,16 @@ fn example(p: cell::RefMut<'_, Point>) {
There have been some developments in pin ergonomics https://github.com/rust-lang/rust/issues/130494: "alternative B" is now the main approach which means that Pin<&mut T> has linear projections, which means that it doesn't change its output type depending on the concrete field (really depending on the field, not only its type). So it falls into the general projection pattern Pin<&mut Struct> -> Pin<&mut Field> which means that Pin doesn't need any where clauses when implementing Project.
Additionally we have found out that RCU also doesn't need where clauses, as we can also make its projections linear by introducing a MutexRef<'_, T> smart pointer that always allows projections and only has special behavior for T = Rcu<U>. Discussed on zulip after this message.
For this reason we can get rid of the generic argument to Project and mandate that all types that support projections support them for all fields. So the new Project trait looks like this:
+
+
// still need a common super trait for `Project` & `ProjectMut`
pub trait Projectable {
type Target: ?Sized;
-}
+}
+
pub unsafe trait Project: Projectable {
- type Output<F: Field<Base = Self::Target>>;
+ type Output<F: Field<Base = Self::Target>>;
+
unsafe fn project<F: Field<Base = Self::Target>>(
this: *const Self,
) -> Self::Output<F>;
@@ -236,8 +247,11 @@ pub unsafe trait Project: Projectable {
Are FRTs even necessary?
With this change we can also think about getting rid of FRTs entirely. For example we could have the following Project trait:
+
+
pub unsafe trait Project: Projectable {
- type Output<F>;
+ type Output<F>;
+
unsafe fn project<const OFFSET: usize, F>(
this: *const Self,
) -> Self::Output<F>;
@@ -246,72 +260,95 @@ pub unsafe trait Project: Projectable {
There are other applications for FRTs that are very useful for Rust-for-Linux. For example, storing field information for intrusive data structures directly in that structure as a generic.
More concretely, in the kernel there are workqueues that allow you to run code in parallel to the currently running thread. In order to insert an item into a workqueue, an intrusive linked list is used. However, we need to be able to insert the same item into multiple lists. This is done by storing multiple instances of the Work struct. Its definition is:
+
+
pub struct Work<T, const ID: u64> { ... }
Where the ID generic must be unique inside of the struct.
+
+
struct MyDriver {
data: Arc<MyData>,
main_work: Work<Self, 0>,
aux_work: Work<Self, 1>,
// more fields ...
-}
+}
+
// Then you call a macro to implement the unsafe `HasWork` trait safely.
// It asserts that there is a field of type `Work<MyDriver, 0>` at the given field
// (and also exposes its offset).
impl_has_work!(impl HasWork<MyDriver, 0> for MyDriver { self.main_work });
-impl_has_work!(impl HasWork<MyDriver, 1> for MyDriver { self.aux_work });
-// Then you implement `WorkItem` twice:
+impl_has_work!(impl HasWork<MyDriver, 1> for MyDriver { self.aux_work });
+
+// Then you implement `WorkItem` twice:
+
impl WorkItem<0> for MyDriver {
- type Pointer = Arc<Self>;
+ type Pointer = Arc<Self>;
+
fn run(this: Self::Pointer) {
println!("doing the main work here");
}
-}
+}
+
impl WorkItem<1> for MyDriver {
- type Pointer = Arc<Self>;
+ type Pointer = Arc<Self>;
+
fn run(this: Self::Pointer) {
println!("doing the aux work here");
}
-}
-// And finally you can call `enqueue` on a `Queue`:
+}
+
+// And finally you can call `enqueue` on a `Queue`:
+
let my_driver = Arc::new(MyDriver::new());
let queue: &'static Queue = kernel::workqueue::system_highpri();
-queue.enqueue::<_, 0>(my_driver.clone()).expect("my_driver is not yet enqueued for id 0");
+queue.enqueue::<_, 0>(my_driver.clone()).expect("my_driver is not yet enqueued for id 0");
+
// there are different queues
let queue = kernel::workqueue::system_long();
-queue.enqueue::<_, 1>(my_driver.clone()).expect("my_driver is not yet enqueued for id 1");
+queue.enqueue::<_, 1>(my_driver.clone()).expect("my_driver is not yet enqueued for id 1");
+
// cannot insert multiple times:
assert!(queue.enqueue::<_, 1>(my_driver.clone()).is_err());
FRTs could be used instead of this id, making the definition be Work<F: Field> (also merging the T parameter).
+
+
struct MyDriver {
data: Arc<MyData>,
main_work: Work<field_of!(Self, main_work)>,
aux_work: Work<field_of!(Self, aux_work)>,
// more fields ...
-}
+}
+
impl WorkItem<field_of!(MyDriver, main_work)> for MyDriver {
- type Pointer = Arc<Self>;
+ type Pointer = Arc<Self>;
+
fn run(this: Self::Pointer) {
println!("doing the main work here");
}
-}
+}
+
impl WorkItem<field_of!(MyDriver, aux_work)> for MyDriver {
- type Pointer = Arc<Self>;
+ type Pointer = Arc<Self>;
+
fn run(this: Self::Pointer) {
println!("doing the aux work here");
}
-}
+}
+
let my_driver = Arc::new(MyDriver::new());
let queue: &'static Queue = kernel::workqueue::system_highpri();
queue
.enqueue(my_driver.clone(), field_of!(MyDriver, main_work))
// ^ using Gary's idea to avoid turbofish
- .expect("my_driver is not yet enqueued for main_work");
+ .expect("my_driver is not yet enqueued for main_work");
+
let queue = kernel::workqueue::system_long();
queue
.enqueue(my_driver.clone(), field_of!(MyDriver, aux_work))
- .expect("my_driver is not yet enqueued for aux_work");
+ .expect("my_driver is not yet enqueued for aux_work");
+
assert!(queue.enqueue(my_driver.clone(), field_of!(MyDriver, aux_work)).is_err());
This makes it overall a lot more readable (by providing sensible names instead of magic numbers), and maintainable (we can add a new variant without worrying about which IDs are unused). It also avoids the unsafe HasWork trait and the need to write the impl_has_work! macro for each Work field.
@@ -325,17 +362,24 @@ assert!(queue.enqueue(my_driver.clone(), field_of!(MyDriver, aux_work)).is_err()
Making Project::project safe
In the current proposal the Project::project function is unsafe, because it takes a raw pointer as an argument. This is pretty unusual for an operator trait (it would be the first). Tyler Mandry thought about a way of making it safe by introducing "partial struct types". This new type is spelled Struct.F where F is an FRT of that struct. It's like Struct, but with the restriction that only the field represented by F can be accessed. So for example &Struct.F would point to Struct, but only allow one to read that single field. This way we could design the Project trait in a safe manner:
+
+
// governs conversion of `Self` to `Narrowed<F>` & replaces Projectable
pub unsafe trait NarrowPointee {
- type Target;
+ type Target;
+
type Narrowed<F: Field<Base = Self::Target>>;
-}
+}
+
pub trait Project: NarrowPointee {
- type Output<F: Field<Base = Self::Type>>;
+ type Output<F: Field<Base = Self::Type>>;
+
fn project(narrowed: Self::Narrowed<F>) -> Self::Output<F>;
}
The NarrowPointee trait allows a type to declare that it supports conversions of its Target type to Target.F. For example, we would implement it for RefMut like this:
+
+
unsafe impl<'a, T> NarrowPointee for RefMut<'a, T> {
type Target = T;
type Narrowed<F: Field<Base = T>> = RefMut<'a, T.F>;
@@ -387,7 +431,7 @@ Reborrow traits
-
+
1 detailed update available.
@@ -631,7 +675,7 @@ Ergonomic ref-counting: RFC decision and preview https://smallcultfollowing.com/babysteps/blog/2025/10/13/ergonomic-explicit-handles/
The point of this post is to argue that, whatever else we do, Rust should have a way to create handles/clones (and closures that work with them) which is at once explicit and ergonomic.
To give a preview of my current thinking, I am working now on the next post which will discuss how we should add an explicit capture clause syntax. This is somewhat orthogonal but not really, in that an explicit syntax would make closures that clone more ergonomic (but only mildly). I don't have a proposal I fully like for this syntax though and there are a lot of interesting questions to work out. As a strawperson, though, you might imagine [this older proposal I wrote up](https://hackmd.io/Niko Matsakis/SyI0eMFXO?type=view), which would mean something like this:
+
+
let actor1 = async move(reply_tx.handle()) {
reply_tx.send(...);
};
@@ -703,6 +749,8 @@ let actor2 = async move(reply_tx.handle()) {
};
This is an improvement on
+
+
let actor1 = {
let reply_tx = reply_tx.handle();
async move(reply_tx.handle()) {
@@ -712,12 +760,16 @@ let actor2 = async move(reply_tx.handle()) {
but only mildly.
The next post I intend to write would be a variant on "use, use everywhere" that recommends method call syntax and permitting the compiler to elide handle/clone calls, so that the example becomes
+
+
let actor1 = async move {
reply_tx.handle().send(...);
// -------- due to optimizations, this would capture the handle creation to happen only when future is *created*
};
This would mean that cloning of strings and things might benefit from the same behavior:
+
+
let actor1 = async move {
reply_tx.handle().send(some_id.clone());
// -------- the `some_id.clone()` would occur at future creation time
@@ -860,7 +912,7 @@ In-place initialization
-
+
1 detailed update available.
@@ -991,7 +1043,7 @@ Stabilizable Polonius support on nightly
+
## Other goal updates