Skip to main content

Crate embassy_supervisor_macros

Crate embassy_supervisor_macros 

Source
Expand description

Proc-macro for embassy-supervisor.

supervisor_graph! is the single source of a task graph: it declares the nodes (and an optional elastic pool), generates their statics, and computes the topological order at compile time. A dependency cycle is a compile error; an unknown dependency name is a compile error.

Surface (each item may be #[cfg(...)]-prefixed):

node NAME = Mode, deps: [A, B], spawn: <spawn>[, executor: EXEC][, disabled];
node NAME = Mode, deps: [A, B], task: <worker>[, pool_size: N][, executor: EXEC]
    [, resources: [[#[cfg(..)]] RES: [local] [shared|consume] Type, ..]]
    [, slot_timeout: MS][, disabled];
node NAME = Mode, deps: [A];                 // neither => a parked node the app spawns
executor EXEC;                               // runtime-filled SendSpawner slot
pool NAME = [Mode, ..], deps: [A][, executor: EXEC], spawn: <fn> | task: <worker>,
    [resources: [RES: [local] shared Type, ..],]
    policy: [<Ty> =] <expr>, min: N, max: M[, slot_timeout: MS];

deps: entries name a node or a pool; a pool dep resolves to that pool’s floor member (member 0, the min-kept one), i.e. “start after the pool is up”. A repeated dep or a redeclared node/pool name is a compile error.

An executor NAME; slot may carry #[cfg(...)], but validation does not model cfg predicates: a node referencing a slot that is cfg’d out while the node is cfg’d in surfaces as rustc’s cannot find value NAME, not a macro error — don’t gate an executor slot more restrictively than the nodes that reference it. executor EXEC; emits a pub static EXEC: SpawnerSlot; the app fills it with a SendSpawner (InterruptExecutor::start, Spawner::make_send) before Supervisor::start, and nodes carrying executor: EXEC spawn through it instead of the supervisor’s own executor (their futures must be Send; an unfilled slot fails the spawn with SpawnError::Busy). A pool is emitted as ElasticPool<P>, so the macro needs the policy type P. By default it derives P from a Ty::new(..)-shaped policy: value (e.g. DeferredShrink::new(..) => P = DeferredShrink). Give policy: <Ty> = <expr> to state P explicitly when the value isn’t that shape — a const, a free fn, a builder chain (X::new(..).with(..)), or a qualified path. spawn: takes a path or a partial call to a task fn taking the node first (spawn: f => s.spawn(f(&NAME)?); spawn: f(a) => s.spawn(f(&NAME, a)?)), or, for a node, a closure / ready spawn fn emitted verbatim (for anything that doesn’t fit that shape). A pool’s spawn: is the same path/partial-call form with &POOL[j] injected first, via a generated spawn_<pool>::<j> glue fn; a pool has no closure form (members are instantiated per index).

task: takes the same path/partial-call forms but names a plain async worker fn — possibly generic (turbofish or inferred) — instead of a hand-written #[embassy_executor::task]. The macro stamps a concrete shell task per declaration (embassy forbids generic tasks: one static TaskPool per concrete future type), sized by pool_size: on a node (default 1) or by the member count on a pool. Worker args are evaluated inside the shell — at the task’s first poll, on the node’s own executor — so cross-node data should go through awaited accessors, and a cross-core node builds its resources on its own core. task: and spawn: are mutually exclusive; pool_size: requires task:.

Prefer task: — no attribute boilerplate, generic workers, auto-sized pool shells, and it is the only form supporting resources:; the shell inlines into the same poll and its TaskPool replaces the one the attribute would emit. spawn: remains for: a fn that already carries #[embassy_executor::task] and can’t be de-attributed (another crate); a task also spawned outside the graph (sharing its one TaskPool instead of duplicating it as a shell); the verbatim closure form (custom spawn-time logic); and args that must be evaluated at spawn time on the supervisor’s executor rather than at the shell’s first poll. Worked examples: README “spawn: vs task: — which to use”.

Two task: footguns, spelled out because nothing warns about either:

  • a partial-call extra that can be missing at first poll is a task-side panic, not a failed spawn — extras are for infallible accessors. A value that might not exist yet belongs in resources: (a shared entry for a fan-out handle), where the pre-spawn gate turns “missing” into a clean SpawnError::Busy;
  • a verbatim-closure spawn: node is invisible to the trace/name glue — the closure owns the SpawnToken, so adopt/stamp_name is YOUR job inside it, and a stable proc-macro cannot emit a warning when you forget.

resources: [RES: [local] [shared|consume] Type, ..] (requires task:) threads owned resources from main into the worker instead of re-acquiring them inside the task (Peripherals::steal()). Each entry emits a pub static RES slot at the declaration site; main moves the resource in with RES.provide(..) (consuming the Peripherals field — the compile-time exclusive-ownership guarantee), the generated glue take()s it just before the spawn (an unprovided slot fails Supervisor::start with SpawnError::Busy after a bounded wait — fail-closed, not a task-side panic), and the shell passes the worker &mut Type (after the node arg, in declared order, before any partial-call extras) and restore()s the value after the worker returns, so a Terminate respawn re-takes the same instance. Take-kind slot names are statics: unique across the graph. Entries may carry per-entry #[cfg(...)] (the slot, gate, glue, shell param, and worker-call argument all follow it — gate the worker fn’s matching parameter with the same #[cfg]).

Per-entry kind markers refine that default (order-free; local composes with either of the mutually-exclusive consume/shared):

  • consume — the worker receives the value by value and no restore is emitted: the slot stays empty after the task exits, so the worker may drop the resource at teardown (a driver whose Drop releases pins/DMA) and a respawn fail-closes (SpawnError::Busy) until the application provide()s a fresh value — the pattern for resources rebuilt each run (e.g. radio driver objects that go stale across a power cycle).
  • shared — a fan-out slot for a Copy handle (an embassy_net::Stack, a &'static shared-bus ref): the glue copies the value out non-destructively (get()T: Copy enforced by its bound), the worker receives it by value, no restore, and the slot STAYS FILLED — so any number of nodes (and whole pools: the only resources: kind pools accept, task: pools only) may declare the SAME slot name. The static is emitted once, with the union of the declaring sites’ cfg predicates; every re-declaration must repeat the kinds + type verbatim.
  • localrequires the non-default local-resources feature (of the supervisor crate, forwarded here): the slot is the graph-site __SvLocalResourceSlot type instead of ResourceSlot — same protocol, no T: Send bound, for !Send driver handles (RefCell-/NoopRawMutex-based). Feature-gated because it is the one graph form that emits unsafe code into the CONSUMER’S crate: an unsafe impl Sync whose soundness is the single-core contract (all provide/take/restore of the slot on one core). It cannot combine with executor: (a SendSpawner-routed node needs a Send future — macro error), and a consumer crate forbidding unsafe_code cannot use local (the assertion lands in its code, like the trace-hooks symbols).

slot_timeout: MS (node and pool; milliseconds ≥ 1) overrides the node’s pre-spawn wait bound for its executor: slot and resources: gates (default 100 ms — sized for “provided before start()”). Raise it for consumers of a provider node — a first-in-topo node whose worker builds the resources at runtime and provide()s them (the graph-native hw_init): size the timeout to the provider’s async build time and the gate wait becomes a rendezvous instead of a Busy. See the README’s provider-node recipe.

A graph holds at most 256 node slots (including pool members): all graph indices are u8, and the macro rejects a larger declaration at expansion.

Nodes, pools, and individual deps may carry #[cfg(...)] attributes. A proc-macro can’t evaluate cfg, so the node array is a fixed-length [Option<&TaskNode>; M] over all declared slots (each entry Some/None via a cfg-expression), the dep table is cfg-aware per-dep, and the order runs through topo_sort_const at const-eval (after cfg). Absent nodes are skipped at runtime.

Generated items (at the call site): one pub static per node, a [TaskNode; K] array + spawn_<pool> glue fn + <POOL>_POOL ElasticPool + the structural pub consts <POOL>_MIN / <POOL>_MAX / <POOL>_MEMBERS (usize; for const-context sizing downstream — a const cannot read them off the member static) per pool, one slot pub static per resources: entry (shared entries: one per unique name; plus, iff any entry is local, the __SvLocalResourceSlot type), plus a single pub static GRAPH: Graph<M> bundling the node slots, the dependency table, the topological order, and (with the pool feature) the pools — pass &GRAPH to Supervisor::new. The backing tables are private; read them through GRAPH.nodes / GRAPH.deps / GRAPH.order / GRAPH.pools (node count is GRAPH.nodes.len()).

With the supervisor’s trace feature (forwarded here) the generated spawn glue also captures each SpawnToken’s task id into its node (set_task_id); with metadata-names it stamps the node name into the task Metadata. These are independent: metadata-names without trace emits a name-only spawn path (stamp_name, no id capture, no _embassy_trace_* dependency), so node names reach external tooling (rtos-trace/SystemView) without the trace recorders. With trace-hooks the macro additionally defines the seven _embassy_trace_* hook symbols at the declaration site (the supervisor crate is forbid(unsafe_code) and cannot), forwarding to the supervisor’s trace recorders — requires an edition-2024 consumer, and exactly one graph declaration (or hook set) per binary.

Types are referenced absolutely (::embassy_supervisor::…), so the consuming crate must depend on embassy-supervisor under its real name (not aliased).

Macros§

supervisor_graph
Declare a supervised task graph; see the crate docs for the surface syntax.