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.