Idea: AbortSafeFuture trait?

It seems reasonable:
trait AbortSafeFuture {
    type Output;
    /// Dropping is safe when the future is complete.
    fn poll(self: Pin<&mut ManuallyDrop<Self>>, cx: &mut Context<'_>) -> Poll<Self::Output>;
    /// Setting the cancellation flag, to make the future complete as early as possible.
    /// Can be used to propagate cancellation for futures.
    fn cancel(self: Pin<&mut ManuallyDrop<Self>>);
}

edit:

pub trait AbortSafeFuture {
    type Output;
    /// Similar to Future in std.
    ///
    /// * The caller cannot destruct `Self`, so it need to free resources by itself.
    /// * When cancelling inner Future, its `poll_cancel` method needs to be called until it returns' Poll::Ready'. Otherwise, leaks may occur.
    fn poll(self: Pin<&mut ManuallyDrop<Self>>, cx: &mut Context<'_>) -> Poll<Self::Output>;
    fn poll_cancel(self: Pin<&mut ManuallyDrop<Self>>, cx: &mut Context<'_>) -> Poll<()>;
}

Does this look similar to what you want to achieve?

I have a couple of issues with this:

  • what are you trying to achieve, why is this reasonable?
  • ManuallyDrop currently cannot be used as a receiver type and I do not see why we would need it in the type signature here.
  • you require some invariants to be upheld ("Dropping is safe when the future is complete.") but never use the unsafe keyword, giving the caller a heads up that there are invariants they need to fulfill.

I'm sorry I haven't been able to fill in the details these days. Here are somethings I tried with this API, and you can see the code details in this repo.

The API actually aims to achieve two goals:

  • Future can sense and handle cancellations, propagating cancellations to the inner Future.
  • Future cannot drop by RAII, in order to prevent Future from being destructed in an incorrect state.

To meet goal one, there is an additional method called poll_cancel than std's Future, which is called when an cancellation is needed. It implements some propagating cancellation logic.

To achieve goal two, poll_* takes the argument self: Pin<&mut ManuallyDrop<Self>> . So we can't move Self around, and we can't destruct Self around.

The two goals are actually related, because the Future in std is currently canceled by destructor, and the drop order does not satisfy Goal 1, the ManuallyDrop is needed to control the drop order. This is also related to AsyncDrop, 将AsyncDrop从AbortSafeFuture中拆分出来 by TOETOE55 · Pull Request #1 · TOETOE55/abort_safe_future · GitHub.

How do you plan to make this backward-compatible? In particular:

  • how can existing Futures opt into being cancellable without breaking changes (like not implementing the old trait anymore)?
  • how can existing runtimes support this new trait while still accepting types that implement the old Future trait?

Also, what are the semantics if poll_cancel is not called? And what if the caller doesn't want to/can't call it?

The trait is intended for some async code that cannot satisfy Future, which may result in undefined behavior when destructed in an incorrect state.

In addition, std Future can implement AbortSafeFuture like

#[pin_project]
pub struct Compat<Fut> {
    #[pin]
    inner: Option<Fut>,
}

/// all the `future: Future` is abort safe
impl<Fut: Future> AbortSafeFuture for Compat<Fut> {
    type Output = <Fut as Future>::Output;

    fn poll(mut self: Pin<&mut ManuallyDrop<Self>>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let mut this = pin_manually_drop_as_mut(&mut self).project();

        let output = if let Some(fut) = this.inner.as_mut().as_pin_mut() {
            ready!(fut.poll(cx))
        } else {
            panic!("Compat::poll called after completion or after canceled")
        };

        // drop inner future
        this.inner.set(None);
        Poll::Ready(output)
    }

    fn poll_cancel(mut self: Pin<&mut ManuallyDrop<Self>>, _cx: &mut Context<'_>) -> Poll<()> {
        let mut this = pin_manually_drop_as_mut(&mut self).project();
        // drop inner future
        this.inner.set(None);
        Poll::Ready(())
    }
}

what are the semantics if poll_cancel is not called?

This may lead to memory leaks, but not UB.

Could you explain, what the difference between CompletionFuture and AbortSafeFuture is, because to me it seems like it does the same thing that you want to achieve.

CompletionFuture does this by requiring the caller to promise that they do not drop or forget the future. I think it is weird to wrap it inside of an ManuallyDrop, because you also require the caller to not call e.g. ManuallyDrop::drop/into_inner. But you also place additional burden on implementors of AbortSafeFuture, because to avoid memory leaks, they need to manually drop the future.

async code always satisfies Future, maybe you're thinking of manually implemented Futures that need to be destructed? But then:

  • they could already be implemented today with some fallbacks, in that case can you implement AbortSafeFuture for them without de-implementing Future (since that would be a breaking change)?

  • leaks being safe means a bunch of usecases won't be sound, for example io-uring.

You're contradicting yourself here. Does it lead to undefined behaviour (UB) or not?

You're not implementing AbortSafeFuture for the current Futures, you're implementing it for a new type Compat. Thus if runtimes are changed to take an AbortSafeFuture the existing code that uses them with the current Futures will break because it isn't using your Compat type. This is a breaking change.

I guess the two goals are the same. I just want a safe trait. :joy:

Oops, Pin<&mut ManuallyDrop<Self>> doesn't seem to prevent Self from dropping... :smiling_face_with_tear:

fn foo(x: Pin<&mut ManuallyDrop<A>>) {
    **x = A::new();
   // then the old value will be dropped.
}
1 Like

Is this the expected behavior of ManuallyDrop?

Yes this should be expected behavior, because you use the DerefMut impl of ManuallyDrop and receive a &mut A from that. If you write to &mut A then you always drop the old value.

ManuallyDrop only guarantees that its contents are not dropped when it is dropped (in the example it is not dropped, because the function takes it by reference).

You could use ptr::write to write without dropping the value.