rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

RFC: Field projection

Open BennoLossin opened this issue 3 years ago • 114 comments

Rendered

Also see the pre-RFC discussion.

Unresolved questions

  • [ ] How should the limited negative reasoning look like? What are the limitations? Discussed at zulip
  • [ ] What other alternatives are there for negative reasoning?

BennoLossin avatar Sep 21 '22 11:09 BennoLossin

In ece8b0d I fixed the soundness hole found by pitaj in the internals thread.

Sadly this is to the detriment of ergonomics, but I think keeping existing behavior the same is very important.

BennoLossin avatar Sep 23 '22 11:09 BennoLossin

That might be nice to have for ergonomics but there is already a, mind you stable, way to do this through addr_of!(...). In other words, there already is a mechanism of doing it. IMHO, if this is added, it should just be a sugar around the already existing APIs.

KirilMihaylov avatar Sep 29 '22 14:09 KirilMihaylov

addr_of! only works on raw pointers. It does nothing for Pin, MaybeUninit, Cell, UnsafeCell, ...

bjorn3 avatar Sep 29 '22 15:09 bjorn3

I feel like it's missing a drawback: the implicit conversions. While supporting this for MaybeUninit is very powerful and I'd like to have something like that in the language, Option on the other hands feels a bit too "magic" (perhaps because that's not #[repr(transparent)] and incurs a hidden conversion which Rust otherwise avoids).

Rust generally prefers explicit over implicit, and this is adding implicit conversions on the very common dot operator (unlike, say, ? or .await which sticks out a fair bit more). I think this implicit conversion is not given enough weight as a drawback.

I'd also like to see a bit on "how do we teach this?" to users. How come I can access the inner field of some wrapper types but not others? The field was an usize, why is it suddenly an Option<usize>?

Lonami avatar Sep 29 '22 15:09 Lonami

Option on the other hands feels a bit too "magic" (perhaps because that's not #[repr(transparent)] and incurs a hidden conversion which Rust otherwise avoids).

To me there is no hidden conversion. If I have Option<&mut Struct> and turn it into Option<&mut Field> via struct.field, then it essentially is just doing this:

match struct {
    Some(struct) => Some(&mut struct.field),
    None => None,
}

To me this conversion is fully expressed by the member access syntax (I think the argument for different syntax can be made). It also is functionally very similar to Pin.

(I do not know if it is clear from the RFC, but Option and Pin are #[inner_projecting], which means that they only allow projection when the generic parameter is a Pointer type.)

BennoLossin avatar Sep 29 '22 15:09 BennoLossin

addr_of! only works on raw pointers. It does nothing for Pin, MaybeUninit, Cell, UnsafeCell, ...

True, yet can't you call get() on UnsafeCell? It is how you are supposed to initialize a structure inside MaybeUninit.

KirilMihaylov avatar Sep 29 '22 16:09 KirilMihaylov

yes you can do

struct Foo {
    bar: Bar,
}
struct Bar {
    a: usize
}
let data = UnsafeCell::new(...);
let a: &UnsafeCell<usize> = unsafe { &*addr_of_mut!((*data.get()).bar.a).cast::<UnsafeCell<usize>>() }

But

  1. you need unsafe
  2. it is very verbose

BennoLossin avatar Sep 29 '22 16:09 BennoLossin

The trait suggested here isn't actually implementable: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=23e3e48fbc2816cf3f6460bceeba6dd2

Probably it should have an extra lifetime param: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=23e3e48fbc2816cf3f6460bceeba6dd2

pub trait FieldProjecting<'a, T> where Self: 'a {
    type Inner<U: 'a>;
    unsafe fn project<U>(self, proj: impl FnOnce(*mut T) -> *mut U) -> Self::Inner<U>;
}

nbdd0121 avatar Sep 29 '22 17:09 nbdd0121

The trait must also be unsafe. Because it is only allowed to do projections.

BennoLossin avatar Sep 29 '22 17:09 BennoLossin

To me there is no hidden conversion

~~The layout of T is not guaranteed to be the same as the layout of Option<T>. The cost may neglible, and it may even be optimized out in most cases, but it's still there, because Option is not #[repr(transparent)]. For example, if T is u64, we're talking going from 8 to 16 bytes (twice as big!).~~

~~Perhaps support for inner_projecting could be a future possibility instead (but this is just my opinion).~~

As later pointed I missed the detail where this only applies to pointers, my bad! (I read about it but my brain chose to ignore that for this example.)

Lonami avatar Sep 29 '22 19:09 Lonami

The layout of T is not guaranteed to be the same as the layout of Option<T>. The cost may neglible, and it may even be optimized out in most cases, but it's still there, because Option is not #[repr(transparent)]. For example, if T is u64, we're talking going from 8 to 16 bytes (twice as big!).

There are no conversions from Option<T> to Option<U>. I am only proposing Option<&mut T> to Option<&mut U> where U is a field of T. &mut T and &mut U have the same layout (this is only available for thin pointers, I do not know of any fat pointers where the pointee has fields, please let me know if you know any!). Additionally I propose allowing this conversion for the pointer types listed in the supported pointers section. These are:

  • &mut T, &T
  • *mut T, *const T, NonNull<T>, AtomicPtr<T>
  • Pin<P> where P is from above

Note that all of these types have the same layout as *mut T.

BennoLossin avatar Sep 29 '22 19:09 BennoLossin

yes you can do

struct Foo {
    bar: Bar,
}
struct Bar {
    a: usize
}
let data = UnsafeCell::new(...);
let a: &UnsafeCell<usize> = unsafe { &*addr_of_mut!((*data.get()).bar.a).cast::<UnsafeCell<usize>>() }

But

  1. you need unsafe
  2. it is very verbose

My point there was that: (1) with the example of UnsafeCell, you might as well use unsafe for the rest too and (2) while it's very verbose in this plain form, it's not quite that hard to abstract it away in a trait from a crate.

There are already crates providing lens as a functionality, this is no different. This would be a great crate, but I personally don't think it has a place in the standard library, or at least not like this.

KirilMihaylov avatar Sep 29 '22 20:09 KirilMihaylov

My point there was that: (1) with the example of UnsafeCell, you might as well use unsafe for the rest too and (2) while it's very verbose in this plain form, it's not quite that hard to abstract it away in a trait from a crate.

  1. I think isolating unsafe is important and we should strive to make every operation safe, if it can be done.
  2. I have a demo crate doing this, but it still needs some type hints and the syntax is not optimal (postfix macros would help there), a language feature would improve on both points (and fits well together with Pin projections)

There are already crates providing lens as a functionality, this is no different. This would be a great crate, but I personally don't think it has a place in the standard library, or at least not like this.

To me, field projection feels different compared to lenses. It also is something native to Rust. We have these different wrapper types (Cell,UnsafeCell, etc.) that impose something on the type they contain (be it mutability or the ability to be partially initialized). They also affect the fields of structs they wrap and so producing a pointer to a wrapper just around the field is to me something we should support.

This is something general that many Rust programs will need to do, so in my opinion it is something the language should provide. Most of the impact will be by allowing this for Pin and MaybeUninit. I am open to the idea of moving Option, Cell etc. into the future possibilities section and having their projections be unstable for longer.

BennoLossin avatar Sep 29 '22 20:09 BennoLossin

I do not know of any fat pointers where the pointee has fields, please let me know if you know any

struct Foo {
    a: i32,
    b: dyn Send,
}

Jules-Bertholet avatar Sep 29 '22 20:09 Jules-Bertholet

I think for the beginning we are not going to support those. But conversion can only happen in one way, right? (so from fat => thin) So it might be alright to allow that at some point (it would be weird to not support pin projection for Foo).

BennoLossin avatar Sep 29 '22 20:09 BennoLossin

Option projection can be almost as easy with try blocks, and given that there is a branch there I feel like it suits Rust model of showing costs explicitly better.

ChayimFriedman2 avatar Sep 29 '22 21:09 ChayimFriedman2

There is also the big question of how we want pin projection to look like. Here is a zulip post about that

BennoLossin avatar Sep 29 '22 21:09 BennoLossin

I do like the idea of field projection in general, but I am not at all convinced that making . more magical than it already is (Deref/DerefMut/partial moves, ...) is the way to go there.

You reference RFC 2708, and I cannot help but feel that re-ifying field access is a direction that should be explored more: it is useful on its own, and it seems like it could power field projection.

From a pure syntax perspective, it would certain be more verbose, especially in the absence of aliasing, yet:

maybe_uninit.field

maybe_uninit.project(Type::field)

The latter is indubitably more verbose, but at the same time it's within a factor of x2 and has zero magic.


Is Field Projection frequent enough to warrant special treatment?

I believe not. It is used, certainly, but UnsafeCell and MaybeUninit are used sparingly -- being unsafe -- and even though Cell and Pin are used more frequently, they are not that frequent either.

Contrast with Deref, which is used all the time.

It's clear why Deref warrants "magic" in the . operator, and it's not clear why field projection would.

Should Field Projection be implementable safely?

It's a laudable goal, yet at the same time all the types you wish to implement for already have an unsafe implementation to start with, so one more method with an unsafe implementation on them certainly won't be a deal-breaker.


So I'll propose an alternative, lightweight design:

  • Compiler: Type::field is now valid syntax, and desugars to fn(*mut Type) -> *mut FieldType.
  • Standard Library: UnsafeCell, Cell, MaybeUninit, Pin, Option, ... all implement an inherent project function.

That's it. No complex attributes for the compiler to implement, no trait.

Any additional implementation cost, any additional language or library change, should be justified as providing sufficient added value above what this lightweight alternative offers.

matthieu-m avatar Sep 30 '22 17:09 matthieu-m

and even though Cell and Pin are used more frequently, they are not that frequent either.

Pin is very frequent in rust-for-linux as every lock and pretty much every other concurrency primitive is pinned due to containing a doubly linked list when lockdep is enabled. This means that every struct containing a lock, which are very common, has to be pinned too. Initializing them is an absolute pain without this feature involving two unsafe code blocks. One to create the lock and the other to do a pin projection to allow initializing it once it is in it's final place. As an example https://github.com/Rust-for-Linux/linux/blob/542379556669fee413ad45bc9611187e075b0d47/samples/rust/rust_semaphore.rs#L112-L131

Standard Library: UnsafeCell, Cell, MaybeUninit, Pin, Option, ... all implement an inherent project function.

This project function can't be made to have a safe interface as anyone could write their own function returning a null pointer. Having a safe way to do pin projection is the whole point of this RFC.

bjorn3 avatar Sep 30 '22 17:09 bjorn3

So I'll propose an alternative, lightweight design:

  • Compiler: Type::field is now valid syntax, and desugars to fn(*mut Type) -> *mut FieldType.

imho Type::field should be syntax for creating a field pointer (like c++'s member pointer), not a function.

// Struct::field could be syntax sugar for unsafe { FieldPtr::<Struct, FieldType>::new(offset_of!(Struct, field)) }

// in core
#[repr(transparent)]
pub struct FieldPtr<S: ?Sized, F: ?Sized>(usize, PhantomData<(fn(&S)->&F, fn(&mut S)->&mut F, fn(S) -> F)>)
where
    S: Pointee,
    F: Pointee<Metadata = S::Metadata>;

// impl Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash for FieldPtr, can't use #[derive] because it adds too many bounds

impl<S: ?Sized, F: ?Sized> FieldPtr<S, F>
where
    S: Pointee,
    F: Pointee<Metadata = S::Metadata>
{
    pub unsafe const fn new(offset: usize) -> Self {
        Self(offset, PhantomData)
    }
    pub const fn offset(self) -> usize {
        self.0
    }
    pub const fn cell(self) -> FieldPtr<Cell<S>, Cell<F>> {
        FieldPtr(self.offset(), PhantomData)
    }
    pub const fn unsafe_cell(self) -> FieldPtr<UnsafeCell<S>, UnsafeCell<F>> {
        FieldPtr(self.offset(), PhantomData)
    }
    pub const fn maybe_uninit(self) -> FieldPtr<MaybeUninit<S>, MaybeUninit<F>> {
        FieldPtr(self.offset(), PhantomData)
    }
}

impl<T: ?Sized> *mut T {
    pub const fn project<F: ?Sized>(self, field: FieldPtr<T, F>) -> *mut F
    where
        T: Pointee,
        F: Pointee<Metadata = T::Metadata>
    {
        // technically incorrect, the correct code offsets by bytes rather than `T`s
        self.add(field.offset())
    }
}

// safe project impls for *const T, &T, &mut T, and NonNull<T>, and maybe unsafe project impls for Pin<ptr_type>

programmerjake avatar Sep 30 '22 18:09 programmerjake

@matthieu-m

but I am not at all convinced that making . more magical

I have added a section about the possibility of adding a ~ operator. This was suggested by @joshtriplett . I really like the post by @Gankra (sorry to ping you, but this might be interesting to you) about this and I think that field projection (in the more general monad sense) applies to this.

I think that introducing a dedicated projection operator is definitely something we should consider. Although this comes at the relatively high cost of migration for pin-project users. As they have to fix every single field access (as opposed to just removing .project()).

maybe_uninit.project(Type::field)

This is not much longer compared to maybe_uninit.project(|s| &mut s.field). I think either we get the language support, or we create a postfix projection macro to do this for us. I prefer the language solution.

Should Field Projection be implementable safely?

My general opinion is: If we can make it safe, we should. [^1]

When using MaybeUninit to initialize a value field by field, we currently need to use unsafe all the way through. With this proposal one would only need one unsafe.

Is Field Projection frequent enough to warrant special treatment?

As björn already mentioned, Pin is almost ubiquitous in the kernel. It would be a shame if Rust had no solution to this (I think it would make Rust look bad).


@programmerjake

maybe unsafe project impls for Pin<ptr_type>

It is very important that this projection is safe. Otherwise we could just continue to use Pin::map_unchecked_mut.

[^1]: the obvious caveat to that would be other language requirements that make this impossible e.g. backwards compatibility

BennoLossin avatar Sep 30 '22 19:09 BennoLossin

I experimented the field pointer idea before, and I find it not satisfactory because there are no way to distinguish two fields of the same type in the same struct in the type level. However it is desirable to be able to tell two fields of the same type apart when pinning is involved, because one field can be pinned and another not.

I recently came up with an idea and implemented it here: https://github.com/nbdd0121/field-projection. I haven't finished writing up the idea and the docs, but the core idea is to have a separate type to represent each field (so two fields with the same type are represented use two different types, rather than two values of the same type). For example, if we have

struct Foo {
    #[pin]
    a: usize,
    b: usize,
}

then we have a FieldOffset<Foo, "a"> to represent field a and FieldOffset<Foo, "b"> to represent field b, so they are of different type despite a and b are both usize. (The support for using str as const generic is not complete so I use hash of the field instead).

The result type of projection then can be represented as an assoc type, and can vary for different fields since each field is represented by an unique type:

trait Projectable<F: Field> {
    type Target;
    fn project(self) -> Self::Target;
}

If a macro, e.g. #[pin_project] wants to convey additional info about whether a field is pinned or not, it can just implement new traits for FieldOffset<Foo, "a"> and FieldOffset<Foo, "b"> differently, and pairing it with a Projectable impl of Pin, it can project a pinned field to Pin<&mut T> and an unpinned field to &mut T.

  • Library part https://github.com/nbdd0121/field-projection/blob/8a44448792cb3d324ebb6f108ba6eeae60399247/src/pin.rs
  • Macro part https://github.com/nbdd0121/field-projection/blob/8a44448792cb3d324ebb6f108ba6eeae60399247/field-projection-internal/src/pin.rs
  • Usage example https://github.com/nbdd0121/field-projection/blob/8a44448792cb3d324ebb6f108ba6eeae60399247/tests/foo.rs

nbdd0121 avatar Oct 01 '22 02:10 nbdd0121

So to be clear, &mut pinned_struct.field would have type Pin<&mut Field>? That's a bit weird, though I can't think of a better option.

comex avatar Oct 01 '22 06:10 comex

With the ~ operator we could change that: let _: Pin<&mut Field> = pinned_struct~field;

BennoLossin avatar Oct 01 '22 07:10 BennoLossin

val->field would also be an option I think.

bjorn3 avatar Oct 01 '22 09:10 bjorn3

@bjorn3

This project function can't be made to have a safe interface as anyone could write their own function returning a null pointer. Having a safe way to do pin projection is the whole point of this RFC.

If taking a function indeed, in this case a dedicated pointer-to-member type would solve the issue of guaranteeing a non-null pointer.

However I want to emphasize the fact that there is a difference between a safe usage of project and a safe implementation of project. UnsafeCell, MaybeUninit, Cell, Pin are all implemented with unsafe under the hood, hence I see no issue in having their project function require unsafe in its implementation.

@y86-dev

I have added a section about the possibility of adding a ~ operator.

Please not ~. The ~ symbol was removed from Rust because it is hard to type on a number of keyboard layouts:

I'm not necessarily opposed to an operator (though I prefer words), but please be mindful that different keyboard layouts make different symbols awkward to reach for. ALT-GR + "key" requires hand calisthenics.

In that sense, bjorn's proposal of -> is quite more viable. It involves a SHIFT on some layouts, but that's easier to use than ALT-GT.

I prefer the language solution.

I favor being conservative with language changes:

  • Pointers-to-member have broad applicability and seemingly come at little cost: low implementation cost, no impact on compile time when not used, low impact on language complexity.
  • This RFC is not as general purpose, and comes at a quite higher cost: much more complex implementation, if overloading . impact on compile times even when not used, higher impact on language complexity.

I understand now that Pin is central to the kernel, and thus has broader usage that I initially thought, but that's still a relative restricted use, so the more lightweight the solution the better.

This is not much longer compared to maybe_uninit.project(|s| &mut s.field). I think either we get the language support, or we create a postfix projection macro to do this for us. I prefer the language solution.

I agree it's not much longer, but it's much simpler, which is worth considering.

Why does clippy recommends using opt.map(String::as_str) rather than opt.map(|s| s.as_str())? They're of similar length!

The recommendation comes because one is much easier to parse (for a human) than the other. String::as_str, or Type::field, are only 3 elements: type, separator, method/field. On the other hand, the closure is many more elements, and much more general, thus requires more attention and more time to parse, lest a detail is missed.

I also note that maybe_uninit.project(Type::field) is somewhat orthogonal to introducing syntactic sugar maybe_uninit->field for it later.

I do recommend strongly that the RFC should be tightened to focus on semantics and functionality, and leave any syntax sugar for later -- if anything, because syntax sugar involves bikeshedding, and will delay the introduction of the functionality.

@nbdd0121

I experimented the field pointer idea before, and I find it not satisfactory because there are no way to distinguish two fields of the same type in the same struct in the type level.

That's a good point. I do note that it's a restriction of C++ pointer-to-member rather than an inherent restriction of the concept, though. Rust's functions have a unique type, unlike C++'s, so I see no reason that Rust's pointer-to-member would not have a unique type as well, on top of (like functions) being coercible to a "field-erased" version.

It may actually be necessary for Pin to be able to distinguish between different fields of the same type, as not all fields may be equal there.

matthieu-m avatar Oct 01 '22 10:10 matthieu-m

I have added a section about the possibility of adding a ~ operator.

Please not ~. The ~ symbol was removed from Rust because it is hard to type on a number of keyboard layouts:

* [French layout](https://en.wikipedia.org/wiki/AZERTY): it requires ALT-GR + 2.

* [German layout](https://en.wikipedia.org/wiki/German_keyboard_layout): it requires ALT-GR + +.

I'm not necessarily opposed to an operator (though I prefer words), but please be mindful that different keyboard layouts make different symbols awkward to reach for. ALT-GR + "key" requires hand calisthenics.

In that sense, bjorn's proposal of -> is quite more viable. It involves a SHIFT on some layouts, but that's easier to use than ALT-GT.

While I am very sympathetic to this argument, I am not sure if the German keyboard layout should be used as a reference point here.

The following symbols all require ALT-GR to type on a German keyboard: {, }, [, ], |, and @, and all are used by existing rust syntax. ~ would not be a big change on this front.

I am not familiar with the French layout, but according to the wikipedia page you linked the situation seems to be very similar there.

deltragon avatar Oct 01 '22 11:10 deltragon

I am not familiar with the French layout, but according to the wikipedia page you linked the situation seems to be very similar there.

You are correct, it's already difficult to type { and } (the most frequent), I see no reason to add to that woe.

I do note that the rationale was -- if I remember correctly -- mentioned when ~T was dropped in favor of Box<T> pre-1.0.

And I do think we should avoid any debate on syntax for now -- I should have avoided mentioning it really -- and focus on how to obtain the functionality. We can come back to sugar later, once we agree what it should desugar to.

matthieu-m avatar Oct 01 '22 13:10 matthieu-m

@nbdd0121

I recently came up with an idea and implemented it here: https://github.com/nbdd0121/field-projection. I haven't finished writing up the idea and the docs, but the core idea is to have a separate type to represent each field (so two fields with the same type are represented use two different types, rather than two values of the same type).

Your idea certainly seems interesting. I know that you are aware of these problem, I am just placing them here, because this applies to every library level solution:

  • looking through your code I came up with a way to fix my own library's ergonomics: you can now project using project!(&mut a.b) and it works for almost every type.
  • I have not been able to make projections work for Pin<&mut T> and &mut MaybeUninit<T> and Pin<&mut MaybeUninit<T>>, specialization is not ready enough and my other attempts have failed. I think that this could be inherent to a library solution.
  • a proc macro solution with syn is a poor fit for the kernel. That is just how it is.

My macro solution is also not ideal, as already mentioned earlier, the kernel relies on Pin projections a lot. Having to type p!(a => b) instead of a.b/a~b is a huge inconvenience. The C developers will see this macro and ask "what does this do?". After explaining, they will follow up with "why can't Rust do this natively?" and I have to agree with them. This is something a systems programming language needs to be able to do.


@bjorn3

I do not want to bikeshed the exact tokens for the operator now. Personally I do not like ->, because it has a very different meaning in C and C++, I would like to avoid the confusion.

(but lets put it on the list of options)


@matthieu-m

However I want to emphasize the fact that there is a difference between a safe usage of project and a safe implementation of project. UnsafeCell, MaybeUninit, Cell, Pin are all implemented with unsafe under the hood, hence I see no issue in having their project function require unsafe in its implementation.

Yes all of those types certainly require unsafe in their implementation, but what about custom user types? All trait implementations will be required to use raw pointers in order to support MaybeUninit and Pin. Does this mean that we want to force these on users of wrapper types that do not require unsafe? One could certainly argue that this feature is "advanced enough" to be only used by users that also know how to write unsafe, but I do not think that want that.

Please not ~. The ~ symbol was removed from Rust because it is hard to type on a number of keyboard layouts:

French layout: it requires ALT-GR + 2.
German layout: it requires ALT-GR + +.

I'm not necessarily opposed to an operator (though I prefer words), but please be mindful that different keyboard layouts make different symbols awkward to reach for. ALT-GR + "key" requires hand calisthenics.

In that sense, bjorn's proposal of -> is quite more viable. It involves a SHIFT on some layouts, but that's easier to use than ALT-GT.

I agree with @deltragon, there are lots of other symbols that are hard to type on a German keyboard. You also are not required to use that keyboard layout, there exist other compatible keyboard layouts: https://www.neo-layout.org/Layouts/neoqwertz/

I think ultimately the decision of syntax will fall to the style team. I hope that we get an operator, as everything else is just not really an improvement over a macro invocation project!(&a.b).

I favor being conservative with language changes:

  • Pointers-to-member have broad applicability and seemingly come at little cost: low implementation cost, no impact on compile time when not used, low impact on language complexity.

I understand why you are reluctant to language changes. And in general I have to agree with you. "Can it be done with a library?", "Can it be done with a macro?" all of these questions are very important when trying to solve such a problem. In this specific case however, I believe a language solution to be the best possible option.

I do not think that pointers-to-member will be as clear cut as you make them out to be. Here is a discussion from 2018 about field offsets (essentially member pointers). I would also like to know what else this would enable. I have given it a quick online search, but I found nothing except improved offset_of! (granted this would be nice).

How would pointers-to-members fix the pin situation though?

  • This RFC is not as general purpose, and comes at a quite higher cost: much more complex implementation, if overloading . impact on compile times even when not used, higher impact on language complexity.

I agree, we could make this much more general by adding the notion of pointers-to-members. However, I do not want this to come at the expense of ergonomics. What do you think of:

pub trait Project<'a> {
    type Target<U: 'a>;
    type Inner: 'a;

    fn project<U: 'a>(self, field: Field<Self::Inner, U>) -> Self::Target;
}

impl<T> Project<'_> for *mut T {
    type Inner = T;
    type Target<U> = *mut U;

    fn project(self, field: Field<Self::Inner, U>) -> Self::Target {
        unsafe { self.cast::<u8>().add(field.offset()).cast::<U>() }
    }
}

impl<'a, T> Project<'a> for &'a mut MaybeUninit<T> {
    type Inner = T;
    type Target<U: 'a> = &'a mut MaybeUninit<U>;

    fn project(self, field: Field<Self::Inner, U>) -> Self::Target {
        unsafe { &mut *self.as_mut_ptr().project(field).cast::<MaybeUninit<U>>() }
    }
}

And then making $a:expr ~ $b:ident (or whatever other operator) be syntactic sugar for Project::project($a, <$a's type>::$b)? I really want an operator (or a similarly concise way of doing field projection).

This approach still has the pin situation to sort out, but that could just be language magic.

I understand now that Pin is central to the kernel, and thus has broader usage that I initially thought, but that's still a relative restricted use, so the more lightweight the solution the better.

I think that the Rust-for-Linux project is going to be crucial in the future public image of Rust. Many other embedded projects need similar things. I do not see this as "restricted use", especially since futures also require pinning.

I agree it's not much longer, but it's much simpler, which is worth considering.

But I do not think adding so much to the language and getting such a minute improvement is going to be worth it in the end. FWIW I can already do great with the project! macro, or I could write a shorthand macro for the closure maybe_uninit.project(f!(.field)) (this would be even better with post fix macros: maybe_uninit.p!(field)).

I also note that maybe_uninit.project(Type::field) is somewhat orthogonal to introducing syntactic sugar maybe_uninit->field for it later.

I do recommend strongly that the RFC should be tightened to focus on semantics and functionality, and leave any syntax sugar for later -- if anything, because syntax sugar involves bikeshedding, and will delay the introduction of the functionality.

For the reasons previously listed, I think that it is absolutely vital for us to have some sugar from the get-go.

BennoLossin avatar Oct 01 '22 13:10 BennoLossin

@deltragon

I am not familiar with the French layout, but according to the wikipedia page you linked the situation seems to be very similar there.

Further investigation led me to https://en.wikipedia.org/wiki/List_of_QWERTY_keyboard_language_variants: the ~ is fully absent from the Italian and there seems to be difficulties in Polish (dead key) and Spanish (Latin American variant) as well.

matthieu-m avatar Oct 01 '22 14:10 matthieu-m