Generic Field Projection

I found a way to add field projection as a library without requiring a derive macro like previous methods.

For context, field projection basically lets you do things like this:

struct Foo {
    a: i32,
}

fn project(p: Pin<&Foo>) -> Pin<&i32> {
    p.a // or something like this
}

What I’ve figured out is that with a trait, Project, it’s possible to add projection to basically any wrapping struct. Pin is an obvious one. MaybeUninit also makes sense.

Here’s the trait:

pub trait Project {
    type Base: ?Sized;
    type Output<'a, Field: 'a> where Self: 'a;
    
    unsafe fn project<'a, Field>(&'a self, project_field: fn(*const Self::Base) -> *const Field) -> Self::Output<'a, Field>;
}

To use it, you wrap your projections in a macro, proj!, which is defined as this:

macro_rules! proj {
    ($input:ident.$field:ident) => {{
        unsafe {
            <_ as $crate::Project>::project(&$input, |base| unsafe { core::ptr::addr_of!((*base).$field) })
        }
    }};
    (mut $input:ident.$field:ident) => {{
        unsafe {
            <_ as $crate::ProjectMut>::project_mut(&mut $input, |base| unsafe { core::ptr::addr_of_mut!((*base).$field) })
        }
    }};
}

Then, you can use this like so:

struct Foo {
    a: i32,
}

fn project(p: Pin<&Foo>) -> Pin<&i32> {
    proj!(p.a)
}

I’d love to see field projection eventually make it’s way into the language (with better syntax than a macro, of course).

I’ve published a crate for this so people can play around with it: field-project.

Unfortunately that macro is unsound in (at least) the presence of Drop implementations:

use field_project::proj;
use futures_test::future::{AssertUnmoved, FutureTestExt};
use futures::future::{Ready, ready};

struct Foo {
    field: Option<AssertUnmoved<Ready<()>>>,
}

impl Drop for Foo {
    fn drop(&mut self) {
        self.field.take();
    }
}

fn main() {
    futures::executor::block_on(async {
        let foo = Foo {
            field: Some(ready(()).assert_unmoved()),
        };
        futures::pin_mut!(foo);
        proj!(mut foo.field).as_pin_mut().unwrap().await;
    });
}
thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `0x7ffdf841efc0`,
 right: `0x7ffdf841ec30`: AssertUnmoved moved before drop', crates.io/futures-test-0.3.21/src/assert_unmoved.rs:171:13
1 Like

Ah interesting! Mapping a pin to a field is usually safe—is the edge-case Drop implementations?

I think the brute-force way of fixing this is would be to require Unpin on the ProjectMut implementation for Pin. Maybe there's a more elegant way to solve it?

Yeah, the problem here is that Drop breaks the promises of Pin and provides &mut Self rather than Pin<&mut Self>.

There's thus two concessions to make this not just immediately unsound:

  • If self is mem::swapped, and this is unsound, it is inside the privacy safety barrier of the unsafe code which relies on the pinning. (Any code outside the privacy barrier cannot possibly observe the move; the generic guarantee ends when Drop::drop is called, and the value can no longer be observed by outside code since it's been dropped.)
  • The type is in charge of determining if its fields are transitively pinned. Even with Pin<&mut Self>, the default, safe option is that the fields themselves are not pinned (they are only pinned once you create Pin<&mut Field>). Once unsafe is used to project the pin, this promises that Drop::drop doesn't violate the pinning guarantee of that field.

So, for Pin specifically, the type does have to be aware of pin projection. However, for the UnsafeCell family of types, I believe this outside-world projection is sound (though am not 100% confident). This would work well for e.g. Cell projection.

So, projecting Pin<&mut T> is a no go? Does that include Unpin types?

Take a look at this project by @RustyYato. He did a great job with it, it just needs documentation (sadly, even though I should be helping with it, every moment I have is taken up by other things).

Obviously it doesn't impact Unpin field types, as you can translate between Pin<&mut T> and &mut T freely.

This is also important to note that an Unpin type containing a !Unpin field also makes outside pin projection trivially unsound:

#[derive(Default)]
struct Bomb {
    pinned: PhantomPinned,
}

impl Unpin for Bomb {}

let mut bomb = Bomb::default();

{
    let pin = Pin::new(&mut bomb);
    let pinned: Pin<&mut PhantomPinned> = proj!(mut pin.pinned);
}

let oops: &mut PhantomPinned = &mut bomb.pinned;

The pin_project crate emits guards to prevent incorrect impls of Drop or Unpin from making pin projection unsound.

3 Likes

Hey, thanks for the advice here!

It looks like this won't be as flexible as it could've been otherwise, but it is possible to put bounds on the Field type so that it's Unpin.

impl<P, Field: ?Sized> ProjectMut<Field> for Pin<P> where P: DerefMut, Field: Unpin {
    type OutputMut<'a> where Self: 'a, Field: 'a = Pin<&'a mut Field>;

    unsafe fn project_mut<'a>(&'a mut self, project_field: fn(*mut Self::Base) -> *mut Field) -> Pin<&'a mut Field> {
        unsafe {
            self.as_mut().map_unchecked_mut(|base| &mut *project_field(base))
        }
    }
}

I'm not sure how useful a pin projection that's only for Unpin is, but I think that's safe.