宏
主线位置:定时器主线的第 1 步,
#[embassy_executor::main]展开。用户写的async fn main(spawner: Spawner)和每个#[embassy_executor::task]在编译期变成什么:一个静态任务池、一个返回令牌的函数、一个建执行器的真正的main。运行期的事从任务存储和 Spawner 接着讲。
flowchart TB user["async fn blink"] --> inner["__blink_task"] inner --> innerfn["内层 async fn"] user --> outer["fn blink 返回 Result"] outer --> pool["static POOL"] pool --> holder["TaskPoolHolder"] outer --> get["__task_pool_get"] get --> spawn["_spawn_async_fn"] mainu["async fn main"] --> maint["__embassy_main 任务"] mainu --> realmain["fn main 返回 !"] realmain --> run["Executor::run"]
宏在 embassy-executor-macros 0.8.0(tag embassy-executor-macros-v0.8.0):src/macros/task.rs 299 行,main.rs 240 行,lib.rs 224 行(每个平台一个入口宏)。宏生成的代码调用的辅助类型在 embassy-executor 的 lib.rs 里的 _export 模块,文档写着「Do not use. Used for macros and HALs only. Not covered by semver guarantees.」。实验在 exp-embassy/exec-std(std,release,M2 Max、macOS 27、rustc 1.99.0)。
展开后的代码
本机没有 cargo-expand。稳定版的 rustc 不接受 -Zunpretty,但设置 RUSTC_BOOTSTRAP=1 后可以用:RUSTC_BOOTSTRAP=1 cargo rustc --release --bin expand -- -Zunpretty=expanded。这只用来读代码,不影响别的实验。输入(exec-std/src/bin/expand.rs):
#[embassy_executor::task(pool_size = 2)]
async fn blink(period_ms: u64) {
embassy_time::Timer::after_millis(period_ms).await;
}
#[embassy_executor::main]
async fn main(spawner: Spawner) {
spawner.spawn(blink(100).unwrap());
}
#[task] 的输出(rustc 的排版,原样):
#[doc(hidden)]
fn __blink_task(period_ms: u64) -> impl ::core::future::Future<Output = ()> {
async fn __blink_task_inner_function(period_ms: u64) {
embassy_time::Timer::after_millis(period_ms).await;
}
{ __blink_task_inner_function(period_ms) }
}
fn blink(period_ms: u64)
->
::core::result::Result<::embassy_executor::SpawnToken<impl Sized>,
::embassy_executor::SpawnError> {
const fn __task_pool_get<F, Args, Fut>(_: F)
-> &'static ::embassy_executor::raw::TaskPool<Fut, POOL_SIZE> where
F: ::embassy_executor::_export::TaskFn<Args, Fut = Fut>,
Fut: ::core::future::Future + 'static {
unsafe { &*POOL.get().cast() }
}
const POOL_SIZE: usize = 2;
static POOL:
::embassy_executor::_export::TaskPoolHolder<{
::embassy_executor::_export::task_pool_size::<_, _, _,
POOL_SIZE>(__blink_task)
},
{
::embassy_executor::_export::task_pool_align::<_, _, _,
POOL_SIZE>(__blink_task)
}> =
unsafe {
::core::mem::transmute(::embassy_executor::_export::task_pool_new::<_,
_, _, POOL_SIZE>(__blink_task))
};
unsafe {
__task_pool_get(__blink_task)._spawn_async_fn(move ||
__blink_task(period_ms))
}
}
逐段看:
- 两层函数。 用户的函数体原样搬进
__blink_task_inner_function,外面再包一层__blink_task,返回impl Future<Output = ()>。改名是因为blink这个名字要留给外层;任务函数可能递归地 spawn 自己,run里的注释写着「We have to rename the function since it might be recursive」。 static POOL在外层函数体里。 函数体里的static只有一份,不管函数被调用多少次,所以每个任务函数恰好一个池;放在函数体里还让POOL这个名字不会和用户代码冲突(task.rs的注释:「otherwise the items defined there (such as POOL) would be in scope in the user's code」)。- 池的类型。 稳定版 Rust 不能把
impl Future写进static的类型。宏绕了一下:POOL的类型是TaskPoolHolder<SIZE, ALIGN>,一个[MaybeUninit<u8>; SIZE]加一个零大小、对齐为ALIGN的字段。SIZE和ALIGN由task_pool_size/task_pool_align这两个const fn算:它们接受__blink_task这个函数项的值,从TaskFn的关联类型Fut推出 future 的类型,返回size_of::<TaskPool<Fut, POOL_SIZE>>()。future 的类型始终没有被写出来,只是被推断。 - 初值。
task_pool_new返回一个真正的TaskPool<Fut, N>(const求值,全零),再transmute成同样大小的 holder。初值全零,所以落在.bss。 - 取池。
__task_pool_get把 holder 的地址cast回&'static TaskPool<Fut, POOL_SIZE>,Fut同样从__blink_task推断。 - spawn。 调
_spawn_async_fn,闭包move || __blink_task(period_ms)就是令牌类型参数里的那个FutFn,见 Spawner。
TaskFn 由 task_fn_impl! 为 0 到 16 个参数各实现一遍,trait 上带着一条诊断属性:
#[diagnostic::on_unimplemented(
message = "task futures must resolve to `()` or `!`",
note = "use `async fn` or change the return type to `impl Future<Output = ()>`"
)]
任务函数的 future 输出不是 () 或 ! 时,用户看到的是这条消息而不是一串 trait 约束。开了 nightly feature 时走另一条路:宏生成一个内部 trait,用 type Fut = impl Future<...>(type_alias_impl_trait)直接声明 static POOL: TaskPool<<() as _EmbassyInternalTaskTrait>::Fut, POOL_SIZE>,不需要 holder 和 transmute。
问: 我的任务明明是 async fn、什么也不返回,为什么报「task futures must resolve to () or !」?
答: 看看参数个数。TaskFn 只实现到 16 个参数(task_fn_impl!(T0, ..., T15)),第 17 个参数让函数项不满足 TaskFn,而这个 trait 上唯一的 on_unimplemented 消息是给返回类型写的。exec-std/src/bin/err_args17.rs 定义一个 17 个 u8 参数的空任务:
error[E0277]: task futures must resolve to `()` or `!`
--> src/bin/err_args17.rs:2:10
|
1 | #[embassy_executor::task]
| ------------------------- required by a bound introduced by this call
2 | async fn many(a0: u8, a1: u8, a2: u8, a3: u8, a4: u8, a5: u8, a6: u8, a7: u8, a8: u8, a9: u8, a10: u8, a11: u8, a12: u8, a13: u8, a...
| ^^^^ 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 = ()>`
真正的线索在 help 那一行的参数列表里。把参数收进一个结构体就能编过。这条限制只在稳定版路径上有;nightly 路径不经过 TaskFn。
宏在编译期拒绝什么
task.rs 的 run 在生成代码之前逐条检查签名:不是 async 也不返回 impl Trait;有泛型参数;有 where 子句;有 ABI(extern "C");可变参数;async fn 返回了 () 和 ! 以外的类型;self 参数;参数用了模式((a, b): (u8, u8))。参数类型再交给 check_arg_ty 用 syn::visit 遍历:省略了生命周期的引用、不是 'static 的生命周期、impl Trait,都报错。
三个常见的错误,exec-std 里各一个 bin 复现(rustc 原文,去掉了随后的 unused variable 警告):
error: Arguments for tasks must live forever. Try using the `'static` lifetime.
--> src/bin/err_nonstatic.rs:2:20
|
2 | async fn show(msg: &str) {
| ^
error: task functions must not be generic
--> src/bin/err_generic.rs:2:1
|
2 | async fn show<T: core::fmt::Debug + 'static>(value: T) {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: `impl Trait` is not allowed in task arguments. It is syntax sugar for generics, and tasks can't be generic.
--> src/bin/err_impl.rs:2:22
|
2 | async fn show(value: impl core::fmt::Debug + 'static) {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
为什么不能泛型:一个任务函数只有一个 static POOL,它的大小在编译期由唯一的 future 类型决定;泛型会产生多个 future 类型,却只有一个池。为什么参数必须 'static:future 存进 static 里、活到任务结束,借用的东西必须活得一样久。
出错时宏不会只吐一个错误了事:外层函数体被换成 let _x: Result<SpawnToken<()>, SpawnError> = todo!();,内层换成 async { todo!() },签名照旧。run 开头的注释说这样「helps out IDEs such that completions and other related features keep working」,调用方的代码不会跟着冒出一堆连锁错误。
impl Trait 参数曾经能编过,而且是内存错误impl Trait 参数是泛型的语法糖,但它不出现在函数的泛型参数列表里,generics.params.is_empty() 的检查拦不住它。CHANGELOG 0.6.1 写道:「Soundness fix: Deny using impl Trait in task arguments. This was previously accidentally allowed when not using the nightly feature, and could cause out of bounds memory accesses if spawning the same task mulitple times with different underlying types for the impl Trait. Affected versions are 0.4.x, 0.5.0 and 0.6.0, which have been yanked.」两种具体类型的 future 大小不同,却共用按其中一种算出来的那一个池。修复在 PR #3425(squash 提交「executor: fix unsoundness due to impl Trait, improve macro error handling.」,首次进入 embassy-executor-macros 0.6.1,对应 embassy-executor 0.6.1 和 0.5.1),就是上面 check_arg_ty 里的 visit_type_impl_trait;同一个 PR 还让宏不再解析函数体,并加上了出错时仍然展开出完整签名的恢复逻辑。
#[main]
#[main] 的输出:
#[doc(hidden)]
fn ____embassy_main_task(spawner: Spawner)
-> impl ::core::future::Future<Output = ()> {
#[allow(clippy :: future_not_send)]
async fn ____embassy_main_task_inner_function(spawner: Spawner) {
spawner.spawn(blink(100).unwrap());
}
{ ____embassy_main_task_inner_function(spawner) }
}
// ... __embassy_main:和 blink 一样的外层函数,POOL_SIZE = 1
fn main() -> ! {
unsafe fn __make_static<T>(t: &mut T) -> &'static mut T {
unsafe { ::core::mem::transmute(t) }
}
let mut executor = ::embassy_executor::Executor::new();
let executor = unsafe { __make_static(&mut executor) };
executor.run(|spawner|
{
let main_task = __embassy_main(spawner).unwrap();
spawner.spawn(main_task);
})
}
main.rs 的 run 先把用户的 main 改名 __embassy_main,再给它套上 #[::embassy_executor::task()]:主任务就是一个 pool_size = 1 的普通任务,和其他任务一样存在 static 里、一样被 poll。真正的 main 返回 !:建执行器、把主任务 spawn 进去、run 永不返回。
执行器本身放在 main 的栈上,用 transmute 把 &mut 的生命周期抬成 'static,满足 run(&'static mut self, ...)。这之所以可以,是因为 main 返回 !,它的栈帧永远不会被弹出;Executor::run 的文档也把「a local variable in a function you know never returns」列为三种办法之一。在没有 static_cell 的情况下这省掉了一个 static,代价是执行器占的是栈,不是 .bss。
每个平台有一个入口宏(main_cortex_m、main_riscv、main_std……),差别只在一张 Arch 表:
| 宏 | 默认入口属性 | 风格 |
|---|---|---|
main_cortex_m | #[cortex_m_rt::entry] | 标准 |
main_riscv | #[riscv_rt::entry] | 标准 |
main_avr | #[avr_device::entry] | 标准 |
main_cortex_ar、main_spin、main_std | 无 | 标准 |
main_wasm | #[wasm_bindgen::prelude::wasm_bindgen(start)] | Box::leak 执行器,start 后返回 Ok(()) |
main_unspecified | 无 | 必须给 executor = "...",否则报错 |
两个参数可以覆盖默认:entry = "..." 换入口属性(给空字符串就不加),executor = "..." 换执行器类型。embassy-stm32 的低功耗执行器就是这样接进来的:#[embassy_executor::main(executor = "embassy_stm32::executor::Executor", entry = "cortex_m_rt::entry")],embassy-executor 本身不开任何平台,#[main] 解析到 main_unspecified,由 executor 参数满足它的要求(见各平台的执行器)。main 的签名检查和任务类似,另外要求恰好一个参数(「main function must have 1 argument: the spawner.」)。
小结
#[task]生成两层函数:内层是原函数体,外层返回Result<SpawnToken<impl Sized>, SpawnError>,体内有唯一的static POOL。- 稳定版上池的类型是按
const fn算出大小和对齐的字节数组TaskPoolHolder,future 类型从函数项推断、从不写出;nightly走type_alias_impl_trait。 - 宏拒绝泛型、
where、impl Trait参数、非'static引用和模式参数;超过 16 个参数时报的却是返回类型的错。 impl Trait参数在 0.4 到 0.6.0 曾被放行并导致越界访问,0.6.1 起拒绝。#[main]把用户的main变成一个pool_size = 1的任务,在不返回的main栈上建执行器;entry和executor参数让 HAL 换入自己的执行器。