Skip to content

New projection macro #20

@Pauan

Description

@Pauan

I have used pin-utils extensively on a Pin-heavy library.

Overall I found it rather unergonomic, so I created a new macro:

#[macro_export]
macro_rules! unsafe_project {
    (@parse $value:expr,) => {};
    (@parse $value:expr, pin $name:ident, $($rest:tt)*) => {
        #[allow(unused_mut)]
        let mut $name = unsafe { ::std::pin::Pin::new_unchecked(&mut $value.$name) };
        $crate::unsafe_project! { @parse $value, $($rest)* }
    };
    (@parse $value:expr, mut $name:ident, $($rest:tt)*) => {
        #[allow(unused_mut)]
        let mut $name = &mut $value.$name;
        $crate::unsafe_project! { @parse $value, $($rest)* }
    };

    ($value:expr => { $($bindings:tt)+ }) => {
        let value = unsafe { ::std::pin::Pin::get_unchecked_mut($value) };
        $crate::unsafe_project! { @parse value, $($bindings)+ }
    };
}

Here is an example of using it:

unsafe_project!(self => {
    pin signal1,
    pin signal2,
    mut left,
    mut right,
    mut callback,
});

Basically, you pass in something which is pinned (in this case self), and then it will destructure the individual fields (which are marked as either pin or mut).

Using pin is similar to unsafe_pinned and using mut is similar to unsafe_unpinned.


Here is an example of real code using the pin-utils macros (unsafe_pinned and unsafe_unpinned):

#[derive(Debug)]
#[must_use = "Futures do nothing unless polled"]
pub struct CancelableFuture<A, B> {
    state: Arc<CancelableFutureState>,
    future: Option<A>,
    when_cancelled: Option<B>,
}

impl<A, B> CancelableFuture<A, B> {
    unsafe_unpinned!(state: Arc<CancelableFutureState>);
    unsafe_pinned!(future: Option<A>);
    unsafe_unpinned!(when_cancelled: Option<B>);
}

impl<A, B> Unpin for CancelableFuture<A, B> where A: Unpin {}

impl<A, B> Future for CancelableFuture<A, B>
    where A: Future,
          B: FnOnce() -> A::Output {

    type Output = A::Output;

    fn poll(mut self: Pin<&mut Self>, waker: &LocalWaker) -> Poll<Self::Output> {
        if self.as_mut().state().is_cancelled.load(Ordering::SeqCst) {
            Pin::set(self.as_mut().future(), None);
            let callback = self.as_mut().when_cancelled().take().unwrap();
            Poll::Ready(callback())

        } else {
            match self.as_mut().future().as_pin_mut().unwrap().poll(waker) {
                Poll::Pending => {
                    *self.as_mut().state().waker.lock().unwrap() = Some(waker.clone().into_waker());
                    Poll::Pending
                },
                a => a,
            }
        }
    }
}

And the same code with my unsafe_project macro:

#[derive(Debug)]
#[must_use = "Futures do nothing unless polled"]
pub struct CancelableFuture<A, B> {
    state: Arc<CancelableFutureState>,
    future: Option<A>,
    when_cancelled: Option<B>,
}

impl<A, B> Unpin for CancelableFuture<A, B> where A: Unpin {}

impl<A, B> Future for CancelableFuture<A, B>
    where A: Future,
          B: FnOnce() -> A::Output {

    type Output = A::Output;

    fn poll(self: Pin<&mut Self>, waker: &LocalWaker) -> Poll<Self::Output> {
        unsafe_project!(self => {
            mut state,
            pin future,
            mut when_cancelled,
        });

        if state.is_cancelled.load(Ordering::SeqCst) {
            Pin::set(future, None);
            let callback = when_cancelled.take().unwrap();
            Poll::Ready(callback())

        } else {
            match future.as_pin_mut().unwrap().poll(waker) {
                Poll::Pending => {
                    *state.waker.lock().unwrap() = Some(waker.clone().into_waker());
                    Poll::Pending
                },
                a => a,
            }
        }
    }
}

There are three huge advantages to this:

  1. It's overall much shorter. Rather than using self.as_mut().future() you just use future. This makes working with pinned structs almost as convenient as unpinned structs!

  2. unsafe_pinned and unsafe_unpinned require you to specify the bounds/types of the fields twice. But with unsafe_project you don't need to.

  3. It fully supports disjoint borrows, unlike unsafe_pinned and unsafe_unpinned.

    This allowed me to take this horrible code... and replace it with this lovely code.

    In addition, there are a couple places where I need disjoint borrows, like here. Using unsafe_project works great, but I cannot use unsafe_pinned and unsafe_unpinned for this.

I ported my entire codebase to use unsafe_project, overall I'm very happy with it.

The only downside I found is that you can't call other self methods at the same time as using unsafe_project:

unsafe_project!(self.as_mut() => {
    pin signal1,
    pin signal2,
    mut left,
    mut right,
    mut callback,
});

// Error: doesn't work, since `self` is borrowed
self.foo();

Are you interested in adding in a macro like unsafe_project into pin-utils?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions