SilverMira opened issue #14366:
Feature
Currently, spawning a task into an
Accessor's event loop is very boilerplate heavy due to theAccessorTasksignature needing to create aFuturewith lifetime constrained to&Accessor<T>. After suffering to write boilerplate for spawning tasks over and over again. I decided to dive deeper into making the ideal API withAsyncFnOncepossible, and I think I managed to do it.I have included the code snippet that I managed to make
AsyncFnOncespawnable directly onAccessor::spawnvia an extension trait. No unstable features were used, and no boxing ofFuture. I think MSRV shouldn't be affected, I did not check the actual MSRV but the only relatively new feature we are using here should beAsyncFnOnce.I did not create a PR yet, since I think someone has to decide whether breaking the public API as of now is appropriate or not.
Benefit
No boilerplate! 34 -> 17 lines of code! 50% reduction!
// T: Send is required because of `TypedFunc::call_concurrent`'s bounds // 17 lines of code async fn poc_new<T: Send>( accessor: &wasmtime::component::Accessor<T>, func: wasmtime::component::TypedFunc<(i64,), (String,)>, ) { let (tx, rx) = oneshot::async_channel(); accessor .spawn_fn(async move |accessor| { let (result,) = func.call_concurrent(accessor, (32,)).await?; let _ = tx.send(result); Ok(()) }) .unwrap(); let string = rx.await.unwrap(); println!("value: {string}"); } // T: Send is required because of `TypedFunc::call_concurrent`'s bounds // 34 lines of code async fn poc_old<T: Send>( accessor: &wasmtime::component::Accessor<T>, func: wasmtime::component::TypedFunc<(i64,), (String,)>, ) { let (tx, rx) = oneshot::async_channel(); struct Task<T: Send> { func: wasmtime::component::TypedFunc<(i64,), (String,)>, tx: oneshot::Sender<String>, _marker: PhantomData<fn() -> T>, } impl<T: Send> wasmtime::component::AccessorTask<T> for Task<T> where Self: 'static, { async fn run( self, accessor: &wasmtime::component::Accessor<T, wasmtime::component::HasSelf<T>>, ) -> wasmtime::Result<()> { let (result,) = self.func.call_concurrent(accessor, (32,)).await?; let _ = self.tx.send(result); Ok(()) } } accessor .spawn(Task { func, tx, _marker: PhantomData, }) .unwrap(); let string = rx.await.unwrap(); println!("value: {string}"); }Implementation
Behold, the lifetime tricks that even I didn't know was possible
// This is the main trait that we will coerce AsyncFnOnce into, the `run` func matches the // signature on `AccessorTask`, however the lifetime on `&Accessor` is named as `'a`, and a generic lifetime // `'fut` is on the trait which is constrained to be the same as `'a` #[doc(hidden)] pub trait AsyncFnOnceAccessorTask<'fut, T, D: wasmtime::component::HasData> { fn run<'a>( self, accessor: &'a wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send + 'a where // Tell rust that 'fut is same as 'a, so we can use // for<'fut> T: AsyncFnOnceAccessorTask<'fut> later to model AccessorTask's lifetime // on the `run` function 'fut: 'a, 'a: 'fut; } // wrapper needed for orphan rule #[repr(transparent)] struct AsyncFnOnceAccessorTaskWrapper<F>(F); // Implement `AccessorTask` on our wrapper so we can spawn it through `Accessor::spawn` impl<F, T, D> wasmtime::component::AccessorTask<T, D> for AsyncFnOnceAccessorTaskWrapper<F> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, D: wasmtime::component::HasData, { fn run( self, accessor: &wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send { (self.0).run(accessor) } } // Blanket implementation for coercing // `impl for<'a> FnOnce(&'a wasmtime::component::Accessor) -> impl Future<Output = wasmtime::Result<()> + Send + 'a` // into our trait, without `impl_trait_in_fn_trait_return` or `return_type_notation` impl<'fut, F, Fut, T, D> AsyncFnOnceAccessorTask<'fut, T, D> for F where T: 'static, F: FnOnce(&'fut wasmtime::component::Accessor<T, D>) -> Fut, Fut: Future<Output = wasmtime::Result<()>> + Send + 'fut, D: wasmtime::component::HasData, { fn run<'a>( self, accessor: &'a wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send + 'a where 'fut: 'a, 'a: 'fut, { (self)(accessor) } } // Extension trait on Accessor for spawning AccessorTask from async closures pub trait AccessorExt<T, D: wasmtime::component::HasData> { fn spawn_fn<F>(&self, f: F) -> wasmtime::Result<wasmtime::component::JoinHandle> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>; } impl<T, D> AccessorExt<T, D> for wasmtime::component::Accessor<T, D> where D: wasmtime::component::HasData, { fn spawn_fn<F>(&self, f: F) -> wasmtime::Result<wasmtime::component::JoinHandle> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, // Technically, the below is redundant since the blanket implementation of AsyncFnOnceAccessorTask // is the same as the bounds below. However putting it here helps rust to infer F is supposed // to be a closure properly, and allows us to skip spelling out closure params and return type. // Note that the bounds below do not constraint the unnamed `Future` type to be `Send`, the // `Send` bound enforced within AsyncFnOnceAccessorTask F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>, { self.spawn(AsyncFnOnceAccessorTaskWrapper(f)) } } // Make an accessor task from a closure, utility function, not used in this POC pub fn accessor_task<F, T, D>(f: F) -> impl wasmtime::component::AccessorTask<T, D> + 'static where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>, D: wasmtime::component::HasData, { AsyncFnOnceAccessorTaskWrapper(f) }
SilverMira edited issue #14366:
Feature
Currently, spawning a task into an
Accessor's event loop is very boilerplate heavy due to theAccessorTasksignature needing to create aFuturewith lifetime constrained to&Accessor<T>. After suffering to write boilerplate for spawning tasks over and over again. I decided to dive deeper into making the ideal API withAsyncFnOncepossible, and I think I managed to do it.I have included the code snippet that I managed to make
AsyncFnOncespawnable directly onAccessor::spawnvia an extension trait. No unstable features were used, and no boxing ofFuture. I think MSRV shouldn't be affected, I did not check the actual MSRV but the only relatively new feature we are using here should beAsyncFnOnce.I did not create a PR yet, since I think someone has to decide whether breaking the public API as of now is appropriate or not, or having a companion
spawn_fnBenefit
No boilerplate! 34 -> 17 lines of code! 50% reduction!
// T: Send is required because of `TypedFunc::call_concurrent`'s bounds // 17 lines of code async fn poc_new<T: Send>( accessor: &wasmtime::component::Accessor<T>, func: wasmtime::component::TypedFunc<(i64,), (String,)>, ) { let (tx, rx) = oneshot::async_channel(); accessor .spawn_fn(async move |accessor| { let (result,) = func.call_concurrent(accessor, (32,)).await?; let _ = tx.send(result); Ok(()) }) .unwrap(); let string = rx.await.unwrap(); println!("value: {string}"); } // T: Send is required because of `TypedFunc::call_concurrent`'s bounds // 34 lines of code async fn poc_old<T: Send>( accessor: &wasmtime::component::Accessor<T>, func: wasmtime::component::TypedFunc<(i64,), (String,)>, ) { let (tx, rx) = oneshot::async_channel(); struct Task<T: Send> { func: wasmtime::component::TypedFunc<(i64,), (String,)>, tx: oneshot::Sender<String>, _marker: PhantomData<fn() -> T>, } impl<T: Send> wasmtime::component::AccessorTask<T> for Task<T> where Self: 'static, { async fn run( self, accessor: &wasmtime::component::Accessor<T, wasmtime::component::HasSelf<T>>, ) -> wasmtime::Result<()> { let (result,) = self.func.call_concurrent(accessor, (32,)).await?; let _ = self.tx.send(result); Ok(()) } } accessor .spawn(Task { func, tx, _marker: PhantomData, }) .unwrap(); let string = rx.await.unwrap(); println!("value: {string}"); }Implementation
Behold, the lifetime tricks that even I didn't know was possible
// This is the main trait that we will coerce AsyncFnOnce into, the `run` func matches the // signature on `AccessorTask`, however the lifetime on `&Accessor` is named as `'a`, and a generic lifetime // `'fut` is on the trait which is constrained to be the same as `'a` #[doc(hidden)] pub trait AsyncFnOnceAccessorTask<'fut, T, D: wasmtime::component::HasData> { fn run<'a>( self, accessor: &'a wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send + 'a where // Tell rust that 'fut is same as 'a, so we can use // for<'fut> T: AsyncFnOnceAccessorTask<'fut> later to model AccessorTask's lifetime // on the `run` function 'fut: 'a, 'a: 'fut; } // wrapper needed for orphan rule #[repr(transparent)] struct AsyncFnOnceAccessorTaskWrapper<F>(F); // Implement `AccessorTask` on our wrapper so we can spawn it through `Accessor::spawn` impl<F, T, D> wasmtime::component::AccessorTask<T, D> for AsyncFnOnceAccessorTaskWrapper<F> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, D: wasmtime::component::HasData, { fn run( self, accessor: &wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send { (self.0).run(accessor) } } // Blanket implementation for coercing // `impl for<'a> FnOnce(&'a wasmtime::component::Accessor) -> impl Future<Output = wasmtime::Result<()> + Send + 'a` // into our trait, without `impl_trait_in_fn_trait_return` or `return_type_notation` impl<'fut, F, Fut, T, D> AsyncFnOnceAccessorTask<'fut, T, D> for F where T: 'static, F: FnOnce(&'fut wasmtime::component::Accessor<T, D>) -> Fut, Fut: Future<Output = wasmtime::Result<()>> + Send + 'fut, D: wasmtime::component::HasData, { fn run<'a>( self, accessor: &'a wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send + 'a where 'fut: 'a, 'a: 'fut, { (self)(accessor) } } // Extension trait on Accessor for spawning AccessorTask from async closures pub trait AccessorExt<T, D: wasmtime::component::HasData> { fn spawn_fn<F>(&self, f: F) -> wasmtime::Result<wasmtime::component::JoinHandle> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>; } impl<T, D> AccessorExt<T, D> for wasmtime::component::Accessor<T, D> where D: wasmtime::component::HasData, { fn spawn_fn<F>(&self, f: F) -> wasmtime::Result<wasmtime::component::JoinHandle> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, // Technically, the below is redundant since the blanket implementation of AsyncFnOnceAccessorTask // is the same as the bounds below. However putting it here helps rust to infer F is supposed // to be a closure properly, and allows us to skip spelling out closure params and return type. // Note that the bounds below do not constraint the unnamed `Future` type to be `Send`, the // `Send` bound enforced within AsyncFnOnceAccessorTask F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>, { self.spawn(AsyncFnOnceAccessorTaskWrapper(f)) } } // Make an accessor task from a closure, utility function, not used in this POC pub fn accessor_task<F, T, D>(f: F) -> impl wasmtime::component::AccessorTask<T, D> + 'static where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>, D: wasmtime::component::HasData, { AsyncFnOnceAccessorTaskWrapper(f) }
SilverMira edited issue #14366:
Feature
Currently, spawning a task into an
Accessor's event loop is very boilerplate heavy due to theAccessorTasksignature needing to create aFuturewith lifetime constrained to&Accessor<T>. After suffering to write boilerplate for spawning tasks over and over again. I decided to dive deeper into making the ideal API withAsyncFnOncepossible, and I think I managed to do it.I have included the code snippet that I managed to make
AsyncFnOncespawnable directly onAccessor::spawnvia an extension trait. No unstable features were used, and no boxing ofFuture. I think MSRV shouldn't be affected, I did not check the actual MSRV but the only relatively new feature we are using here should beAsyncFnOnce.I did not create a PR yet, since I think someone has to decide whether breaking the public API as of now is appropriate or not, or having a companion
Accessor::spawn_fnBenefit
No boilerplate! 34 -> 17 lines of code! 50% reduction!
// T: Send is required because of `TypedFunc::call_concurrent`'s bounds // 17 lines of code async fn poc_new<T: Send>( accessor: &wasmtime::component::Accessor<T>, func: wasmtime::component::TypedFunc<(i64,), (String,)>, ) { let (tx, rx) = oneshot::async_channel(); accessor .spawn_fn(async move |accessor| { let (result,) = func.call_concurrent(accessor, (32,)).await?; let _ = tx.send(result); Ok(()) }) .unwrap(); let string = rx.await.unwrap(); println!("value: {string}"); } // T: Send is required because of `TypedFunc::call_concurrent`'s bounds // 34 lines of code async fn poc_old<T: Send>( accessor: &wasmtime::component::Accessor<T>, func: wasmtime::component::TypedFunc<(i64,), (String,)>, ) { let (tx, rx) = oneshot::async_channel(); struct Task<T: Send> { func: wasmtime::component::TypedFunc<(i64,), (String,)>, tx: oneshot::Sender<String>, _marker: PhantomData<fn() -> T>, } impl<T: Send> wasmtime::component::AccessorTask<T> for Task<T> where Self: 'static, { async fn run( self, accessor: &wasmtime::component::Accessor<T, wasmtime::component::HasSelf<T>>, ) -> wasmtime::Result<()> { let (result,) = self.func.call_concurrent(accessor, (32,)).await?; let _ = self.tx.send(result); Ok(()) } } accessor .spawn(Task { func, tx, _marker: PhantomData, }) .unwrap(); let string = rx.await.unwrap(); println!("value: {string}"); }Implementation
Behold, the lifetime tricks that even I didn't know was possible
// This is the main trait that we will coerce AsyncFnOnce into, the `run` func matches the // signature on `AccessorTask`, however the lifetime on `&Accessor` is named as `'a`, and a generic lifetime // `'fut` is on the trait which is constrained to be the same as `'a` #[doc(hidden)] pub trait AsyncFnOnceAccessorTask<'fut, T, D: wasmtime::component::HasData> { fn run<'a>( self, accessor: &'a wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send + 'a where // Tell rust that 'fut is same as 'a, so we can use // for<'fut> T: AsyncFnOnceAccessorTask<'fut> later to model AccessorTask's lifetime // on the `run` function 'fut: 'a, 'a: 'fut; } // wrapper needed for orphan rule #[repr(transparent)] struct AsyncFnOnceAccessorTaskWrapper<F>(F); // Implement `AccessorTask` on our wrapper so we can spawn it through `Accessor::spawn` impl<F, T, D> wasmtime::component::AccessorTask<T, D> for AsyncFnOnceAccessorTaskWrapper<F> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, D: wasmtime::component::HasData, { fn run( self, accessor: &wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send { (self.0).run(accessor) } } // Blanket implementation for coercing // `impl for<'a> FnOnce(&'a wasmtime::component::Accessor) -> impl Future<Output = wasmtime::Result<()> + Send + 'a` // into our trait, without `impl_trait_in_fn_trait_return` or `return_type_notation` impl<'fut, F, Fut, T, D> AsyncFnOnceAccessorTask<'fut, T, D> for F where T: 'static, F: FnOnce(&'fut wasmtime::component::Accessor<T, D>) -> Fut, Fut: Future<Output = wasmtime::Result<()>> + Send + 'fut, D: wasmtime::component::HasData, { fn run<'a>( self, accessor: &'a wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send + 'a where 'fut: 'a, 'a: 'fut, { (self)(accessor) } } // Extension trait on Accessor for spawning AccessorTask from async closures pub trait AccessorExt<T, D: wasmtime::component::HasData> { fn spawn_fn<F>(&self, f: F) -> wasmtime::Result<wasmtime::component::JoinHandle> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>; } impl<T, D> AccessorExt<T, D> for wasmtime::component::Accessor<T, D> where D: wasmtime::component::HasData, { fn spawn_fn<F>(&self, f: F) -> wasmtime::Result<wasmtime::component::JoinHandle> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, // Technically, the below is redundant since the blanket implementation of AsyncFnOnceAccessorTask // is the same as the bounds below. However putting it here helps rust to infer F is supposed // to be a closure properly, and allows us to skip spelling out closure params and return type. // Note that the bounds below do not constraint the unnamed `Future` type to be `Send`, the // `Send` bound enforced within AsyncFnOnceAccessorTask F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>, { self.spawn(AsyncFnOnceAccessorTaskWrapper(f)) } } // Make an accessor task from a closure, utility function, not used in this POC pub fn accessor_task<F, T, D>(f: F) -> impl wasmtime::component::AccessorTask<T, D> + 'static where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>, D: wasmtime::component::HasData, { AsyncFnOnceAccessorTaskWrapper(f) }
SilverMira edited issue #14366:
Feature
Currently, spawning a task into an
Accessor's event loop is very boilerplate heavy due to theAccessorTasksignature needing to create aFuturewith lifetime constrained to&Accessor<T>. After suffering to write boilerplate for spawning tasks over and over again. I decided to dive deeper into making the ideal API withAsyncFnOncepossible, and I think I managed to do it.I have included the code snippet that I managed to make
AsyncFnOncespawnable directly onAccessor::spawnvia an extension trait. No unstable features were used, and no boxing ofFuture. I think MSRV shouldn't be affected, I did not check the actual MSRV but the only relatively new feature we are using here should beAsyncFnOnce, this is tested on nightly rust1.100.0I did not create a PR yet, since I think someone has to decide whether breaking the public API as of now is appropriate or not, or having a companion
Accessor::spawn_fnBenefit
No boilerplate! 34 -> 17 lines of code! 50% reduction!
// T: Send is required because of `TypedFunc::call_concurrent`'s bounds // 17 lines of code async fn poc_new<T: Send>( accessor: &wasmtime::component::Accessor<T>, func: wasmtime::component::TypedFunc<(i64,), (String,)>, ) { let (tx, rx) = oneshot::async_channel(); accessor .spawn_fn(async move |accessor| { let (result,) = func.call_concurrent(accessor, (32,)).await?; let _ = tx.send(result); Ok(()) }) .unwrap(); let string = rx.await.unwrap(); println!("value: {string}"); } // T: Send is required because of `TypedFunc::call_concurrent`'s bounds // 34 lines of code async fn poc_old<T: Send>( accessor: &wasmtime::component::Accessor<T>, func: wasmtime::component::TypedFunc<(i64,), (String,)>, ) { let (tx, rx) = oneshot::async_channel(); struct Task<T: Send> { func: wasmtime::component::TypedFunc<(i64,), (String,)>, tx: oneshot::Sender<String>, _marker: PhantomData<fn() -> T>, } impl<T: Send> wasmtime::component::AccessorTask<T> for Task<T> where Self: 'static, { async fn run( self, accessor: &wasmtime::component::Accessor<T, wasmtime::component::HasSelf<T>>, ) -> wasmtime::Result<()> { let (result,) = self.func.call_concurrent(accessor, (32,)).await?; let _ = self.tx.send(result); Ok(()) } } accessor .spawn(Task { func, tx, _marker: PhantomData, }) .unwrap(); let string = rx.await.unwrap(); println!("value: {string}"); }Implementation
Behold, the lifetime tricks that even I didn't know was possible
// This is the main trait that we will coerce AsyncFnOnce into, the `run` func matches the // signature on `AccessorTask`, however the lifetime on `&Accessor` is named as `'a`, and a generic lifetime // `'fut` is on the trait which is constrained to be the same as `'a` #[doc(hidden)] pub trait AsyncFnOnceAccessorTask<'fut, T, D: wasmtime::component::HasData> { fn run<'a>( self, accessor: &'a wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send + 'a where // Tell rust that 'fut is same as 'a, so we can use // for<'fut> T: AsyncFnOnceAccessorTask<'fut> later to model AccessorTask's lifetime // on the `run` function 'fut: 'a, 'a: 'fut; } // wrapper needed for orphan rule #[repr(transparent)] struct AsyncFnOnceAccessorTaskWrapper<F>(F); // Implement `AccessorTask` on our wrapper so we can spawn it through `Accessor::spawn` impl<F, T, D> wasmtime::component::AccessorTask<T, D> for AsyncFnOnceAccessorTaskWrapper<F> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, D: wasmtime::component::HasData, { fn run( self, accessor: &wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send { (self.0).run(accessor) } } // Blanket implementation for coercing // `impl for<'a> FnOnce(&'a wasmtime::component::Accessor) -> impl Future<Output = wasmtime::Result<()> + Send + 'a` // into our trait, without `impl_trait_in_fn_trait_return` or `return_type_notation` impl<'fut, F, Fut, T, D> AsyncFnOnceAccessorTask<'fut, T, D> for F where T: 'static, F: FnOnce(&'fut wasmtime::component::Accessor<T, D>) -> Fut, Fut: Future<Output = wasmtime::Result<()>> + Send + 'fut, D: wasmtime::component::HasData, { fn run<'a>( self, accessor: &'a wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send + 'a where 'fut: 'a, 'a: 'fut, { (self)(accessor) } } // Extension trait on Accessor for spawning AccessorTask from async closures pub trait AccessorExt<T, D: wasmtime::component::HasData> { fn spawn_fn<F>(&self, f: F) -> wasmtime::Result<wasmtime::component::JoinHandle> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>; } impl<T, D> AccessorExt<T, D> for wasmtime::component::Accessor<T, D> where D: wasmtime::component::HasData, { fn spawn_fn<F>(&self, f: F) -> wasmtime::Result<wasmtime::component::JoinHandle> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, // Technically, the below is redundant since the blanket implementation of AsyncFnOnceAccessorTask // is the same as the bounds below. However putting it here helps rust to infer F is supposed // to be a closure properly, and allows us to skip spelling out closure params and return type. // Note that the bounds below do not constraint the unnamed `Future` type to be `Send`, the // `Send` bound enforced within AsyncFnOnceAccessorTask F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>, { self.spawn(AsyncFnOnceAccessorTaskWrapper(f)) } } // Make an accessor task from a closure, utility function, not used in this POC pub fn accessor_task<F, T, D>(f: F) -> impl wasmtime::component::AccessorTask<T, D> + 'static where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>, D: wasmtime::component::HasData, { AsyncFnOnceAccessorTaskWrapper(f) }
SilverMira edited issue #14366:
Feature
Currently, spawning a task into an
Accessor's event loop is very boilerplate heavy due to theAccessorTasksignature needing to create aFuturewith lifetime constrained to&Accessor<T>. After suffering to write boilerplate for spawning tasks over and over again. I decided to dive deeper into making the ideal API withAsyncFnOncepossible, and I think I managed to do it.I have included the code snippet that I managed to make
AsyncFnOncespawnable directly onAccessor::spawnvia an extension trait. No unstable features were used, and no boxing ofFuture. I think MSRV shouldn't be affected, I did not check the actual MSRV but the only relatively new feature we are using here should beAsyncFnOnce, this is tested on nightly rust1.100.0. Since I'm on this version, I did also verified that this works on both the nightly trait solver, and the stable one viaRUSTFLAGS=-Znext-solver=coherenceI did not create a PR yet, since I think someone has to decide whether breaking the public API as of now is appropriate or not, or having a companion
Accessor::spawn_fnBenefit
No boilerplate! 34 -> 17 lines of code! 50% reduction!
// T: Send is required because of `TypedFunc::call_concurrent`'s bounds // 17 lines of code async fn poc_new<T: Send>( accessor: &wasmtime::component::Accessor<T>, func: wasmtime::component::TypedFunc<(i64,), (String,)>, ) { let (tx, rx) = oneshot::async_channel(); accessor .spawn_fn(async move |accessor| { let (result,) = func.call_concurrent(accessor, (32,)).await?; let _ = tx.send(result); Ok(()) }) .unwrap(); let string = rx.await.unwrap(); println!("value: {string}"); } // T: Send is required because of `TypedFunc::call_concurrent`'s bounds // 34 lines of code async fn poc_old<T: Send>( accessor: &wasmtime::component::Accessor<T>, func: wasmtime::component::TypedFunc<(i64,), (String,)>, ) { let (tx, rx) = oneshot::async_channel(); struct Task<T: Send> { func: wasmtime::component::TypedFunc<(i64,), (String,)>, tx: oneshot::Sender<String>, _marker: PhantomData<fn() -> T>, } impl<T: Send> wasmtime::component::AccessorTask<T> for Task<T> where Self: 'static, { async fn run( self, accessor: &wasmtime::component::Accessor<T, wasmtime::component::HasSelf<T>>, ) -> wasmtime::Result<()> { let (result,) = self.func.call_concurrent(accessor, (32,)).await?; let _ = self.tx.send(result); Ok(()) } } accessor .spawn(Task { func, tx, _marker: PhantomData, }) .unwrap(); let string = rx.await.unwrap(); println!("value: {string}"); }Implementation
Behold, the lifetime tricks that even I didn't know was possible
// This is the main trait that we will coerce AsyncFnOnce into, the `run` func matches the // signature on `AccessorTask`, however the lifetime on `&Accessor` is named as `'a`, and a generic lifetime // `'fut` is on the trait which is constrained to be the same as `'a` #[doc(hidden)] pub trait AsyncFnOnceAccessorTask<'fut, T, D: wasmtime::component::HasData> { fn run<'a>( self, accessor: &'a wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send + 'a where // Tell rust that 'fut is same as 'a, so we can use // for<'fut> T: AsyncFnOnceAccessorTask<'fut> later to model AccessorTask's lifetime // on the `run` function 'fut: 'a, 'a: 'fut; } // wrapper needed for orphan rule #[repr(transparent)] struct AsyncFnOnceAccessorTaskWrapper<F>(F); // Implement `AccessorTask` on our wrapper so we can spawn it through `Accessor::spawn` impl<F, T, D> wasmtime::component::AccessorTask<T, D> for AsyncFnOnceAccessorTaskWrapper<F> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, D: wasmtime::component::HasData, { fn run( self, accessor: &wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send { (self.0).run(accessor) } } // Blanket implementation for coercing // `impl for<'a> FnOnce(&'a wasmtime::component::Accessor) -> impl Future<Output = wasmtime::Result<()> + Send + 'a` // into our trait, without `impl_trait_in_fn_trait_return` or `return_type_notation` impl<'fut, F, Fut, T, D> AsyncFnOnceAccessorTask<'fut, T, D> for F where T: 'static, F: FnOnce(&'fut wasmtime::component::Accessor<T, D>) -> Fut, Fut: Future<Output = wasmtime::Result<()>> + Send + 'fut, D: wasmtime::component::HasData, { fn run<'a>( self, accessor: &'a wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send + 'a where 'fut: 'a, 'a: 'fut, { (self)(accessor) } } // Extension trait on Accessor for spawning AccessorTask from async closures pub trait AccessorExt<T, D: wasmtime::component::HasData> { fn spawn_fn<F>(&self, f: F) -> wasmtime::Result<wasmtime::component::JoinHandle> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>; } impl<T, D> AccessorExt<T, D> for wasmtime::component::Accessor<T, D> where D: wasmtime::component::HasData, { fn spawn_fn<F>(&self, f: F) -> wasmtime::Result<wasmtime::component::JoinHandle> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, // Technically, the below is redundant since the blanket implementation of AsyncFnOnceAccessorTask // is the same as the bounds below. However putting it here helps rust to infer F is supposed // to be a closure properly, and allows us to skip spelling out closure params and return type. // Note that the bounds below do not constraint the unnamed `Future` type to be `Send`, the // `Send` bound enforced within AsyncFnOnceAccessorTask F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>, { self.spawn(AsyncFnOnceAccessorTaskWrapper(f)) } } // Make an accessor task from a closure, utility function, not used in this POC pub fn accessor_task<F, T, D>(f: F) -> impl wasmtime::component::AccessorTask<T, D> + 'static where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>, D: wasmtime::component::HasData, { AsyncFnOnceAccessorTaskWrapper(f) }
SilverMira edited issue #14366:
Feature
Currently, spawning a task into an
Accessor's event loop is very boilerplate heavy due to theAccessorTasksignature needing to create aFuturewith lifetime constrained to&Accessor<T>. After suffering to write boilerplate for spawning tasks over and over again. I decided to dive deeper into making the ideal API withAsyncFnOncepossible, and I think I managed to do it.I have included the code snippet that I managed to make
AsyncFnOncespawnable directly onAccessor::spawnvia an extension trait. No unstable features were used, and no boxing ofFuture. I think MSRV shouldn't be affected, I did not check the actual MSRV but the only relatively new feature we are using here should beAsyncFnOnce, this is tested on nightly rust1.100.0. Since I'm on this version, I did also verified that this works on both the nightly trait solver, and the stable one viaRUSTFLAGS=-Znext-solver=coherenceI did not create a PR yet, since I think someone has to decide whether this kind of hack(?) is appropriate or not, and whether the public API should be changed, or having do we leave
Accessor::spawnas is and instead have a companionAccessor::spawn_fnBenefit
No boilerplate! 34 -> 17 lines of code! 50% reduction!
// T: Send is required because of `TypedFunc::call_concurrent`'s bounds // 17 lines of code async fn poc_new<T: Send>( accessor: &wasmtime::component::Accessor<T>, func: wasmtime::component::TypedFunc<(i64,), (String,)>, ) { let (tx, rx) = oneshot::async_channel(); accessor .spawn_fn(async move |accessor| { let (result,) = func.call_concurrent(accessor, (32,)).await?; let _ = tx.send(result); Ok(()) }) .unwrap(); let string = rx.await.unwrap(); println!("value: {string}"); } // T: Send is required because of `TypedFunc::call_concurrent`'s bounds // 34 lines of code async fn poc_old<T: Send>( accessor: &wasmtime::component::Accessor<T>, func: wasmtime::component::TypedFunc<(i64,), (String,)>, ) { let (tx, rx) = oneshot::async_channel(); struct Task<T: Send> { func: wasmtime::component::TypedFunc<(i64,), (String,)>, tx: oneshot::Sender<String>, _marker: PhantomData<fn() -> T>, } impl<T: Send> wasmtime::component::AccessorTask<T> for Task<T> where Self: 'static, { async fn run( self, accessor: &wasmtime::component::Accessor<T, wasmtime::component::HasSelf<T>>, ) -> wasmtime::Result<()> { let (result,) = self.func.call_concurrent(accessor, (32,)).await?; let _ = self.tx.send(result); Ok(()) } } accessor .spawn(Task { func, tx, _marker: PhantomData, }) .unwrap(); let string = rx.await.unwrap(); println!("value: {string}"); }Implementation
Behold, the lifetime tricks that even I didn't know was possible
// This is the main trait that we will coerce AsyncFnOnce into, the `run` func matches the // signature on `AccessorTask`, however the lifetime on `&Accessor` is named as `'a`, and a generic lifetime // `'fut` is on the trait which is constrained to be the same as `'a` #[doc(hidden)] pub trait AsyncFnOnceAccessorTask<'fut, T, D: wasmtime::component::HasData> { fn run<'a>( self, accessor: &'a wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send + 'a where // Tell rust that 'fut is same as 'a, so we can use // for<'fut> T: AsyncFnOnceAccessorTask<'fut> later to model AccessorTask's lifetime // on the `run` function 'fut: 'a, 'a: 'fut; } // wrapper needed for orphan rule #[repr(transparent)] struct AsyncFnOnceAccessorTaskWrapper<F>(F); // Implement `AccessorTask` on our wrapper so we can spawn it through `Accessor::spawn` impl<F, T, D> wasmtime::component::AccessorTask<T, D> for AsyncFnOnceAccessorTaskWrapper<F> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, D: wasmtime::component::HasData, { fn run( self, accessor: &wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send { (self.0).run(accessor) } } // Blanket implementation for coercing // `impl for<'a> FnOnce(&'a wasmtime::component::Accessor) -> impl Future<Output = wasmtime::Result<()> + Send + 'a` // into our trait, without `impl_trait_in_fn_trait_return` or `return_type_notation` impl<'fut, F, Fut, T, D> AsyncFnOnceAccessorTask<'fut, T, D> for F where T: 'static, F: FnOnce(&'fut wasmtime::component::Accessor<T, D>) -> Fut, Fut: Future<Output = wasmtime::Result<()>> + Send + 'fut, D: wasmtime::component::HasData, { fn run<'a>( self, accessor: &'a wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send + 'a where 'fut: 'a, 'a: 'fut, { (self)(accessor) } } // Extension trait on Accessor for spawning AccessorTask from async closures pub trait AccessorExt<T, D: wasmtime::component::HasData> { fn spawn_fn<F>(&self, f: F) -> wasmtime::Result<wasmtime::component::JoinHandle> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>; } impl<T, D> AccessorExt<T, D> for wasmtime::component::Accessor<T, D> where D: wasmtime::component::HasData, { fn spawn_fn<F>(&self, f: F) -> wasmtime::Result<wasmtime::component::JoinHandle> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, // Technically, the below is redundant since the blanket implementation of AsyncFnOnceAccessorTask // is the same as the bounds below. However putting it here helps rust to infer F is supposed // to be a closure properly, and allows us to skip spelling out closure params and return type. // Note that the bounds below do not constraint the unnamed `Future` type to be `Send`, the // `Send` bound enforced within AsyncFnOnceAccessorTask F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>, { self.spawn(AsyncFnOnceAccessorTaskWrapper(f)) } } // Make an accessor task from a closure, utility function, not used in this POC pub fn accessor_task<F, T, D>(f: F) -> impl wasmtime::component::AccessorTask<T, D> + 'static where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>, D: wasmtime::component::HasData, { AsyncFnOnceAccessorTaskWrapper(f) }
SilverMira edited issue #14366:
Feature
Currently, spawning a task into an
Accessor's event loop is very boilerplate heavy due to theAccessorTasksignature needing to create aFuturewith lifetime constrained to&Accessor<T>. After suffering to write boilerplate for spawning tasks over and over again. I decided to dive deeper into making the ideal API withAsyncFnOncepossible, and I think I managed to do it.I have included the code snippet that I managed to make
AsyncFnOncespawnable directly onAccessor::spawnvia an extension trait. No unstable features were used, and no boxing ofFuture. I think MSRV shouldn't be affected, I did not check the actual MSRV but the only relatively new feature we are using here should beAsyncFnOnce, this is tested on nightly rust1.100.0. Since I'm on this version, I did also verified that this works on both the nightly trait solver, and the stable one viaRUSTFLAGS=-Znext-solver=coherenceI did not create a PR yet, since I think someone has to decide whether this kind of hack(?) is appropriate or not, and whether the public API should be changed, or do we leave
Accessor::spawnas is and instead have a companionAccessor::spawn_fnBenefit
No boilerplate! 34 -> 17 lines of code! 50% reduction!
// T: Send is required because of `TypedFunc::call_concurrent`'s bounds // 17 lines of code async fn poc_new<T: Send>( accessor: &wasmtime::component::Accessor<T>, func: wasmtime::component::TypedFunc<(i64,), (String,)>, ) { let (tx, rx) = oneshot::async_channel(); accessor .spawn_fn(async move |accessor| { let (result,) = func.call_concurrent(accessor, (32,)).await?; let _ = tx.send(result); Ok(()) }) .unwrap(); let string = rx.await.unwrap(); println!("value: {string}"); } // T: Send is required because of `TypedFunc::call_concurrent`'s bounds // 34 lines of code async fn poc_old<T: Send>( accessor: &wasmtime::component::Accessor<T>, func: wasmtime::component::TypedFunc<(i64,), (String,)>, ) { let (tx, rx) = oneshot::async_channel(); struct Task<T: Send> { func: wasmtime::component::TypedFunc<(i64,), (String,)>, tx: oneshot::Sender<String>, _marker: PhantomData<fn() -> T>, } impl<T: Send> wasmtime::component::AccessorTask<T> for Task<T> where Self: 'static, { async fn run( self, accessor: &wasmtime::component::Accessor<T, wasmtime::component::HasSelf<T>>, ) -> wasmtime::Result<()> { let (result,) = self.func.call_concurrent(accessor, (32,)).await?; let _ = self.tx.send(result); Ok(()) } } accessor .spawn(Task { func, tx, _marker: PhantomData, }) .unwrap(); let string = rx.await.unwrap(); println!("value: {string}"); }Implementation
Behold, the lifetime tricks that even I didn't know was possible
// This is the main trait that we will coerce AsyncFnOnce into, the `run` func matches the // signature on `AccessorTask`, however the lifetime on `&Accessor` is named as `'a`, and a generic lifetime // `'fut` is on the trait which is constrained to be the same as `'a` #[doc(hidden)] pub trait AsyncFnOnceAccessorTask<'fut, T, D: wasmtime::component::HasData> { fn run<'a>( self, accessor: &'a wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send + 'a where // Tell rust that 'fut is same as 'a, so we can use // for<'fut> T: AsyncFnOnceAccessorTask<'fut> later to model AccessorTask's lifetime // on the `run` function 'fut: 'a, 'a: 'fut; } // wrapper needed for orphan rule #[repr(transparent)] struct AsyncFnOnceAccessorTaskWrapper<F>(F); // Implement `AccessorTask` on our wrapper so we can spawn it through `Accessor::spawn` impl<F, T, D> wasmtime::component::AccessorTask<T, D> for AsyncFnOnceAccessorTaskWrapper<F> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, D: wasmtime::component::HasData, { fn run( self, accessor: &wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send { (self.0).run(accessor) } } // Blanket implementation for coercing // `impl for<'a> FnOnce(&'a wasmtime::component::Accessor) -> impl Future<Output = wasmtime::Result<()> + Send + 'a` // into our trait, without `impl_trait_in_fn_trait_return` or `return_type_notation` impl<'fut, F, Fut, T, D> AsyncFnOnceAccessorTask<'fut, T, D> for F where T: 'static, F: FnOnce(&'fut wasmtime::component::Accessor<T, D>) -> Fut, Fut: Future<Output = wasmtime::Result<()>> + Send + 'fut, D: wasmtime::component::HasData, { fn run<'a>( self, accessor: &'a wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send + 'a where 'fut: 'a, 'a: 'fut, { (self)(accessor) } } // Extension trait on Accessor for spawning AccessorTask from async closures pub trait AccessorExt<T, D: wasmtime::component::HasData> { fn spawn_fn<F>(&self, f: F) -> wasmtime::Result<wasmtime::component::JoinHandle> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>; } impl<T, D> AccessorExt<T, D> for wasmtime::component::Accessor<T, D> where D: wasmtime::component::HasData, { fn spawn_fn<F>(&self, f: F) -> wasmtime::Result<wasmtime::component::JoinHandle> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, // Technically, the below is redundant since the blanket implementation of AsyncFnOnceAccessorTask // is the same as the bounds below. However putting it here helps rust to infer F is supposed // to be a closure properly, and allows us to skip spelling out closure params and return type. // Note that the bounds below do not constraint the unnamed `Future` type to be `Send`, the // `Send` bound enforced within AsyncFnOnceAccessorTask F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>, { self.spawn(AsyncFnOnceAccessorTaskWrapper(f)) } } // Make an accessor task from a closure, utility function, not used in this POC pub fn accessor_task<F, T, D>(f: F) -> impl wasmtime::component::AccessorTask<T, D> + 'static where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>, D: wasmtime::component::HasData, { AsyncFnOnceAccessorTaskWrapper(f) }
SilverMira edited issue #14366:
Feature
Currently, spawning a task into an
Accessor's event loop is very boilerplate heavy due to theAccessorTasksignature needing to create aFuturewith lifetime constrained to&Accessor<T>. After suffering to write boilerplate for spawning tasks over and over again. I decided to dive deeper into making the ideal API withAsyncFnOncepossible, and I think I managed to do it.I have included the code snippet that I managed to make
AsyncFnOncespawnable directly onAccessor::spawnvia an extension trait. No unstable features were used, and no boxing ofFuture. I think MSRV shouldn't be affected, I did not check the actual MSRV but the only relatively new feature we are using here should beAsyncFnOnce, this is tested on nightly rust1.100.0. Since I'm on this version, I did also verified that this works on both the nightly trait solver, and the stable one viaRUSTFLAGS=-Znext-solver=coherenceI did not create a PR yet, since I think someone has to decide whether this kind of hack(?) is appropriate or not, and whether the public API should be changed, or do we leave
Accessor::spawnas is and instead have a companionAccessor::spawn_fn. And generally want to see whether simplification of all the jargon needed to get it work is possible.Benefit
No boilerplate! 34 -> 17 lines of code! 50% reduction!
// T: Send is required because of `TypedFunc::call_concurrent`'s bounds // 17 lines of code async fn poc_new<T: Send>( accessor: &wasmtime::component::Accessor<T>, func: wasmtime::component::TypedFunc<(i64,), (String,)>, ) { let (tx, rx) = oneshot::async_channel(); accessor .spawn_fn(async move |accessor| { let (result,) = func.call_concurrent(accessor, (32,)).await?; let _ = tx.send(result); Ok(()) }) .unwrap(); let string = rx.await.unwrap(); println!("value: {string}"); } // T: Send is required because of `TypedFunc::call_concurrent`'s bounds // 34 lines of code async fn poc_old<T: Send>( accessor: &wasmtime::component::Accessor<T>, func: wasmtime::component::TypedFunc<(i64,), (String,)>, ) { let (tx, rx) = oneshot::async_channel(); struct Task<T: Send> { func: wasmtime::component::TypedFunc<(i64,), (String,)>, tx: oneshot::Sender<String>, _marker: PhantomData<fn() -> T>, } impl<T: Send> wasmtime::component::AccessorTask<T> for Task<T> where Self: 'static, { async fn run( self, accessor: &wasmtime::component::Accessor<T, wasmtime::component::HasSelf<T>>, ) -> wasmtime::Result<()> { let (result,) = self.func.call_concurrent(accessor, (32,)).await?; let _ = self.tx.send(result); Ok(()) } } accessor .spawn(Task { func, tx, _marker: PhantomData, }) .unwrap(); let string = rx.await.unwrap(); println!("value: {string}"); }Implementation
Behold, the lifetime tricks that even I didn't know was possible
// This is the main trait that we will coerce AsyncFnOnce into, the `run` func matches the // signature on `AccessorTask`, however the lifetime on `&Accessor` is named as `'a`, and a generic lifetime // `'fut` is on the trait which is constrained to be the same as `'a` #[doc(hidden)] pub trait AsyncFnOnceAccessorTask<'fut, T, D: wasmtime::component::HasData> { fn run<'a>( self, accessor: &'a wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send + 'a where // Tell rust that 'fut is same as 'a, so we can use // for<'fut> T: AsyncFnOnceAccessorTask<'fut> later to model AccessorTask's lifetime // on the `run` function 'fut: 'a, 'a: 'fut; } // wrapper needed for orphan rule #[repr(transparent)] struct AsyncFnOnceAccessorTaskWrapper<F>(F); // Implement `AccessorTask` on our wrapper so we can spawn it through `Accessor::spawn` impl<F, T, D> wasmtime::component::AccessorTask<T, D> for AsyncFnOnceAccessorTaskWrapper<F> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, D: wasmtime::component::HasData, { fn run( self, accessor: &wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send { (self.0).run(accessor) } } // Blanket implementation for coercing // `impl for<'a> FnOnce(&'a wasmtime::component::Accessor) -> impl Future<Output = wasmtime::Result<()> + Send + 'a` // into our trait, without `impl_trait_in_fn_trait_return` or `return_type_notation` impl<'fut, F, Fut, T, D> AsyncFnOnceAccessorTask<'fut, T, D> for F where T: 'static, F: FnOnce(&'fut wasmtime::component::Accessor<T, D>) -> Fut, Fut: Future<Output = wasmtime::Result<()>> + Send + 'fut, D: wasmtime::component::HasData, { fn run<'a>( self, accessor: &'a wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send + 'a where 'fut: 'a, 'a: 'fut, { (self)(accessor) } } // Extension trait on Accessor for spawning AccessorTask from async closures pub trait AccessorExt<T, D: wasmtime::component::HasData> { fn spawn_fn<F>(&self, f: F) -> wasmtime::Result<wasmtime::component::JoinHandle> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>; } impl<T, D> AccessorExt<T, D> for wasmtime::component::Accessor<T, D> where D: wasmtime::component::HasData, { fn spawn_fn<F>(&self, f: F) -> wasmtime::Result<wasmtime::component::JoinHandle> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, // Technically, the below is redundant since the blanket implementation of AsyncFnOnceAccessorTask // is the same as the bounds below. However putting it here helps rust to infer F is supposed // to be a closure properly, and allows us to skip spelling out closure params and return type. // Note that the bounds below do not constraint the unnamed `Future` type to be `Send`, the // `Send` bound enforced within AsyncFnOnceAccessorTask F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>, { self.spawn(AsyncFnOnceAccessorTaskWrapper(f)) } } // Make an accessor task from a closure, utility function, not used in this POC pub fn accessor_task<F, T, D>(f: F) -> impl wasmtime::component::AccessorTask<T, D> + 'static where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>, D: wasmtime::component::HasData, { AsyncFnOnceAccessorTaskWrapper(f) }
alexcrichton commented on issue #14366:
Whoa, this is awesome! I had no idea this was possible...
In the abstract, yes
AccessorTaskis horribly unergonomic and not intended to be a long-term solution, it's a workaround for the (hopefully temporary) state where we can't add bounds to the future returned byAsyncFnOnce(). Basically all we need is to say that the future returned has theSendbound (and maybe'staticI forget), but we're unable to do that with the currentAsyncFnOnce()syntax.I'll be honest I've no idea how what you've got here works. I've never seen a bound like
'fut: 'a, 'a: 'futbefore, but hey if rustc is willing to accept it then I'd say no need to understand it too too deeply. Assuming it works, I'd say the preexistingspawnAPI could be replaced entirely with what you've sketched out here. We don't have much usage in-repo of this API, but some minor usage incrates/wasi-tlswhich could vet this change a bit. If this works for your embedding, and works for this repo, then I'd say ship it :+1:
alexcrichton added the wasm-proposal:component-model-async label to Issue #14366.
SilverMira commented on issue #14366:
Turns out it has nothing to do with
'fut: 'a, 'a: 'fut, the changes within the PR are much simpler than what's going on in this initial POC, the main lifting pieces here seem to be:
- The generic lifetime on the trait, which goes into the
runfunction- The blanket implementation properly enforcing
Sendand&Accessor's lifetime on the returnedFutureviaFnOnce- The trait having a super trait on
AsyncFnOnce, so Rust can properly infer thatimpl Traitshould be an async closure.- Use the trait's
runfunction to get aFuturethat is properly bound to the desired lifetime andSendEverywhere I looked seem to say having
Sendbound onAsyncFnOnceis not possible in stable rust, might have accidentally discovered stable workaround forAsyncFnOnce::CallOnceFuture: Send:thinking:
alexcrichton closed issue #14366:
Feature
Currently, spawning a task into an
Accessor's event loop is very boilerplate heavy due to theAccessorTasksignature needing to create aFuturewith lifetime constrained to&Accessor<T>. After suffering to write boilerplate for spawning tasks over and over again. I decided to dive deeper into making the ideal API withAsyncFnOncepossible, and I think I managed to do it.I have included the code snippet that I managed to make
AsyncFnOncespawnable directly onAccessor::spawnvia an extension trait. No unstable features were used, and no boxing ofFuture. I think MSRV shouldn't be affected, I did not check the actual MSRV but the only relatively new feature we are using here should beAsyncFnOnce, this is tested on nightly rust1.100.0. Since I'm on this version, I did also verified that this works on both the nightly trait solver, and the stable one viaRUSTFLAGS=-Znext-solver=coherenceI did not create a PR yet, since I think someone has to decide whether this kind of hack(?) is appropriate or not, and whether the public API should be changed, or do we leave
Accessor::spawnas is and instead have a companionAccessor::spawn_fn. And generally want to see whether simplification of all the jargon needed to get it work is possible.Benefit
No boilerplate! 34 -> 17 lines of code! 50% reduction!
// T: Send is required because of `TypedFunc::call_concurrent`'s bounds // 17 lines of code async fn poc_new<T: Send>( accessor: &wasmtime::component::Accessor<T>, func: wasmtime::component::TypedFunc<(i64,), (String,)>, ) { let (tx, rx) = oneshot::async_channel(); accessor .spawn_fn(async move |accessor| { let (result,) = func.call_concurrent(accessor, (32,)).await?; let _ = tx.send(result); Ok(()) }) .unwrap(); let string = rx.await.unwrap(); println!("value: {string}"); } // T: Send is required because of `TypedFunc::call_concurrent`'s bounds // 34 lines of code async fn poc_old<T: Send>( accessor: &wasmtime::component::Accessor<T>, func: wasmtime::component::TypedFunc<(i64,), (String,)>, ) { let (tx, rx) = oneshot::async_channel(); struct Task<T: Send> { func: wasmtime::component::TypedFunc<(i64,), (String,)>, tx: oneshot::Sender<String>, _marker: PhantomData<fn() -> T>, } impl<T: Send> wasmtime::component::AccessorTask<T> for Task<T> where Self: 'static, { async fn run( self, accessor: &wasmtime::component::Accessor<T, wasmtime::component::HasSelf<T>>, ) -> wasmtime::Result<()> { let (result,) = self.func.call_concurrent(accessor, (32,)).await?; let _ = self.tx.send(result); Ok(()) } } accessor .spawn(Task { func, tx, _marker: PhantomData, }) .unwrap(); let string = rx.await.unwrap(); println!("value: {string}"); }Implementation
Behold, the lifetime tricks that even I didn't know was possible
// This is the main trait that we will coerce AsyncFnOnce into, the `run` func matches the // signature on `AccessorTask`, however the lifetime on `&Accessor` is named as `'a`, and a generic lifetime // `'fut` is on the trait which is constrained to be the same as `'a` #[doc(hidden)] pub trait AsyncFnOnceAccessorTask<'fut, T, D: wasmtime::component::HasData> { fn run<'a>( self, accessor: &'a wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send + 'a where // Tell rust that 'fut is same as 'a, so we can use // for<'fut> T: AsyncFnOnceAccessorTask<'fut> later to model AccessorTask's lifetime // on the `run` function 'fut: 'a, 'a: 'fut; } // wrapper needed for orphan rule #[repr(transparent)] struct AsyncFnOnceAccessorTaskWrapper<F>(F); // Implement `AccessorTask` on our wrapper so we can spawn it through `Accessor::spawn` impl<F, T, D> wasmtime::component::AccessorTask<T, D> for AsyncFnOnceAccessorTaskWrapper<F> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, D: wasmtime::component::HasData, { fn run( self, accessor: &wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send { (self.0).run(accessor) } } // Blanket implementation for coercing // `impl for<'a> FnOnce(&'a wasmtime::component::Accessor) -> impl Future<Output = wasmtime::Result<()> + Send + 'a` // into our trait, without `impl_trait_in_fn_trait_return` or `return_type_notation` impl<'fut, F, Fut, T, D> AsyncFnOnceAccessorTask<'fut, T, D> for F where T: 'static, F: FnOnce(&'fut wasmtime::component::Accessor<T, D>) -> Fut, Fut: Future<Output = wasmtime::Result<()>> + Send + 'fut, D: wasmtime::component::HasData, { fn run<'a>( self, accessor: &'a wasmtime::component::Accessor<T, D>, ) -> impl Future<Output = wasmtime::Result<()>> + Send + 'a where 'fut: 'a, 'a: 'fut, { (self)(accessor) } } // Extension trait on Accessor for spawning AccessorTask from async closures pub trait AccessorExt<T, D: wasmtime::component::HasData> { fn spawn_fn<F>(&self, f: F) -> wasmtime::Result<wasmtime::component::JoinHandle> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>; } impl<T, D> AccessorExt<T, D> for wasmtime::component::Accessor<T, D> where D: wasmtime::component::HasData, { fn spawn_fn<F>(&self, f: F) -> wasmtime::Result<wasmtime::component::JoinHandle> where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, // Technically, the below is redundant since the blanket implementation of AsyncFnOnceAccessorTask // is the same as the bounds below. However putting it here helps rust to infer F is supposed // to be a closure properly, and allows us to skip spelling out closure params and return type. // Note that the bounds below do not constraint the unnamed `Future` type to be `Send`, the // `Send` bound enforced within AsyncFnOnceAccessorTask F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>, { self.spawn(AsyncFnOnceAccessorTaskWrapper(f)) } } // Make an accessor task from a closure, utility function, not used in this POC pub fn accessor_task<F, T, D>(f: F) -> impl wasmtime::component::AccessorTask<T, D> + 'static where for<'fut> F: AsyncFnOnceAccessorTask<'fut, T, D> + Send + 'static, F: AsyncFnOnce(&wasmtime::component::Accessor<T>) -> wasmtime::Result<()>, D: wasmtime::component::HasData, { AsyncFnOnceAccessorTaskWrapper(f) }
alexcrichton commented on issue #14366:
Landed in https://github.com/bytecodealliance/wasmtime/pull/14376 (thanks!)
Last updated: Oct 11 2026 at 02:20 UTC