Stream: git-wasmtime

Topic: wasmtime / issue #14366 Using closures for Accessor::spawn


view this post on Zulip Wasmtime GitHub notifications bot (Sep 21 2026 at 16:03):

SilverMira opened issue #14366:

Feature

Currently, spawning a task into an Accessor's event loop is very boilerplate heavy due to the AccessorTask signature needing to create a Future with 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 with AsyncFnOnce possible, and I think I managed to do it.

I have included the code snippet that I managed to make AsyncFnOnce spawnable directly on Accessor::spawn via an extension trait. No unstable features were used, and no boxing of Future. 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 be AsyncFnOnce.

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)
}

view this post on Zulip Wasmtime GitHub notifications bot (Sep 21 2026 at 16:04):

SilverMira edited issue #14366:

Feature

Currently, spawning a task into an Accessor's event loop is very boilerplate heavy due to the AccessorTask signature needing to create a Future with 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 with AsyncFnOnce possible, and I think I managed to do it.

I have included the code snippet that I managed to make AsyncFnOnce spawnable directly on Accessor::spawn via an extension trait. No unstable features were used, and no boxing of Future. 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 be AsyncFnOnce.

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_fn

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)
}

view this post on Zulip Wasmtime GitHub notifications bot (Sep 21 2026 at 16:04):

SilverMira edited issue #14366:

Feature

Currently, spawning a task into an Accessor's event loop is very boilerplate heavy due to the AccessorTask signature needing to create a Future with 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 with AsyncFnOnce possible, and I think I managed to do it.

I have included the code snippet that I managed to make AsyncFnOnce spawnable directly on Accessor::spawn via an extension trait. No unstable features were used, and no boxing of Future. 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 be AsyncFnOnce.

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_fn

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)
}

view this post on Zulip Wasmtime GitHub notifications bot (Sep 21 2026 at 16:06):

SilverMira edited issue #14366:

Feature

Currently, spawning a task into an Accessor's event loop is very boilerplate heavy due to the AccessorTask signature needing to create a Future with 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 with AsyncFnOnce possible, and I think I managed to do it.

I have included the code snippet that I managed to make AsyncFnOnce spawnable directly on Accessor::spawn via an extension trait. No unstable features were used, and no boxing of Future. 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 be AsyncFnOnce, this is tested on nightly rust 1.100.0

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_fn

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)
}

view this post on Zulip Wasmtime GitHub notifications bot (Sep 21 2026 at 16:31):

SilverMira edited issue #14366:

Feature

Currently, spawning a task into an Accessor's event loop is very boilerplate heavy due to the AccessorTask signature needing to create a Future with 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 with AsyncFnOnce possible, and I think I managed to do it.

I have included the code snippet that I managed to make AsyncFnOnce spawnable directly on Accessor::spawn via an extension trait. No unstable features were used, and no boxing of Future. 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 be AsyncFnOnce, this is tested on nightly rust 1.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 via RUSTFLAGS=-Znext-solver=coherence

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_fn

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)
}

view this post on Zulip Wasmtime GitHub notifications bot (Sep 21 2026 at 16:43):

SilverMira edited issue #14366:

Feature

Currently, spawning a task into an Accessor's event loop is very boilerplate heavy due to the AccessorTask signature needing to create a Future with 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 with AsyncFnOnce possible, and I think I managed to do it.

I have included the code snippet that I managed to make AsyncFnOnce spawnable directly on Accessor::spawn via an extension trait. No unstable features were used, and no boxing of Future. 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 be AsyncFnOnce, this is tested on nightly rust 1.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 via RUSTFLAGS=-Znext-solver=coherence

I 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::spawn as is and instead have a companion Accessor::spawn_fn

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)
}

view this post on Zulip Wasmtime GitHub notifications bot (Sep 21 2026 at 16:44):

SilverMira edited issue #14366:

Feature

Currently, spawning a task into an Accessor's event loop is very boilerplate heavy due to the AccessorTask signature needing to create a Future with 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 with AsyncFnOnce possible, and I think I managed to do it.

I have included the code snippet that I managed to make AsyncFnOnce spawnable directly on Accessor::spawn via an extension trait. No unstable features were used, and no boxing of Future. 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 be AsyncFnOnce, this is tested on nightly rust 1.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 via RUSTFLAGS=-Znext-solver=coherence

I 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::spawn as is and instead have a companion Accessor::spawn_fn

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)
}

view this post on Zulip Wasmtime GitHub notifications bot (Sep 21 2026 at 18:03):

SilverMira edited issue #14366:

Feature

Currently, spawning a task into an Accessor's event loop is very boilerplate heavy due to the AccessorTask signature needing to create a Future with 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 with AsyncFnOnce possible, and I think I managed to do it.

I have included the code snippet that I managed to make AsyncFnOnce spawnable directly on Accessor::spawn via an extension trait. No unstable features were used, and no boxing of Future. 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 be AsyncFnOnce, this is tested on nightly rust 1.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 via RUSTFLAGS=-Znext-solver=coherence

I 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::spawn as is and instead have a companion Accessor::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)
}

view this post on Zulip Wasmtime GitHub notifications bot (Sep 21 2026 at 18:31):

alexcrichton commented on issue #14366:

Whoa, this is awesome! I had no idea this was possible...

In the abstract, yes AccessorTask is 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 by AsyncFnOnce(). Basically all we need is to say that the future returned has the Send bound (and maybe 'static I forget), but we're unable to do that with the current AsyncFnOnce() 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: 'fut before, 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 preexisting spawn API 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 in crates/wasi-tls which could vet this change a bit. If this works for your embedding, and works for this repo, then I'd say ship it :+1:

view this post on Zulip Wasmtime GitHub notifications bot (Sep 21 2026 at 18:31):

alexcrichton added the wasm-proposal:component-model-async label to Issue #14366.

view this post on Zulip Wasmtime GitHub notifications bot (Sep 22 2026 at 15:05):

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:

  1. The generic lifetime on the trait, which goes into the run function
  2. The blanket implementation properly enforcing Send and &Accessor's lifetime on the returned Future via FnOnce
  3. The trait having a super trait on AsyncFnOnce, so Rust can properly infer that impl Trait should be an async closure.
  4. Use the trait's run function to get a Future that is properly bound to the desired lifetime and Send

Everywhere I looked seem to say having Send bound on AsyncFnOnce is not possible in stable rust, might have accidentally discovered stable workaround for AsyncFnOnce::CallOnceFuture: Send :thinking:

view this post on Zulip Wasmtime GitHub notifications bot (Sep 22 2026 at 18:59):

alexcrichton closed issue #14366:

Feature

Currently, spawning a task into an Accessor's event loop is very boilerplate heavy due to the AccessorTask signature needing to create a Future with 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 with AsyncFnOnce possible, and I think I managed to do it.

I have included the code snippet that I managed to make AsyncFnOnce spawnable directly on Accessor::spawn via an extension trait. No unstable features were used, and no boxing of Future. 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 be AsyncFnOnce, this is tested on nightly rust 1.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 via RUSTFLAGS=-Znext-solver=coherence

I 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::spawn as is and instead have a companion Accessor::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)
}

view this post on Zulip Wasmtime GitHub notifications bot (Sep 22 2026 at 18:59):

alexcrichton commented on issue #14366:

Landed in https://github.com/bytecodealliance/wasmtime/pull/14376 (thanks!)


Last updated: Oct 11 2026 at 02:20 UTC