Is it possible to extend the lifetime of any `AsyncFnOnce`?

Allowing myself to use a couple of Nightly features, this is the closest I could get to it:

#![feature(async_fn_traits)]
#![feature(unboxed_closures)]

use core::marker::PhantomData;
use core::mem;
use core::pin::Pin;

struct Boxed<'a, F>(F, PhantomData<&'a ()>);

impl<'a, T, U, F> AsyncFnOnce<(T,)> for Boxed<'a, F>
where
    F: AsyncFnOnce(T) -> U,
    F::CallOnceFuture: 'a,
{
    type CallOnceFuture = Pin<Box<dyn Future<Output = U> + 'a>>;
    type Output = U;

    extern "rust-call" fn async_call_once(
        self,
        args: (T,),
    ) -> Self::CallOnceFuture {
        Box::pin(self.0(args.0))
    }
}

struct TypeErased<'a, T, U>(
    Box<
        dyn AsyncFnOnce<
                (T,),
                CallOnceFuture = Pin<Box<dyn Future<Output = U> + 'a>>,
                Output = U,
            > + 'a,
    >,
);

impl<'a, T, U> AsyncFnOnce<(T,)> for TypeErased<'a, T, U> {
    type CallOnceFuture = Pin<Box<dyn Future<Output = U> + 'a>>;
    type Output = U;

    extern "rust-call" fn async_call_once(
        self,
        args: (T,),
    ) -> Self::CallOnceFuture {
        self.0(args.0)
    }
}

unsafe fn extend_lifetime<T, U>(
    fun: impl AsyncFnOnce(T) -> U,
) -> impl AsyncFnOnce(T) -> U + 'static {
    // SAFETY: up to caller.
    unsafe {
        mem::transmute::<TypeErased<'_, T, U>, TypeErased<'static, T, U>>(
            TypeErased(Box::new(Boxed(fun, PhantomData))),
        )
    }
}

But that doesn't compile due to T and U not being 'static, which doesn't make sense to me.

Why should a callback's input and output have to be 'static for it to be 'static?

Outlive bounds are syntactically defined. So TypeErased<'static, T, U>: 'static only if T: 'static and U: 'static. This is built into the type system, for example when you return an X = impl AsyncFnOnce(T) -> U + 'static, other parts of the type system can assume that X::Output: 'static too.

Outlive bounds are syntactically defined

Oh, well it sucks that the compiler doesn't look into how generics are actually used in the type, but I guess that makes sense.

In the end, I was able to make it work following your advice:

unsafe fn extend_lifetime<T, U>(
    fun: impl AsyncFnOnce(T) -> U,
) -> impl AsyncFnOnce(T) -> U + 'static {
    let fun = async move |args: *mut ()| {
        let args = *unsafe { Box::from_raw(args as *mut T) };
        let out = fun(args);
        Box::into_raw(Box::new(out)) as *mut ()
    };

    // SAFETY: up to caller.
    let fun = unsafe {
        mem::transmute::<TypeErased<'_, *mut (), *mut ()>, TypeErased<'static, *mut (), *mut ()>>(
            TypeErased(Box::new(Boxed(fun, PhantomData))),
        )
    };
    
    async move |args: T| {
        let out = fun(Box::into_raw(Box::new(args)) as *mut ()).await;
        *unsafe { Box::from_raw(out as *mut U) }
    }
}

Is it possible to make this compile on stable or with less unsafe?

when you return an X = impl AsyncFnOnce(T) -> U + 'static , other parts of the type system can assume that X::Output: 'static too

But X::Output is U, correct? If so, that doesn't seem to be right, for example this doesn't compile:

fn make_async_closure<T, U>() -> impl AsyncFnOnce(T) -> U + 'static {
    async |_: T| todo!()
}

async fn foo<T, U>(t: T) {
    is_static(&make_async_closure::<T, U>()(t).await)
}

fn is_static<T: 'static>(_: &T) {}

I'm on mobile right now, but it's probably because the input parameters to the trait have to be 'static too. If I remember I'll poke at it later and try to give an example.

But for a simpler example of how it prevents UB: if you had a type with non-'static lifetimes but it met a 'static bound, you could unsoundly transmute the lifetimes by round-tripping through Box<dyn Any> as TypeId has no way to encode lifetimes. (There are actually some soundness holes in the compiler that allow this or something very similar currently.)

Here's one of the soundness bugs I referred to. The code snippet demonstrates how the compiler infers that T::Associated is 'static due to the implementor being 'static and the trait having no parameters in static_transfers_to_associated, which all works as designed.