Rust in Everything
基于 embassy-executor-macros 0.8.0

#[task] 的报错:非 'static、泛型、impl Trait、17 个参数

主线位置:定时器主线的第一步,#[embassy_executor::task] 把 async fn 展开成一个静态池和一个返回令牌的函数。展开后的代码和宏的全部检查见宏;本页按报错列改法。

flowchart TB
  sig["任务签名"] --> chk["宏 run 检查"]
  chk -->|泛型| e1["must not be generic"]
  chk -->|引用 生命周期| e2["must live forever"]
  chk -->|impl Trait| e3["impl Trait not allowed"]
  chk -->|通过| ty["类型检查"]
  ty -->|"Led 省略生命周期"| e4["E0726"]
  ty -->|17 个参数| e5["E0277 resolve to ()"]

实验 crate exp-embassy/pita-std,bin mac_nonstatic、mac_generic、mac_impl、mac_elided、mac_args17、mac_fixed(platform-std,debug);M2 Max、macOS 27、rustc 1.99.0。下面的报错去掉了随后的 unused variable 警告。

现象

借来的参数。

#[embassy_executor::task]
async fn log_line(prefix: &str, buf: &mut [u8; 64]) { /* ... */ }
error: Arguments for tasks must live forever. Try using the `'static` lifetime.
 --> src/bin/mac_nonstatic.rs:4:27
  |
4 | async fn log_line(prefix: &str, buf: &mut [u8; 64]) {
  |                           ^

error: Arguments for tasks must live forever. Try using the `'static` lifetime.
 --> src/bin/mac_nonstatic.rs:4:38

泛型和 impl Trait。 想写一个对任意传感器都能用的轮询任务:

error: task functions must not be generic
 --> src/bin/mac_generic.rs:8:1
  |
8 | async fn poll_sensor<S: Sensor + 'static>(mut s: S) {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: `impl Trait` is not allowed in task arguments. It is syntax sugar for generics, and tasks can't be generic.
 --> src/bin/mac_impl.rs:8:29
  |
8 | async fn poll_sensor(mut s: impl Sensor + 'static) {
  |                             ^^^^^^^^^^^^^^^^^^^^^

带生命周期的驱动类型。 embassy 的驱动类型几乎都带生命周期参数(Output<'d>、Uart<'d, M>)。用一个 struct Led<'d> 模拟,两种写法报的错完全不同:

#[embassy_executor::task]
async fn blink(_led: Led<'_>) {}

#[embassy_executor::task]
async fn blink2(_led: Led) {}
error: Arguments for tasks must live forever. Try using the `'static` lifetime.
 --> src/bin/mac_elided.rs:9:26
  |
9 | async fn blink(_led: Led<'_>) {}
  |                          ^^

error[E0726]: implicit elided lifetime not allowed here
  --> src/bin/mac_elided.rs:12:23
   |
12 | async fn blink2(_led: Led) {}
   |                       ^^^ expected lifetime parameter
...
error: lifetime may not live long enough
  --> src/bin/mac_elided.rs:11:1
   |
11 | #[embassy_executor::task]
   | ^^^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'static`
12 | async fn blink2(_led: Led) {}
   |                 ---- has type `Led<'1>`
   |
   = note: this error originates in the attribute macro `embassy_executor::task` (in Nightly builds, run with -Z macro-backtrace for more info)
help: to declare that `impl Sized` captures data from argument `_led`, you can add an explicit `'_` lifetime bound
   |
11 | #[embassy_executor::task] + '_
   |                           ++++

Led<'_> 被宏抓住,给出清楚的提示;Led 什么也没写,宏看不见,错误落到 rustc 的借用检查上,最后那条 help 建议在属性后面写 + '_,不是合法的代码。

17 个参数。 一个配置项一个参数,写到第 17 个:

error[E0277]: task futures must resolve to `()` or `!`
   --> src/bin/mac_args17.rs:4:10
    |
  3 | #[embassy_executor::task]
    | ------------------------- required by a bound introduced by this call
  4 | async fn many(a0: u8, a1: u8, a2: u8, a3: u8, a4: u8, a5: u8, a6: u8, a7: u8, a8: u8,
    |          ^^^^ unsatisfied trait bound
    |
    = help: the trait `embassy_executor::_export::TaskFn<_>` is not implemented for fn item `fn(u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8) -> _ {__many_task}`
    = note: use `async fn` or change the return type to `impl Future<Output = ()>`
...
error: could not compile `pita-std` (bin "mac_args17") due to 7 previous errors

七条同样的错误,都在说返回类型,而这个函数明明是 async fn、什么也不返回。

出处

宏在 run 里检查签名,参数类型交给 check_arg_ty 用 syn::visit 遍历:

        fn visit_type_reference(&mut self, i: &'ast syn::TypeReference) {
            // Only check for elided lifetime here. If not elided, it's checked by `visit_lifetime`.
            if i.lifetime.is_none() {
                error(
                    self.errors,
                    i.and_token,
                    "Arguments for tasks must live forever. Try using the `'static` lifetime.",
                )
            }
            visit::visit_type_reference(self, i);
        }

        fn visit_lifetime(&mut self, i: &'ast syn::Lifetime) {
            if i.ident.to_string() != "static" {
                // ... same error
            }
        }

它只能看见源码里写出来的东西:&str 有一个 & 记号,Led<'_> 有一个 '_ 记号;Led 只是一个路径,它的生命周期参数要到类型检查时才知道存在,宏无从下手。

参数为什么必须 'static、函数为什么不能泛型,答案是同一个:任务的 future 存进一个 static POOL,它活到程序结束,借来的东西也得活那么久;池的大小按唯一的 future 类型在编译期算出,泛型会产生多个类型却只有一个池。

17 个参数的错误来自 embassy-executor 的 TaskFn:宏借它从函数项推出 future 类型,task_fn_impl! 只为 0 到 16 个参数实现了它(lib.rs 第 97 到 113 行),而 trait 上唯一的 on_unimplemented 消息是为「返回类型不对」写的。

修法

四种写法,各改一种(bin mac_fixed):

// 1. borrowed arguments: make them 'static (a string literal, a StaticCell)
#[embassy_executor::task]
async fn log_line(prefix: &'static str, buf: &'static mut [u8; 64]) { /* ... */ }

// 2. generic / impl Trait: a trait object behind a 'static reference
#[embassy_executor::task]
async fn poll_sensor(s: &'static mut dyn Sensor) { /* ... */ }

// 3. generic, keeping static dispatch: one small task per concrete type
async fn poll_generic<S: Sensor>(mut s: S) { /* ... */ }
#[embassy_executor::task]
async fn poll_thermo(s: Thermo) {
  poll_generic(s).await
}

// 4. more than 16 arguments: one struct
struct Config {
  pins: [u8; 17],
}
#[embassy_executor::task]
async fn many(cfg: Config) { /* ... */ }

static BUF: StaticCell<[u8; 64]> = StaticCell::new();
static THERMO: StaticCell<Thermo> = StaticCell::new();

// in main:
spawner.spawn(log_line("line", BUF.init([0; 64])).unwrap());
spawner.spawn(poll_sensor(THERMO.init(Thermo(20))).unwrap());
spawner.spawn(poll_thermo(Thermo(30)).unwrap());
spawner.spawn(many(Config { pins: [0; 17] }).unwrap());
pins 17
generic 31
sensor 21
line 1

四个任务都跑了(输出顺序和 spawn 顺序相反,这是运行队列批内后进先出,见状态与运行队列)。

  • &'static mut dyn Sensor 里的 dyn 不是 impl,宏放行;它是一个胖指针,参数在 future 里只占两个字(thumbv7em 上 8 字节),代价是动态分发。
  • 第 3 种保留泛型和静态分发,每个具体类型写一个三行的任务;泛型 async fn 不加 #[task],只被 await。
  • 带生命周期的驱动类型写成 Led<'static> / Output<'static>:外设单例从 embassy_stm32::init 拿到时就是 'static 的(外设单例与 Peri)。

怎么确认没中招

  • 任务参数里的每个生命周期都写出来,并且是 'static;不写生命周期的路径类型(Led、Output)是 rustc 报 E0726 加一条奇怪 help 的原因。
  • 看到 task futures must resolve to () or ! 而函数确实是 async fn 时,先数参数个数。
  • 想让任务「通用」时,在 dyn 和「每个类型一个小任务」之间选:前者省代码,后者省一次间接调用。

小结

  • 参数必须 'static、任务不能泛型,因为 future 住在一个按唯一类型定大小的 static 池里。
  • 宏只检查写出来的记号:&str、Led<'_> 被抓住;Led 漏过去,变成 E0726 和一个不合法的 help。
  • 超过 16 个参数报的是返回类型的 E0277;把参数收进一个结构体。

💬 关联讨论

发起讨论
加载中...