I turned an async block into a named future that I can impl methods on (transmute used)

I've been wanting a future

  • made from an async block
  • holding exclusive reference to some externally owned data
  • and exposed as a struct so that I can impl custom methods on it

I figured I would need to create another struct, impl future for it, and hold ownership to the data and ownership to a boxed future that will be a view of that data. This is what I ended up with after wrestling with the self-referential lifetime issue.

use aliasable::prelude::AliasableBox;

struct AsyncBlockHandler<T> {
    // Safety: view field must always come before owned field
    // so it (view field) drops first. 
    //
    // Safety: does not actually have a 'static lifetime, but
    // a lifetime of 'owned. Meaning the lifetime of the owned field.
    view: Pin<Box<dyn Future<Output = ()> + 'static>>,
    // Safety: must never never move/drop the owning 
    // data field, without dropping the view field first.
    owned: AliasableBox<T>,
}

impl<T> Future for AsyncBlockHandler<T> {
    type Output = ();

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        self.view.poll_unpin(cx)
    }
}

impl<T> AsyncBlockHandler<T> {
    fn new(owned: T, view_builder: Box<dyn for<'a> FnOnce(&'a mut T) -> Pin<Box<dyn Future<Output = ()> + 'a>>>) -> Self {
        let mut owned = AliasableBox::from_unique(Box::new(owned));
        let view = view_builder(&mut owned);

        // Safety: transmuting Thing<'a> to Thing<'static>, if Thing does a 
        // unsafe impl Send for Thing<'static>, then Thing would end up implementing 
        // Send. Can Thing be Send and remain sound? 
        //
        // self.view should be !Send and !Sync?
        let view = unsafe { std::mem::transmute(view) };

        Self { view, owned }
    }
}

It can be used like so

struct Struct<R>(R);
impl<R: AsyncRead + Send + Unpin> Struct<R> {
    fn build(self) -> AsyncBlockHandler<Self> {        
        let f: Box<dyn for<'a> FnOnce(&'a mut Self) -> Pin<Box<dyn Future<Output = ()> + 'a>>> = Box::new(|_self: &mut Self| {
            async move {
                    let mut buf = String::new();
                    _self.1.read_to_string(&mut buf).await;
                }.boxed_local() // boxed() or boxed_local() ?
        });

        AsyncBlockHandler::new(self, f)
    }
}

I can make the Future<Output = ()> more generic. But now I can impl traits and functions on AsyncBlockHandler or have it delegate calls to it's <T> while also being able to .await it like a regular future. I don't believe there is anyway to screw up AsyncBlockHandler I should be able to attach foo(self), foo(&self) and foo(&mut self) methods on it. Edge case consequences for transmute are a mystery to me though.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.