这是一个稳妥的方法,用来检查AsyncFnMut的实现是否返回实现了Send的 futures?

后端开发 2026-07-11

这里有一个宏,试图验证一个异步闭包总是返回 Send 的futures,同时避免 [HRTBs],以免被这个问题困扰——它使得只能对 'static 闭包做到这一点:

/// Let `f` be a result of a call to `certify_thread_safe!`. All calls made to
/// `f` (after it is returned) will result in futures that are `Send`.
#[macro_export]
macro_rules! certify_thread_safe {
    ($closure:expr) => {{
        let f = $closure;

        // Check that the property holds for one lifetime that the caller can't
        // name. This is sufficient to show it holds for all lifetimes after we
        // return.
        $crate::__internal::check_thread_safe(&f);

        f
    }};
}

#[doc(hidden)]
pub mod __internal {
    use std::marker::Tuple;

    /// Check that the supplied closure returns a `Send` future when called with
    /// a single lifetime for borrowing `self`.
    pub fn check_thread_safe<'a, Args, F>(_: &'a F)
    where
        Args: Tuple,
        F: 'a,
        F: AsyncFnMut<Args>,
        F::CallRefFuture<'a>: Send,
    {
    }
}

(Playground)

这个实现正确吗?也就是说,是否存在某种反例,使得该宏编译通过,但它求值得到的值可能被调用以获取一个非 Send 的future?

我预计如果存在反例,可能恰好涉及对 AsyncFnMut 的手写实现。因此,我也对一个较弱的问题感兴趣,即这在实际的语法闭包(也就是由编译器生成的 AsyncFnMut 实现)上是否也成立。


为了记录,这个想法是将这个宏并入一个更大的宏中,使用一个不安全的API将这样的闭包包装成一个实现自定义 AsyncFnMut-like trait的类型,该trait总是保证结果future的 Send。在我需要线程安全的闭包、但又希望支持非 'static 闭包的场景中,这个trait可以被接受使用。

解决方案

据我所知,这种实现并不可靠,因为 AsyncFnMut 的实现可能对生命周期是泛型的,而对 check_thread_safe 的调用可能会推断出与对闭包的调用不同的生命周期。这种情况即便在“真正的”闭包中也成立,不只是出现在对 AsyncFnMut 的手动实现中。

例如,设想下面这个结构体,它只在生命周期 'static 内部对 Send 有效:

struct S<'a>(&'a String, *const ());
unsafe impl Send for S<'static> {}

异步闭包的future类型包含了它的参数,因此一个接收其中一个参数的闭包只有在 S 的生命周期参数为 'static 时才会是 Send。但如果我们使用一个占位生命周期,编译器很乐意让它同时满足两者——它可以在 certify_thread_safe! 内被推断为 'static,而在调用闭包时又是一个更短的生命周期。下列代码被接受:

fn main() {
    let f = certify_thread_safe!(async |_: S<'_>| todo!());

    let s = String::from("taco");
    let future = f(S(&s, std::ptr::null()));
}

(Playground)

我们可以通过断言它是 Send 来再次检查该未来是否不是 Send,并看到编译现在 [失败],因为唯一让这种情况成立的方式,是字符串具有静态生命周期:

error[E0597]: `s` does not live long enough
  --> src/main.rs:15:22
   |
14 |     let s = String::from("taco");
   |         - binding `s` declared here
15 |     let future = f(S(&s, std::ptr::null()));
   |                      ^^ borrowed value does not live long enough
...
18 |     check_send(&future);
   |     ------------------- argument requires that `s` is borrowed for `'static`
19 | }
   | - `s` dropped here while still borrowed
   |
note: requirement that the value outlives `'static` introduced here
  --> src/main.rs:17:22
   |
17 |     fn check_send<T: Send>(t: &T) {}
   |                      ^^^^

花了一段时间才理解这一个反例的工作原理,因为我本以为闭包的 S<'_> 参数中的占位生命周期会被推断为与 AsyncFnMut::async_call_mut&mut self 参数的生命周期相同。但正如在 [这一帖子] 中所讨论的,那并非如此。

如果把这个闭包(或者说一个更通用的版本—带有需要借用的捕获)手写出来,可能会更清晰一些:

struct SomeClosure;
struct FnMutFuture<'c, 's>(&'c mut SomeClosure, S<'s>);

impl<'s> AsyncFnMut<(S<'s>,)> for SomeClosure {
    type CallRefFuture<'c> = FnMutFuture<'c, 's>;

    extern "rust-call" fn async_call_mut<'c>(
        &'c mut self,
        args: (S<'s>,),
    ) -> Self::CallRefFuture<'c> {
        FnMutFuture(self, args.0)
    }
}

因为 S 需要一个生命周期参数,AsyncFnMut 的实现本身也需要对一个生命周期 's 进行泛化,以便在我们开始借用 self 之前,表达我们正在实现的 AsyncFnMut trait的名称。这个量词的作用域自然比在trait实现中的 CallRefFuture(类型别名)要宽,而且没有任何要求两者必须相同。

在宏中的对 check_thread_safe 的调用可以自由地将外部生命周期推断为 'static,即使 CallRefFuture 的生命周期会较短;而在我们的 main 函数中对 f 的调用会把两者都推断到 main 的作用域内。只有在引入对 check_send 的调用之后,才会出现矛盾,因为那会要求 main 内的生命周期为 'static。若没有那次调用,一切都能编译通过,但我们得到的是一个包含对非静态生命周期 'aS<'a> 的未来,这并不是 Send

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章