Skip to main content

Executor

Struct Executor 

Source
pub struct Executor<'s> { /* private fields */ }

Implementations§

Source§

impl<'s> Executor<'s>

Source

pub unsafe fn open_in( config: &ExecutorConfig<'_>, backing: &'s mut [MaybeUninit<u64>], sizing: ExecutorSizing, ) -> Result<Self, NodeError>

phase-271 — open a new executor session over caller-supplied backing, sized by sizing (per-entry sizing). The core, non-generic sized entry point: the alloc open convenience leaks a default backing and delegates here, and the nros::main! macro emits a backing sized to the entry’s own entity count.

Phase 115.M.4 — auto-registers the cffi vtable for whichever backend the build was configured for, mirroring the C++ side’s #ifdef NROS_RMW_<NAME> fan-out in <rclcpp/node.hpp>. The runtime’s atomic vtable slot is idempotent: a re-call of any backend’s register() is a no-op, so the fan-out below is safe to invoke on every Executor::open (cheaper than a Once and doesn’t pull in std::sync for no_std targets).

Connects to the middleware at the locator specified in config.

§Safety

backing must be ≥ sizing.u64_len() words, live for 's, and be otherwise untouched while the executor lives (see from_session_in).

Source§

impl Executor<'static>

Source

pub fn open(config: &ExecutorConfig<'_>) -> Result<Self, NodeError>

Open a new executor session using the active RMW backend, at the build-time default sizing. Convenience over open_in: leaks a default-sized backing (executor- lifetime) so existing callers keep the zero-storage-arg signature. Per-entry sizing goes through open_in / the nros::main! macro.

§Example
ⓘ
let config = ExecutorConfig::from_env().node_name("my_node");
let mut executor = Executor::open(&config)?;
Source

pub fn open_sized( config: &ExecutorConfig<'_>, sizing: ExecutorSizing, ) -> Result<Self, NodeError>

phase-271 — like open but sized to a caller-supplied sizing (its own declared topology) instead of the build-time default. The alloc entry point the nros::main! macro’s native board path uses to size a fat entry (>default MAX_CBS callbacks) without a workspace-global NROS_EXECUTOR_MAX_CBS. Leaks a sizing-sized backing (executor-lifetime); no-alloc entries use open_in with their own static.

Source

pub fn open_multi(specs: &[SessionSpec<'_>]) -> Result<Self, NodeError>

Phase 128.F.1 — explicit per-backend session declaration for bridge mode. specs[0] becomes the primary session; specs[1..] open as extras keyed by RMW name. After construction, every create_node_on(name, rmw) call dispatches to whichever session was opened under that RMW name (or, when the rmw name matches the primary, the primary session itself).

Single-backend callers should keep using open — this entry costs an extra open_with_rmw per spec and adds no value when only one backend is linked.

$NROS_RMW env is ignored: bridge mode wants explicit names.

Default-sized alloc convenience over open_multi_in (leaks a default backing).

Source

pub fn open_with_rmw( rmw_name: &str, config: &ExecutorConfig<'_>, ) -> Result<Self, NodeError>

Phase 104.C.1 — open the Executor against a specific RMW backend by name. Selects from the named registry (Phase 104.B.2). rmw_name must match one of the names a backend registered under ("zenoh", "cyclonedds", "xrce", …).

Equivalent to Executor::open when the registry has exactly one backend (the default-backend fast path). Use this entry point in multi-backend builds where Executor::open would pick the first-registered slot.

Single-Executor multi-Node multi-RMW (the long-term Design X from docs/roadmap/phase-104-multi-backend-bridges.md) is follow-up work — Phase 104.C.2 + C.3.

Default-sized alloc convenience over open_with_rmw_in (leaks a default backing).

Source§

impl<'s> Executor<'s>

Source

pub unsafe fn open_multi_in( specs: &[SessionSpec<'_>], backing: &'s mut [MaybeUninit<u64>], sizing: ExecutorSizing, ) -> Result<Self, NodeError>

Per-entry-sized open_multi: carves backing for the executor’s tables instead of leaking a default one.

§Safety

backing/sizing as in from_session_in.

Source

pub unsafe fn open_with_rmw_in( rmw_name: &str, config: &ExecutorConfig<'_>, backing: &'s mut [MaybeUninit<u64>], sizing: ExecutorSizing, ) -> Result<Self, NodeError>

Per-entry-sized open_with_rmw: carves backing for the executor’s tables instead of leaking a default one.

§Safety

backing/sizing as in from_session_in.

Source§

impl<'s> Executor<'s>

Source

pub unsafe fn from_session_in( session: CffiSession, backing: &'s mut [MaybeUninit<u64>], sizing: ExecutorSizing, ) -> Self

Create an owning executor over caller-supplied backing, sized by sizing. The core, non-generic, per-entry entry point (the alloc from_session convenience leaks a default backing and calls this; the macro / C FFI pass an entry-sized backing).

§Safety

backing must be ≥ sizing.u64_len() words, stay alive for 's, and not be otherwise accessed while the executor lives (it aliases it). sizing.cbs must be ≤ 64 (the u64 ready-set bitmask ceiling).

Source

pub unsafe fn from_session_ptr_in( session_ptr: *mut CffiSession, backing: &'s mut [MaybeUninit<u64>], sizing: ExecutorSizing, ) -> Self

Create a borrowing executor over caller-supplied backing, sized by sizing. Counterpart to from_session_in for the per-tier / C model (the session is borrowed, not owned).

§Safety
  • session_ptr must point to a valid session that outlives the executor and is not moved/dropped while it exists.
  • backing obligations as in from_session_in.
Source§

impl Executor<'static>

Source

pub fn from_session(session: CffiSession) -> Self

Create an executor from an already-opened session, using the build-time default sizing (MAX_CBS/MAX_SC/ARENA_SIZE). Convenience for std/alloc callers that don’t size per-entry: it leaks a default-sized backing (executor-lifetime, one-time) and calls from_session_in. Per-entry sizing goes through the macro / open_in instead.

Source

pub fn from_session_with( session: CffiSession, config: &ExecutorConfig<'_>, ) -> Self

from_session with an ExecutorConfig, so a caller that brings its own session can also bring its own clock.

issue 0709 / issue 0687 — from_session takes no config, and that is the path the no-port population uses: it accepts any Session, so a consumer with a non-cffi backend reaches the executor through it and had NO way to install clock_us. phase-359 W10 argued the std-without-a- port clock fallbacks could go because “a caller with a clock installs it through ExecutorConfig::clock_us” — true for open, false here, which is half of why that deletion was reverted.

Only the timing sources are read from config; identity (locator, domain, names) belongs to the session the caller already opened. As in open_in, a None field does NOT clobber the platform default — it means “not specified” (the bug issue 0671 records).

Source

pub unsafe fn from_session_ptr(session_ptr: *mut CffiSession) -> Self

Create an executor from a borrowed session pointer, default-sized. The alloc convenience wrapper over from_session_ptr_in — leaks a default backing so existing callers keep the zero-storage-arg signature.

§Safety
  • session_ptr must point to a valid, initialized session that lives at least as long as this executor.
  • The caller must not move or drop the session while the executor exists.
Source§

impl<'s> Executor<'s>

Source

pub unsafe fn open_with_session(session: *mut CffiSession) -> Executor<'static>

Phase 228.B (RFC-0015) — construct a tier task’s executor that shares a session opened once by the orchestration main().

In the per-tier execution model main() opens one RMW session, then spawns one RTOS task per priority tier; each task calls this to get an Executor over the same session (the Borrowed session store — this executor neither owns nor closes it), registers its tier’s callback groups, and spins. Thin alias over Executor::from_session_ptr.

§Safety

session must outlive every executor/task built from it (the orchestration main() holds it and never returns / WFIs), and must not be mutated except through these executors’ spin calls.

Source

pub unsafe fn open_with_session_in( session: *mut CffiSession, backing: &'s mut [MaybeUninit<u64>], sizing: ExecutorSizing, ) -> Self

phase-271 — per-tier borrowed-session constructor over caller-supplied, per-tier-sized backing. The sized counterpart to open_with_session: each RTOS tier task owns its own backing so tiers size independently.

§Safety

session obligations as in open_with_session; backing/sizing as in from_session_ptr_in.

Source

pub fn session_ptr(&mut self) -> *mut CffiSession

Raw pointer to this executor’s RMW session, for the per-tier model: the boot task opens the one session via Executor::open (the RMW session is a process-wide singleton — opening twice fails), then hands this pointer to each spawned tier task’s Executor::open_with_session. The boot task’s executor owns the session and outlives every borrower, so the pointer stays valid for the program’s life. Works for both Owned and Borrowed stores.

§Safety

The returned pointer aliases self.session. Callers must keep self alive (not moved/dropped) for as long as any tier executor uses the pointer, and must only touch the session through executor spin calls (the RMW backend serializes concurrent access through its own locks).

Source

pub fn session_handle(&mut self) -> SessionHandle

Opaque, Send form of session_ptr — the per-tier model hands this to each spawned tier task (it can cross the RTOS task / thread boundary, which a bare *mut cannot). See SessionHandle.

§Safety

Same contract as session_ptr: self (the session owner) must outlive every executor built from the handle.

Source

pub unsafe fn open_with_session_handle( handle: SessionHandle, ) -> Executor<'static>

Open an Executor over the session a SessionHandle refers to (the Borrowed store — neither owns nor closes it). The tier-task counterpart to session_handle.

§Safety

The handle’s session must still be alive (its owning executor not moved or dropped); access only through executor spin calls.

Source

pub unsafe fn open_with_session_handle_in( handle: SessionHandle, backing: &'s mut [MaybeUninit<u64>], sizing: ExecutorSizing, ) -> Self

phase-271 — sized counterpart to open_with_session_handle (per-tier backing).

§Safety

As open_with_session_handle + from_session_ptr_in.

Source

pub unsafe fn open_with_session_slot( session: *mut CffiSession, slot: &'s mut TierExecutorBackingSlot, ) -> Self

issue 1571 — a SPAWNED tier’s executor over its slot of the entry’s TierExecutorBacking — the road every board’s run_tiers takes for each tier but the boot one.

The slot’s TYPE is the size: it is ExecutorSizing::DEFAULT.u64_len() words and this opens with ExecutorSizing::DEFAULT, so the pairing is exact by construction and lives here, once, rather than at five boards. open_with_session is the heap twin this replaced on those roads.

§Safety

session obligations as in open_with_session.

Source

pub unsafe fn open_with_session_handle_slot( handle: SessionHandle, slot: &'s mut TierExecutorBackingSlot, ) -> Self

issue 1571 — open_with_session_slot over a SessionHandle, for the boards whose tier tasks carry one.

§Safety

As open_with_session_handle.

Source

pub fn set_active_groups( &mut self, groups: &[(&str, &str, &str)], ) -> Result<(), BindError>

Phase 228.C — set this tier executor’s active callback-group filter. The generated per-tier task calls this before registering nodes; afterwards only callbacks whose .callback_group() is in groups register here. An empty slice (or never calling it) leaves the wildcard — register all callbacks (the single-tier degenerate case + today’s behaviour).

Source

pub fn group_active( &self, node_name: &str, node_namespace: &str, group: &str, ) -> bool

Phase 228.C — whether a callback in group should register in this executor under the current filter. The wildcard accepts everything.

Source

pub fn set_primary_identity(&mut self, rmw_name: &str, locator: &str)

Set the node name and namespace used for liveliness tokens.

Called by open() to propagate config values. When register_subscription or register_service creates entities, these values are attached to the Phase 156 — record the primary session’s backend identity (rmw name + locator) so NodeBuilder::resolve_session_slot can detect when a .rmw(name) matches the primary instead of opening a SECOND backend session against the same singleton (zenoh-pico’s g_session is process-wide; opening twice fails). Executor::open* calls this automatically; the C surface (rclc_executor_init) calls it manually because it constructs via from_session_ptr which doesn’t know the open metadata. Empty strings = “no primary identity tracked”; the cache check degrades to always-miss.

Source

pub fn set_domain_id(&mut self, domain_id: u32)

TopicInfo/ServiceInfo so the zenoh backend can declare liveliness. Issue 0656 — set the ROS domain for entities this executor declares.

For bindings that build an executor from an existing session (from_session_ptr_in), where no ExecutorConfig is available and the domain would otherwise stay at its 0 floor.

Source

pub fn domain_id(&self) -> u32

The ROS domain this executor declares entities on.

Source

pub fn set_node_identity(&mut self, node_name: &str, namespace: &str)

Source

pub fn declare_remap( &mut self, node_name: &str, namespace: &str, from: &str, to: &str, ) -> Result<(), ()>

Record one launch <remap from= to=/> rule for the node identified by (node_name, namespace). Rules are matched in declaration order (first wins) by Self::resolve_entity_name_for. Errors when a string overflows its slot or the table is at MAX_REMAPS — callers surface this rather than silently dropping a routing rule.

This declares an AUTHORITATIVE rule. Declaration order settles which of two launch rules for one name wins; it is not how an argv rule is ranked against a launch rule. The --ros-args rules share this table but are tagged as the fallback tier, and resolution reads both tiers through crate::names::resolve_name_layered — so an argv rule installed BEFORE a launch rule still loses to it.

Source

pub fn resolve_entity_name_for( &self, node_name: &str, namespace: &str, source: &str, ) -> Result<ResolvedName, ()>

Resolve a source-level entity name for the node identified by (node_name, namespace): ROS 2 name expansion (~/relative → FQN) plus this node’s declared remap rules (exact-FQN match, first rule wins). Nodes with no rules still get expansion. Errors on an unexpandable name (see crate::names::expand_name).

Two tiers, through crate::names::resolve_name_layered: the rules Self::declare_remap recorded for this node are AUTHORITATIVE (the generated entry declares the launch projection there), and the --ros-args rules crate::ros_args::install_argv_remaps put in the same table are the FALLBACK, consulted only when no launch rule matched. rcl does the same with its local and global arguments (rcl_remap_topic_name, Humble rcl/remap.h).

Source

pub fn resolve_entity_name(&self, source: &str) -> Result<ResolvedName, ()>

Self::resolve_entity_name_for against the executor’s CURRENT node identity (set_node_identity) — the nros-c registration sites set that identity per node immediately before registering each entity.

Source

pub fn bind_node_name_sched( &mut self, name: &str, namespace: &str, sc: SchedContextId, ) -> Result<(), BindError>

Seed a config-resolved tier binding by (name, namespace) before the node is built. NodeBuilder::build consults this table when no explicit .sched() override is given — the table entry then wins over the SchedContextId(0) default (precedence: explicit > table > 0).

Call BEFORE node_builder(name).build(). An existing entry for the same (name, namespace) key is overwritten (last-write wins). Overflow past MAX_NODES is an Err, never a dropped binding — issue 1172. An empty namespace is normalised to "/" to match what NodeBuilder::build computes for a root-NS node.

Source

pub fn bind_group_sched( &mut self, name: &str, namespace: &str, group: &str, sc: SchedContextId, ) -> Result<(), BindError>

Seed a config-resolved tier binding by (name, namespace, group) before entities are registered. apply_node_default_sched consults this table first (group table > node default > SchedContextId(0)).

Call BEFORE entity creation. An existing entry for the same (name, namespace, group) key is overwritten (last-write wins). Overflow past MAX_CBS is an Err, never a dropped binding — issue 1172. An empty namespace is normalised to "/" to match NodeBuilder::build. Mirror of bind_node_name_sched.

Source

pub fn default_sched_context_id(&self) -> SchedContextId

Identifier of the auto-created default Fifo-class scheduling context. Every callback registered without an explicit Self::bind_handle_to_sched_context binds to this SC.

Source

pub fn create_sched_context( &mut self, sc: SchedContext, ) -> Result<SchedContextId, NodeError>

Register a new scheduling context. Returns a SchedContextId callers pass to Self::bind_handle_to_sched_context to attach callbacks. Phase 110.B.

Source

pub fn epoch_now_us(&self) -> Option<u64>

RFC-0052 W3b.2 — wall-clock µs since the UNIX epoch, when this target has an epoch source (config epoch_us, defaulted from SystemTime on hosted configs). None = no wall clock; age monitors must not have been baked (the emitter refuses).

Source

pub fn set_monitor_table(&mut self, table: &'static [MonitorSpec])

RFC-0052 W3b.4 — install the baked contract-monitor table. Call BEFORE entity creation so create_publisher can attach each contracted endpoint’s counter cell. Mirrors set_qos_overrides: &'static, codegen-baked, empty by default.

phase-496 W1 – the executor watches at most MAX_MONITORS rows (NROS_EXECUTOR_MAX_MONITORS). A generated entry cannot reach this with more: its table carries a compile-time assertion that names the knob, and the C++ installer refuses at runtime. A hand-written longer table is a debug-build panic naming the knob, because in release the spin loop would inspect only the first MAX_MONITORS and leave the rest silently unwatched.

Source

pub fn try_set_monitor_tables( &mut self, table: &'static [MonitorSpec], ages: &'static [AgeMonitorSpec], ) -> Result<(), MonitorTableFull>

phase-496 W1 – install BOTH baked monitor tables, or neither.

The refusing installer: a table longer than this executor watches (MAX_MONITORS / MAX_AGE_MONITORS) is Err, and its Display names the knob to raise, before anything is installed. Nothing is truncated: the spin loop inspects only the first MAX_* specs, so accepting a longer table would boot with its tail silently unwatched. The C ABI’s nros_cpp_install_monitors refuses on the same check.

Source

pub fn monitor_table(&self) -> &'static [MonitorSpec]

The installed monitor table (empty unless the entry set one).

Source

pub fn set_age_table(&mut self, table: &'static [AgeMonitorSpec])

W3b.5 — install the baked subscriber age-contract table. Call BEFORE entity creation so create_subscription can attach each contracted endpoint’s age cell (needs an epoch source — see ExecutorConfig::epoch_us; without one the take path records nothing and age monitors stay silent).

phase-496 W1 – at most MAX_AGE_MONITORS rows (NROS_EXECUTOR_MAX_AGE_MONITORS), on the same terms as Self::set_monitor_table.

Source

pub fn age_table(&self) -> &'static [AgeMonitorSpec]

The installed age table (empty unless the entry set one).

Source

pub fn set_hazard_table(&mut self, table: &'static [HazardSpec])

phase-462 W3b — install the baked hazard table. Each row’s guards name rows of the tables Self::set_monitor_table and Self::set_age_table install, so install those first; a guard whose row is absent never faults. The observer runs after the monitor rows on every monitor tick and reports hazard-detected, hazard-reaction and hazard-recovered on the same drain.

Source

pub fn hazard_table(&self) -> &'static [HazardSpec]

The installed hazard table (empty unless the entry set one).

Source

pub fn set_epoch_clock(&mut self, epoch_us: fn() -> u64)

W3b.5 — override the wall-clock (epoch µs) source age monitors take message stamps against. Hosted builds default to SystemTime; a board with a synced RTC installs its own here (or via ExecutorConfig::epoch_us). Call BEFORE entity creation — the age hook captures this at create_subscription time.

Source

pub fn skipped_releases(&self) -> u32

phase-474 I11 – grid releases skipped since the last clear_release_jitter_stats, because the loop came back after they were due. Zero for a declared-cadence or zero-timeout caller.

Source

pub fn release_jitter(&self) -> (u64, u32, u32)

Release-jitter statistics from the spin loop: worst lateness in microseconds, the number of wakes that were already late, and the number of wakes total.

The maximum is the figure of merit, and the ratio is what tells the two failures apart: one late wake in ten thousand is a glitch, ten thousand in ten thousand means the period cannot be met at all.

Zero on a build with no clock – spin_period refuses to run at all there (issue 0709), so there is nothing to have measured.

Source

pub fn late_wakes_beyond_granularity(&self) -> u32

phase-436 A2c – of the late wakes release_jitter counts, those late by more than Self::release_jitter_granularity_us.

A wake is late at one microsecond past its release. A loop paced by a tick-granular park comes back up to a tick past it on every wake, by design, so late approaches total on a loop that is keeping its period perfectly well. This count leaves that rounding out: it is the lateness the measurement can actually resolve. Cleared with the rest.

Source

pub fn clear_release_jitter_stats(&mut self)

Reset the release-jitter statistics. For monitoring code that logs and clears per window, so a single early outlier does not pin the maximum for the life of the process.

Source

pub fn set_fault_handler(&mut self, f: fn(&Violation))

Source

pub fn set_reaction_hook(&mut self, hook: Option<(ReactionHookFn, *mut c_void)>)

phase-503 — install the REACTION hook: f(event, ctx) is called on the spin thread when a supervised route passes its deadline (route-deadline-runtime), with the row’s reaction id, row index and mechanism. It also stands in for Self::set_fault_handler on a DeadlineAction::Fault when no fault handler is set (reaction id 0, the sched-context slot as the row), so a C or C++ image has a fault path that does not panic. Replaces any earlier hook; None removes it.

The hook must not block and must not re-enter this executor: it starts the fallback, or hands it to another thread, and returns.

Source

pub fn route_next_due_us(&self) -> Option<u64>

phase-503 — microseconds from now to the earliest deadline of a pending supervised route (0 when one is due), or None when none is waiting or the executor has no clock. A port that supervises with its own timer (MECHANISM_TIMER) re-arms it from this after each spin.

Source

pub fn register_wake_source( &mut self, next: NextDeadlineFn, ctx: *mut c_void, ) -> Result<WakeSourceId, NodeError>

phase-436 W6 — contribute a deadline source.

The core never names an RTOS: a port registers k_poll, epoll, a queue set or a device ISR here, and the executor only ever asks “when will you next need me?”.

Refuses past capacity rather than dropping silently. A source that was quietly discarded would leave the executor sleeping past a deadline it had been told about, which is the failure this whole phase exists to remove.

Source

pub fn set_park_primitive( &mut self, park: ParkUntilFn, ctx: *mut c_void, granularity_us: u64, )

Install THE thing that blocks. Singular by nature — only one primitive can actually wait — so this replaces whatever was there. granularity_us is the finest park this primitive can actually express. The port states it because only the port knows: the ABI signature is not the limit — an RTOS tick is a second, coarser one, and a timespec primitive is a finer one.

Source

pub fn wake_raw_ptr(&self) -> *mut c_void

The wake primitive a port must hand its ParkUntilFn as ctx, or NULL when this build has none.

phase-436 W7 — a park that waits on its own fresh object is NOT breakable by the backend’s listener, and installing one makes latency worse: once a port has parked, the transport drain drops to non-blocking, so an unbreakable park sleeps out its full deadline with data already waiting. Waiting on THIS object keeps the async break.

Source

pub fn has_park_primitive(&self) -> bool

Whether a platform park primitive is installed.

Source

pub fn last_park(&self) -> (u64, WakeSourceId)

phase-436 — the bound the last park used, and which source won it.

Source

pub fn last_park_achieved_us(&self) -> u64

phase-436 W2 — the park the platform was actually asked for, after rounding the request up to an expressible value.

Differs from last_park().0 exactly when the request did not land on the platform’s grid. Both are reported because a cadence that cannot be waited in must not be presented as if it were met.

Source

pub fn release_jitter_granularity_us(&self) -> u64

phase-436 W3 (issue 1194) — the granularity the release-jitter measurement was actually made at, in microseconds.

The jitter statistic is reported in microseconds, but the mechanism that PACES the loop decides how fine it can honestly be, and there are two mechanisms:

  • No declared cadence — the loop is paced by the blocking wait itself, so it cannot come round more precisely than one park granule. Reporting µs there is precision the wait cannot deliver.
  • A declared cadence (set_spin_nominal_us) — the caller paces itself, as nros-cpp’s tiers do with platform_sleep_us between spins. That sleep is finer than the wait, so the measurement really is good to a microsecond and coarsening it would understate it.

Reported rather than applied: this does not change what the monitor judges, because silently coarsening a bound could mask real lateness. It says what the number is worth.

Source

pub fn park_granularity_us(&self) -> u64

The finest park this build can actually express, in microseconds.

Comes from the INSTALLED primitive, because only the port knows. The exported ABI signature is not the limit in either direction (issue 1242):

  • an RTOS tick is a COARSER limit — ThreadX pins TX_TIMER_TICKS_PER_SECOND = 100, a 10 ms tick, so a 1 ms park is not achievable there however the ABI is spelled;
  • a timespec primitive is a FINER one — POSIX’s sem_timedwait and pthread_cond_timedwait are nanosecond-native, and only the uint32_t timeout_ms signature floors them at a millisecond.

This was a hardcoded 1_000 justified by “every nros_platform_wake_* port takes uint32_t timeout_ms”. That was right for FreeRTOS by coincidence and wrong for the other two, and it made the W3 jitter report over-claim on ThreadX — the failure W3 exists to prevent, reproduced one layer below W3 in its own dependency.

With nothing installed the fallback is 1 ms, which IS the honest floor of the wake_wait_ms ABI every port exports today.

The declared granularity counts only while the park is USABLE. An executor whose backend has no live wake (XRCE, or a session whose callback is gone) never parks: spin_once waits in the transport’s own receive instead, whose timeout is whole milliseconds. Claiming a POSIX park’s 1 us there over-claimed the jitter report, and worse, rounded the bound to microseconds that the millisecond wait then truncated, so a sub-millisecond deadline became a non-blocking receive and the loop spun hot until it passed.

Source

pub fn set_report_violations(&mut self, enabled: bool)

Issue #514 — whether the executor logs each violation as it is detected (the default).

Turn this off in an application that reports violations its own way via Self::drain_violations; the ring is unaffected either way.

Source

pub fn set_violation_drain_report(&mut self, enabled: bool)

phase-474 I1 – the drain-and-report hook: at the end of every spin, drain the ring and log each entry with its sequence number and the total / dropped counters (contract violation #N: <rule> <fqn> measured=.. declared=.. (total=.. dropped=..)).

While on it REPLACES the log line at detection, so one verdict is one line, and an application that wanted to drain the ring itself sees an empty one. The generated C/C++ entries turn it on when the image sets CONFIG_NROS_VIOLATION_DRAIN_REPORT (off by default). The trace marker (callback_trace::MARKER_VIOLATION) and the SWD record do not depend on it: both are written at detection.

Source

pub fn violations_dropped(&self) -> u32

Issue #514 / phase-474 I1 – violations evicted from the full ring before any drain took them.

The ring keeps the LATEST MAX_VIOLATIONS, so non-zero means the OLDEST verdicts were lost, never the newest. (Before phase-474 a full ring refused the push, which kept the first verdicts since boot and lost every later one.)

Source

pub fn violations_total(&self) -> u32

phase-474 I1 – contract violations this executor stored since boot (wrapping). The newest entry in the ring is number violations_total(); total - dropped - pending is how many drains have taken.

Source

pub fn drain_violations(&mut self, f: impl FnMut(&Violation))

RFC-0052 W3b.4 – drain pending contract violations, OLDEST first. Draining clears the ring. Call between spins, never from a callback.

Source

pub fn drain_violations_numbered(&mut self, f: impl FnMut(u32, &Violation))

Self::drain_violations with each entry’s sequence number (1-based, per executor; the same number the drain-and-report line and the trace marker carry), so a reader can tell a gap from a quiet interval.

Source

pub fn set_monitor_arming(&mut self, on_call: bool, grace_ms: u32)

phase-474 I2 – choose when this executor’s contract monitors arm, overriding the build default (NROS_MONITOR_ARM_ON_CALL, NROS_MONITOR_ARM_GRACE_MS). Call before the first spin (an arm_monitors made before it is undone by this); after the first spin an executor that already armed stays armed.

on_call: wait for Self::arm_monitors or super::monitor::request_monitor_arming (rclc_monitors_arm()). grace_ms: arm anyway that long after the first spin (0 = never by deadline; with on_call == false, 0 means “at the first spin”).

Source

pub fn arm_monitors(&mut self)

phase-474 I2 – the application says its start-up is over: this executor’s monitors start judging now. Idempotent.

Judgement restarts at arming: every rate/age/silence window reopens, a latency or age measured before arming is discarded, the release- jitter statistics are cleared (so the figure after RUN is RUN’s), and a stack low-water mark already past the declared minimum is reported again, because stack depth reached at start-up is a real fault, not start-up noise.

Source

pub fn monitors_armed(&self) -> bool

phase-474 I2 – whether this executor’s monitors are armed.

Source

pub fn violations_suppressed_before_arm(&self) -> u32

phase-474 I2 – verdicts reached before the monitors armed: counted, never stored, logged, sunk or traced.

Source

pub unsafe fn set_violation_sink( &mut self, sink: Option<(ViolationSink, *mut c_void)>, )

Issue 1635 – hand every stored violation to sink, beside the log line (issue 0514’s floor) and the ring push.

phase-474 I7 – not at detection any more: the verdict is queued and leaves from Executor::flush_violation_reports at the end of a spin with slack, so the report never costs the dispatch it judges.

Before this nothing in a generated image drained the ring, so the log line was the only output and /diagnostics stayed silent on every C and C++ road. A report point the RUNTIME owns reaches every road at once — every board’s spin loop goes through the executor — where one per entry template would have to be rebuilt for each board runner. The ring is still filled, so an application (or a fixture) that drains it with Self::drain_violations sees exactly what it did before. None removes the sink.

§Safety

ctx must stay valid for every call of the sink until it is replaced or this executor is dropped (see super::monitor::ViolationSink).

Source

pub fn violation_report_cost_us(&self) -> (u32, u32, usize, u32)

phase-474 I7 – what handing verdicts to the violation sink has cost this executor: (last_us, max_us, pending, overflowed). overflowed counts verdicts evicted from the report queue by newer ones before a slot came (they are still in the ring and the SWD record).

Source

pub fn set_spin_nominal_us(&mut self, us: u64)

Issue #505 — report activations dropped by TimerOverrunPolicy::Skip since the last check.

Unlike the rate/age/latency rules this needs no baked spec table: every periodic timer counts its own overruns, and a dropped activation is a contract failure for any declared period. It runs on the same tick so violations land in the same ring the entry glue drains. Declare the minimum stack headroom this executor’s thread must keep, in bytes. 0 (the default) disables the stack-headroom-runtime rule.

Set by the entry that spawned the thread, because it is the only party that knows what stack it handed over: the executor never sees stack_bytes, and no portable query returns a task’s total stack, so neither an absolute floor nor a percentage can be inferred here. Declare how often this executor’s loop intends to come round, in microseconds. 0 (the default) falls back to the spin_once timeout.

Needed because a driver’s pacing and its blocking bound are separate numbers. nros-cpp’s tier loops sleep spin_period_us between spins but pass a fixed 10 ms timeout, so the release-jitter rule judged every tier against 10 ms regardless of what the contract declared – too lax for a 1 ms tier and far too strict for a 30 ms one.

This also says the CALLER paces the loop (a sleep finer than the park), so the measurement is reported at 1 us. A loop paced by the spin_once wait itself declares with Self::set_spin_nominal_wait_paced_us instead.

Source

pub fn set_spin_nominal_wait_paced_us(&mut self, us: u64)

phase-436 A2b – declare the loop’s cadence, in microseconds, for a loop paced by the spin_once wait itself. 0 withdraws it.

Self::set_spin_nominal_us couples two facts: the period to judge against, and that the caller paces the loop with its own sleep. A loop like for (;;) spin_once(period_ms) has the first without the second. Declaring through the coupled setter would report its jitter at 1 us when the park it waits on is tick-granular (issue 1194’s failure); not declaring at all makes the timeout the period, which is wrong whenever the timeout is not the period (a cadence that is not whole milliseconds, or one raised to a floor).

Here the cadence is the period of the release grid that a spin_once(timeout) loop already runs on, and the timeout stays a bound on each wait. The jitter is judged at grid releases and reported at the park granularity.

Source

pub fn set_min_stack_headroom_bytes(&mut self, bytes: usize)

Source

pub fn stack_headroom_blind(&self) -> bool

phase-436 E3 — whether stack-headroom-runtime is armed on a port that cannot measure, and so will never report a violation.

true means the rule’s silence is not evidence of headroom. Zephyr answers only with CONFIG_INIT_STACKS && CONFIG_THREAD_STACK_INFO; POSIX never does.

Source

pub fn set_default_sched_context(&mut self, sc: SchedContext)

RFC-0052 / phase-296 W3a — replace the DEFAULT scheduling context (slot 0, the SC every unbound callback dispatches through).

The run_tiers model runs one Executor per tier, so a tier-wide scheduling policy ([tiers.<t>] class/budget_us/period_us and the TT window) is exactly “this executor’s default SC”. Boards call this once, before entity creation; explicit per-handle/per-group bindings still take precedence (they never resolve to slot 0).

Sporadic-class SCs get the same sibling SporadicState the create_sched_context path builds, so budget refill/exhaustion applies to the default queue too.

Source

pub fn bind_handle_to_sched_context( &mut self, handle: HandleId, sc_id: SchedContextId, ) -> Result<(), NodeError>

Bind a registered callback to a scheduling context. The next spin_once cycle dispatches the callback through that SC’s queue (FIFO bitmap or EDF heap). Phase 110.B.

Source

pub fn register_time_triggered_dispatcher(&mut self, major_frame_us: u32)

Phase 110.G — enable time-triggered dispatch by setting the executor’s major-frame length. Once set, every spin_once cycle gates dispatch through each entry’s bound SC’s tt_window_offset_us / tt_window_duration_us fields: dispatch only fires when the current monotonic time falls inside the window [off, off + duration) mod major_frame.

major_frame_us = 0 disables the TT gate (default state). Setting a non-zero major frame after callbacks are already registered is allowed — TT gates take effect on the next spin_once cycle.

Source

pub fn apply_time_triggered_schedule<const N: usize>( &mut self, schedule: &TimeTriggeredSchedule<N>, ) -> Result<[SchedContextId; N], TimeTriggeredScheduleError>

Phase 110.G — apply a declarative cyclic schedule.

One-shot helper that wraps the underlying primitives: validates the schedule (major_frame > 0, no overlapping windows, every window fits inside the major frame), sets the executor’s major-frame length, then materialises one SchedContext per window with class = TimeTriggered + the window’s offset / duration. Returns the per-window SchedContextId array so callers can immediately bind_handle_to_sched_context(handle, sc_id) for their subscription / timer handles.

N is the schedule’s declared maximum window count; schedule.window_count gates how many SCs are actually created. Unused trailing slots return SchedContextId::default() (sentinel — callers must respect window_count).

Source

pub fn register_sporadic_timer( &mut self, sc_id: SchedContextId, timer: OpaqueTimerHandle, ) -> Result<Arc<AtomicSporadicState>, NodeError>

Phase 110.E.b — register an ISR-driven refill timer for an already-created Sporadic SC. The caller invokes their platform’s PlatformTimer::create_periodic with the returned Arc<AtomicSporadicState> as user_data and the atomic_sporadic_refill_thunk as the callback, then hands the resulting platform handle to this method via OpaqueTimerHandle::new(handle, destroy_fn).

The Executor stores both the Arc and the handle so Drop can clean them up. Calling this on a non-Sporadic SC returns Err(InvalidSchedContextBinding).

Source

pub fn sched_context(&self, sc_id: SchedContextId) -> Option<&SchedContext>

Inspect a registered scheduling context. Phase 110.B.

Source

pub fn node_builder<'a, 'cfg>( &'a mut self, name: &'cfg str, ) -> NodeBuilder<'a, 'cfg, 's>

Phase 104.C.2 — start a rclcpp-style Node builder for this Executor. The returned NodeBuilder is chainable:

ⓘ
let id = exec.node_builder("ingress")
    .rmw("zenoh")
    .locator("tcp/127.0.0.1:7447")
    .sched(my_sc_id)
    .build()?;

In Phase 104.C.2 the Node table is storage-only — all registered Nodes share the Executor’s primary session. Per- Node session binding (the bridge feature) lands in Phase 104.C.3 when the session cache is wired.

Source

pub fn nodes(&self) -> &[NodeRecord]

Return the Node table — Phase 104.C.2 read accessor.

Source

pub fn node(&self, id: NodeId) -> Option<&NodeRecord>

Borrow a Node’s metadata by id, returning None if the id is out of range.

Source

pub fn node_mut(&mut self, id: NodeId) -> Node<'_, 's>

Source

pub fn set_node_qos_overrides( &mut self, node_id: NodeId, overrides: &'static [QoSOverrideCode], )

Issue #52 — install the baked QoS-override table on one node. Every entity created on it AFTERWARDS folds the matching (topic, role) entries into its QoS, before the backend-compat check — so an override the active RMW cannot honour still errors loudly rather than silently downgrading.

Called by the generated entry (nros::main! → the register seam) right after the node is created and before the component declares entities. Unknown node_id is a no-op.

Source

pub fn node_session_mut(&mut self, node_id: NodeId) -> Option<&mut CffiSession>

Phase 104.C.9.b — resolve the per-Node session for direct entity creation paths (C++ FFI publisher / subscription / service that bypass the register_*_on arena dispatch). Returns None when node_id is out of range or the Node’s session_idx lands outside the executor’s session table.

Source

pub fn create_publisher_on<M: MessageForRmw>( &mut self, node_id: NodeId, topic_name: &str, qos: QoSProfile, ) -> Result<EmbeddedPublisher<M>, NodeError>

Phase 189.M1 — create a typed publisher bound to a node’s session. Backs node.publisher(t).typed::<M>().build() on the executor-borrowing Node; the returned handle is owned and outlives the Node.

Source

pub fn create_publisher_raw_on( &mut self, node_id: NodeId, topic_name: &str, type_name: &str, type_hash: &str, qos: QoSProfile, ) -> Result<EmbeddedRawPublisher, NodeError>

Phase 189.M1 — create a generic (type-erased) publisher bound to a node’s session. Backs node.publisher(t).generic(ty, hash).build(); the bridge re-publishes through this handle on the dest session.

Source

pub fn install_wake_signal_on_primary(&mut self)

Phase 124.B.1 — install the executor’s wake callback onto the primary session. Best-effort: backends that don’t override Session::set_wake_callback (poll-only XRCE, bare-metal) ignore the call and continue to be drained on the executor’s deadline-bound cv-wait boundary.

pub for issue 1385, and for the same reason set_primary_identity is: the three Executor::open* paths call this automatically, and a binding that builds its executor over a session it BORROWS (from_session_ptr_in) reaches none of them, so it has to call it itself. rclc_executor_init is that binding, and until 1385 it did not — leaving has_async_wake false for the life of every C executor on every backend.

Pairs with clear_wake_signal, which the executor runs from close() and from drop. A caller that installs owes nothing further: the teardown is the executor’s.

phase-436 — the backend’s slot holds a WakeFanout and this executor JOINS it, so every executor over one session is woken by an arrival: the boot executor and each spawned tier (open_with_session* now installs too), or several C executors over one support context. Idempotent. If the fan-out pool is exhausted the executor installs its own context directly, as before; if the entry’s member table is full it joins nothing and reads as having no async wake, so it never parks on a wake nobody will signal.

Source

pub fn with_node_try<R>( &mut self, id: NodeId, f: impl FnOnce(&mut NodeHandle<'_>) -> Result<R, NodeError>, ) -> Result<R, NodeError>

Phase 104.C.3.2 — scoped Node-handle access. The closure receives a NodeHandle bound to the requested NodeId’s session + identity. Use the standard Node::create_publisher, create_subscription, etc. APIs inside.

rclcpp-aligned bridge pattern:

ⓘ
let node_in = exec.node_builder("ingress").rmw("zenoh").build()?;
let node_out = exec.node_builder("egress").rmw("xrce").build()?;

let pub_out = exec.with_node(node_out, |n| {
    n.create_publisher::<Int32>("/fwd")
})??;

exec.with_node(node_in, |n| {
    n.create_subscription_buffered::<Int32, _, 1024>(
        "/src", qos(), move |m| { let _ = pub_out.publish(m); }
    )
})??;

The closure can return any type; double-? unwraps the outer Result<R, NodeError> from with_node and the inner result returned by the closure. Phase 104.C.3.3.d — flat-Result variant of with_node. When the closure already returns Result<R, NodeError>, this avoids the double-?:

ⓘ
// Without `with_node_try`:
let pub_ = exec.with_node(id, |n| n.create_publisher(...))??;

// With `with_node_try`:
let pub_ = exec.with_node_try(id, |n| n.create_publisher(...))?;
Source

pub fn with_node<R>( &mut self, id: NodeId, f: impl FnOnce(&mut NodeHandle<'_>) -> R, ) -> Result<R, NodeError>

Source

pub fn node_id_by_name(&self, name: &str, namespace: &str) -> Option<NodeId>

Find a registered executor node by final name and namespace.

Source

pub fn create_node(&mut self, name: &str) -> Result<Node<'_, 's>, NodeError>

Create a node on this executor.

Registers the node in the executor’s table, deduplicating on (name, namespace) — phase-376 W5/B1.

§This is rclrs::Executor::create_node

phase-427 W10 (RFC-0089 “Context and init, settled”). Same name, same place in the sequence — Context → executor → node — so a ported rclrs main lands here unchanged:

ⓘ
let context = nros::Context::default_from_env()?;
let mut executor = context.create_executor()?;
let mut node = executor.create_node("talker")?;

Two differences from upstream, both structural and both named in the ledger row (rust:Executor::create_node): ours takes &mut self, because the node is registered in the executor’s compile-time-sized table rather than allocated, and it hands back a borrowing NodeHandle rather than an Arc<Node>, because there is no allocator. Neither is silent — the compiler names both.

The name comes from HERE, not from ExecutorConfig::node_name, whose builder spelling is deprecated for exactly this reason: one executor holds several named nodes (RFC-0047), so a name on the session config can only ever describe the first of them.

Until 2026-08-24 this path registered NOTHING. It built a NodeHandle and returned it, so node_id_by_name could not find a node the caller had just created, and two calls with one name handed out two handles the executor had never heard of. create_node_on_with_domain had the dedup (phase-267 added it there when N bridge endpoints overflowed the table); the plain path never got it.

That is a prerequisite for the create_node vtable slot, whose contract is that the runtime calls it ONCE per distinct (name, namespace): without a registry to check, the runtime would call it once per create_node and every backend would need its own dedup — which is the registry the slot exists to delete (zenoh’s ensure_node_liveliness linear-scans per_node_liveliness for exactly this reason).

The table is bounded by MAX_NODES (NROS_EXECUTOR_MAX_NODES, default 4), so a caller creating a fifth DISTINCT node now gets NodeError::NodeTableFull where it previously got a handle. That is the bound doing its job: a node the executor does not know about cannot carry a sched context, a QoS override, or a graph identity. Repeated calls with the SAME name are free.

Source

pub fn create_node_on( &mut self, name: &str, rmw: &str, ) -> Result<Node<'_, 's>, NodeError>

Phase 128.F.2 — bridge-mode node factory. Registers a Node bound to the named RMW backend by opening (or reusing) an extra session via node_builder().rmw(rmw).build(), then returns a NodeHandle borrowing that session. Use when the binary intentionally links more than one backend and a Node must speak a specific one.

The single-backend common case should keep using create_node — this entry costs an extra session lookup and serves no purpose when only one backend is registered.

Source

pub fn create_node_on_with_domain( &mut self, name: &str, rmw: &str, domain_id: Option<u32>, locator: Option<&str>, ) -> Result<Node<'_, 's>, NodeError>

Like create_node_on but pins the extra session’s domain id. Required for a multi-domain config-driven bridge: an extra RMW session’s participant domain follows the node builder’s domain_id (resolve_session_slot → domain_id.unwrap_or(0)), NOT the SessionSpec’s — so without this an egress on a non-zero domain silently opens on domain 0 and never matches its receiver (phase-267 issue 0109). None domain preserves the legacy domain-0 default. locator pins the extra session’s address — REQUIRED for an agent-based backend (xrce: the Micro-XRCE-DDS Agent addr) whose session can’t be opened locator-less; None keeps the rmw-default (cyclonedds is domain-discovered, no locator).

Source

pub fn close(&mut self) -> Result<(), NodeError>

Close the underlying session, running the shutdown hooks around it.

Issue 0790. The order is the feature:

  1. every registered PRE-shutdown hook, while the session is still open and every entity still works — this is where a node publishes a final state, answers a last request, parks an actuator or releases a bus;
  2. the session close;
  3. every registered ON-shutdown hook.

A hook runs EXACTLY ONCE: each phase table is emptied before its first hook is invoked, so a second close() — or the Drop sweep after one — finds nothing left to run. Step 2 runs even if a pre-shutdown hook was registered and step 3 even if the close failed — a hook cannot strand the session, and a dead session must not strand the hooks.

§This is a CLEAN-STOP facility and nothing more

A watchdog reset, a hard fault or a panic does not come through here, so these hooks do not run then. Nothing in a fixed static table can promise otherwise, and an API that implied it would be worse than none: hardware that must be safe across an abnormal stop needs a hardware answer (a pull-down, a watchdog-driven output disable), not a callback.

Source

pub unsafe fn add_pre_shutdown_callback( &mut self, callback: ShutdownCallbackFn, context: *mut c_void, ) -> Result<ShutdownCallbackHandle, NodeError>

Register a hook to run BEFORE the session is closed (issue 0790).

rclcpp’s Context::add_pre_shutdown_callback. Returns the handle Self::remove_pre_shutdown_callback takes, or NodeError::ShutdownCallbacksFull when the phase table is full — raise NROS_EXECUTOR_MAX_SHUTDOWN_CBS (default 2) at build time.

§Safety

callback must be safe to invoke exactly once with context, and context must stay valid until the hook runs or is removed. The hook runs on whichever task calls Self::close (or drops the executor), which is not necessarily the task that registered it.

Source

pub unsafe fn add_on_shutdown_callback( &mut self, callback: ShutdownCallbackFn, context: *mut c_void, ) -> Result<ShutdownCallbackHandle, NodeError>

Register a hook to run AFTER the session is closed (issue 0790).

rclcpp’s Context::add_on_shutdown_callback / rclcpp::on_shutdown. Entities are gone by the time it runs, so anything that needs the wire belongs in Self::add_pre_shutdown_callback instead.

§Safety

Same contract as Self::add_pre_shutdown_callback.

Source

pub fn remove_pre_shutdown_callback( &mut self, handle: ShutdownCallbackHandle, ) -> bool

Remove a pre-shutdown hook. true if handle named a live one.

rclcpp’s Context::remove_pre_shutdown_callback, and bool for the same reason: “it was not there” is an ordinary answer (the hook may already have run), not an error. A handle issued for the OTHER phase returns false and removes nothing — that is what the phase tag in ShutdownCallbackHandle buys.

Source

pub fn remove_on_shutdown_callback( &mut self, handle: ShutdownCallbackHandle, ) -> bool

Remove an on-shutdown hook. See Self::remove_pre_shutdown_callback.

Source

pub fn shutdown_callback_count(&self, phase: ShutdownPhase) -> usize

How many hooks are currently registered for phase. Diagnostic / test surface, and what a “did my registration land?” assertion reads.

Source

pub fn register_dispatch_slot( &mut self, state: *mut c_void, on_callback: unsafe extern "C" fn(*mut c_void, *const u8, usize, *mut c_void), ) -> Result<(), ()>

Phase 216 follow-up — register a per-Node dispatch trampoline.

The board-side Entry pkg (or the macro-emitted register_dispatch(executor) wrapper, once wired) calls this once per deployed Node pkg, handing in the __nros_node_<pkg>_on_callback symbol + the Node’s per-pkg state blob. Executor::dispatch_callback then linear-scans the registered slots when the dispatch task hands off a SignaledCallback.

Returns Err(()) when the registry is full (MAX_NODES entries — raise via NROS_EXECUTOR_MAX_NODES at build time).

§Safety

state must outlive the executor (the typical shape is a *mut State produced by nros::__private_node_state_into_raw from the macro-emitted i(); that pointer’s lifetime IS the Executor’s by construction). on_callback must be safe to invoke with (state, cb_id_ptr, cb_id_len, ctx) matching the per-Node __nros_node_<pkg>_on_callback ABI emitted by the nros::node!() macro (Phase 216.A.5).

Source

pub fn dispatch_slot_count(&self) -> usize

Phase 216 follow-up — current registered dispatch-slot count. Diagnostic / test surface.

Source

pub unsafe fn enroll_component( &mut self, state: *mut c_void, tick: unsafe extern "C" fn(*mut c_void, *mut c_void), drop: unsafe extern "C" fn(*mut c_void), ) -> Result<(), ()>

Phase 258 (Track 2, 2a) — enroll a component into the executor-owned tick registry. Called by nros’s install/register_node_borrowed after it builds the Arc<ComponentCell>: state is the leaked Arc<ComponentCell> (the slot takes ownership), tick/drop are the nros-side trampolines (see ComponentSlot). The slot’s tick runs at the tail of every spin_once; its drop runs once on Executor::drop.

Returns Err(()) when the registry is full (MAX_NODES — raise via NROS_EXECUTOR_MAX_NODES at build time). On error the caller still owns state (the slot was not stored) and must drop it.

§Safety

state must be a *mut produced by leaking the component cell the tick/drop trampolines expect (an Arc<ComponentCell> via Arc::into_raw in the canonical nros caller), and must remain valid until the matching drop runs. tick must be safe to invoke with (state, exec_ctx = *mut Executor) each spin; drop must be safe to invoke exactly once with state.

Source

pub fn component_slot_count(&self) -> usize

Phase 258 (Track 2, 2a) — current enrolled component-slot count. Diagnostic / test surface.

Source

pub fn enrolled_component_states( &self, ) -> impl Iterator<Item = *mut c_void> + '_

issue #140 — the enrolled components’ opaque state pointers, in enroll order. Each is the leaked Arc<ComponentCell> enroll_component was handed (see ComponentSlot::state); the nros layer re-borrows them to fold per-component dispatch counters into observed_callback_counts — install-seam components (register_node_borrowed) live ONLY here, not in ExecutorNodeRuntime::components, so the hosted-spin counts read zero without this surface.

Source

pub fn dispatch_callback(&mut self, cb_id: &str, ctx: *mut c_void)

Phase 216 final dispatch hook — stable entry point the framework’s dispatch task (RTIC __nros_run / Embassy __nros_run_task) calls for each SignaledCallback envelope it dequeues from the board-side SPSC / Embassy channel.

§Signature shape

nros-node sits below nros in the dep graph, so the typed nros::CallbackId<'_> / nros::CallbackCtx<'_> types referenced in the Phase 216 design notes cannot appear in the signature here. The macro emit translates the dequeued envelope to the layer-clean (cb_id: &str, ctx: *mut c_void) pair before calling this method; the per-Node on_callback trampoline ABI (Phase 216.A.5, __nros_node_<pkg>_on_callback(state, cb_id_ptr, cb_id_len, ctx)) uses the same untyped shape on the other side of the fence, so the round-trip stays type-consistent.

§Body — linear scan of the dispatch registry

Each registered DispatchSlot holds an __nros_node_<pkg>_on_callback fn pointer + the owning Node’s state blob. The macro-emitted trampoline body matches on CallbackId tags the Node declared and is a no-op for non-matching cb_ids — at most one Node per cb_id actually acts, the rest are cheap string-compare no-ops. This mirrors the strategy ExecutorNodeRuntime::dispatch_callback uses in packages/api/nros/src/node_runtime.rs:470.

§What’s NOT auto-wired today

The nros::node!() macro doesn’t yet emit a register_dispatch(executor) wrapper that pushes the per-pkg (state, on_callback) into this registry. Until that wiring lands (Phase 216 follow-up — see commit msg), downstream consumers (board’s init_hardware, or the codegen-emitted run_plan) must call Executor::register_dispatch_slot explicitly with the __nros_node_<pkg>_on_callback symbol + a state blob from the macro-emitted i().

Source

pub fn session(&self) -> &CffiSession

Get a reference to the underlying session.

Source

pub fn session_mut(&mut self) -> &mut CffiSession

Get a mutable reference to the underlying session.

Source

pub fn ping(&mut self, timeout_ms: i32) -> Result<(), NodeError>

Phase 124.F.3 — session-level connectivity probe. Wire-level round-trip “is the peer / agent / router still reachable?” — cheaper than the service-availability probe (no discovery state required).

Returns Ok(()) on reply within timeout_ms, Err(NodeError::Transport(Timeout)) on no reply, Err(NodeError::Transport(Unsupported)) when the active backend can’t probe.

Mirrors micro-ROS’s rmw_uros_ping_agent. Useful for reconnect-on-link-loss patterns: bare-metal code can call ping(100) periodically and tear down / re-open the session on timeout.

Source

pub fn get_node_names( &mut self, visit: &mut dyn FnMut(&str, &str, Option<&str>) -> bool, ) -> Result<(), NodeError>

phase-381 W4 — every node on the graph, with its namespace.

visit(name, namespace, enclave) is called once per node and returns false to stop early. A VISITOR rather than a returned collection because there is no allocator at this layer and the graph has no bound the caller can know; peak extra memory is one entry. Every string is BORROWED for the duration of the call.

enclave is None where the backend does not track one, which is what lets one call answer both rmw_get_node_names and rmw_get_node_names_with_enclaves.

This reports what has already been DISCOVERED, and never blocks. The first call after startup legitimately sees a partial graph — the backend keeps a standing query fed by spin, so the view fills in over successive calls. Code that waits for a peer should poll, not call once and conclude. An empty result is “nobody seen yet”, never “nobody exists”.

Err(Transport(Unsupported)) from a backend with no graph — distinct from an empty graph, deliberately.

Source

pub fn get_topic_names_and_types( &mut self, visit: &mut dyn FnMut(&str, &[&str]) -> bool, ) -> Result<(), NodeError>

phase-381 W4 — every topic on the graph, with the types on it.

visit(topic_name, types) is called once per distinct TOPIC — a topic carrying two types is one call with two entries, not two calls. types may legitimately be empty on a partially discovered graph: reporting the name without a type beats dropping it.

Same discovery caveat as Self::get_node_names.

Source

pub fn get_service_names_and_types( &mut self, visit: &mut dyn FnMut(&str, &[&str]) -> bool, ) -> Result<(), NodeError>

phase-381 W4 — every service on the graph, with its types. As Self::get_topic_names_and_types, over servers and clients.

Source

pub fn count_publishers(&mut self, topic_name: &str) -> Result<usize, NodeError>

phase-381 W4 — how many publishers are visible on topic_name.

topic_name is a ROS name ("/chatter"). A count reflects what has been DISCOVERED, so it can be low right after startup and is never a proof of absence — see Self::get_node_names.

Source

pub fn count_subscribers( &mut self, topic_name: &str, ) -> Result<usize, NodeError>

phase-381 W4 — how many subscribers are visible on topic_name. See Self::count_publishers for the caveats.

Source

pub fn get_publisher_names_and_types_by_node( &mut self, node_name: &str, node_namespace: &str, visit: &mut dyn FnMut(&str, &[&str]) -> bool, ) -> Result<(), NodeError>

phase-381 W4 — what one named node PUBLISHES, with the types.

visit(topic_name, types) per distinct topic. A node the graph has not discovered yields no visits, which is not an error — see Self::get_node_names for why an empty answer means “not seen yet”.

Source

pub fn get_subscription_names_and_types_by_node( &mut self, node_name: &str, node_namespace: &str, visit: &mut dyn FnMut(&str, &[&str]) -> bool, ) -> Result<(), NodeError>

phase-381 W4 — what one named node SUBSCRIBES to, with the types.

subscription, not subscriber — this is rclrs’s spelling (get_subscription_names_and_types_by_node), and the Rust surface takes its vocabulary from rclrs so a user porting Rust ROS 2 code types what they already know. The C surface says subscriber because rcl does, and the vtable slot says subscriber because upstream rmw does. Three layers, three upstreams, one word each — not drift. Issue 0788 owns the wider verb sweep.

Source

pub fn get_service_names_and_types_by_node( &mut self, node_name: &str, node_namespace: &str, visit: &mut dyn FnMut(&str, &[&str]) -> bool, ) -> Result<(), NodeError>

phase-381 W4 — what services one named node SERVES, with the types.

Source

pub fn get_client_names_and_types_by_node( &mut self, node_name: &str, node_namespace: &str, visit: &mut dyn FnMut(&str, &[&str]) -> bool, ) -> Result<(), NodeError>

phase-381 W4 — what services one named node CALLS, with the types.

Source

pub fn get_publishers_info_by_topic( &mut self, topic_name: &str, visit: &mut dyn FnMut(&GraphEndpointInfo<'_>) -> bool, ) -> Result<(), NodeError>

phase-381 W4 — the publishers on topic_name, one visit each.

Each GraphEndpointInfo BORROWS its strings for the duration of the visit; copy anything kept.

It carries no QoS. The granted profile is what would answer “why is nothing arriving”, and no backend can read one back yet — reporting the remote’s DECLARED profile instead would be a confident wrong answer, so the field is absent rather than misleading.

Source

pub fn get_subscriptions_info_by_topic( &mut self, topic_name: &str, visit: &mut dyn FnMut(&GraphEndpointInfo<'_>) -> bool, ) -> Result<(), NodeError>

phase-381 W4 — the subscriptions on topic_name, one visit each. See Self::get_publishers_info_by_topic.

Source

pub fn wait_for_publishers( &mut self, topic_name: &str, count: usize, timeout: Duration, ) -> Result<bool, NodeError>

phase-444 — block until at least count publishers are visible on topic_name, or timeout elapses. Upstream’s rcl_wait_for_publishers.

Returns Ok(true) when the graph showed at least count inside the budget, Ok(false) on timeout. The startup-ordering primitive one entity kind over from EmbeddedServiceClient::wait_for_service, and built the same way: a spin on a discovery read, so other subscriptions and timers keep making progress while it waits.

This is the one blocking member of the graph family, and it does not weaken the family’s contract — it composes it. The query underneath (count_publishers) still reports only what has been DISCOVERED and still never blocks; what waits is this loop, and it waits by POLLING that query. RFC-0021 is the rule that governs it, and that rule does not forbid blocking: it requires a blocking helper to take an executor and drive it, which is what makes the timeout reliable on a single-threaded transport and what makes the reentrancy visible at the call site.

Unsupported is propagated IMMEDIATELY, not waited out — and this is the one place it deliberately differs from wait_for_service, which treats a backend that cannot answer as “keep waiting” and reports Ok(false) at the deadline (issue 1087). That is right for a service probe, where not-yet-answerable is a transient. It is wrong here: a backend with no graph (XRCE) can never answer, so spending the budget to return Ok(false) would report “no publishers appeared” for a backend that cannot see publishers at all — collapsing “cannot tell you” into “not there”, which is exactly the distinction RFC-0036 exists to keep and which every other method in this family promises to keep.

count == 0 is satisfied immediately and without asking the backend — including on a backend with no graph, because “at least zero” is true of every graph and needs no discovery to establish.

Source

pub fn wait_for_subscribers( &mut self, topic_name: &str, count: usize, timeout: Duration, ) -> Result<bool, NodeError>

phase-444 — block until at least count subscribers are visible on topic_name. Upstream’s rcl_wait_for_subscribers. See wait_for_publishers for the contract, and for why Unsupported returns rather than waits.

Source

pub unsafe fn action_client_core_mut( &mut self, entry_index: usize, ) -> Option<&mut ActionClientCore>

Get a mutable reference to an action client core in the arena by entry index.

§Safety

The caller must ensure that entry_index refers to an ActionClientRawArenaEntry.

Source

pub unsafe fn service_client_entry_mut( &mut self, entry_index: usize, ) -> Option<&mut ServiceClientRawArenaEntry<{ crate::config::DEFAULT_RX_BUF_SIZE }>>

Get a mutable reference to a service-client arena entry (Phase 82).

Returns None if entry_index doesn’t refer to a service client entry. The default reply buffer size is assumed because the C API always uses the default — the entry was registered via register_service_client_raw_sized::<DEFAULT_RX_BUF_SIZE>.

§Safety

entry_index must refer to a ServiceClientRawArenaEntry.

Source

pub unsafe fn subscription_handle( &self, entry_index: usize, ) -> Option<&RmwSubscriber>

The RmwSubscriber of an arena-registered subscription, by entry index.

None when entry_index names no entry, or names one that is not a subscription.

§Safety

entry_index must be an index this executor handed out. The borrow is valid only while the arena is not re-entered.

Source

pub unsafe fn service_server_handle( &self, entry_index: usize, ) -> Option<&RmwServiceServer>

The RmwServiceServer of an arena-registered service, by entry index.

§Safety

See Executor::subscription_handle.

Source

pub unsafe fn service_client_handle( &self, entry_index: usize, ) -> Option<&RmwServiceClient>

The RmwServiceClient of an arena-registered client, by entry index.

§Safety

See Executor::subscription_handle.

Source

pub fn set_trigger(&mut self, trigger: Trigger)

Set the executor-level trigger condition.

Controls which handles must be ready before spin_once dispatches callbacks. Defaults to Trigger::AnyReady.

Source

pub fn set_semantics(&mut self, semantics: ExecutorSemantics)

Set the executor data communication semantics.

Choose between Direct (process in place) and LET (snapshot-then-process) semantics. See ExecutorSemantics.

Source

pub fn set_invocation(&mut self, id: HandleId, mode: InvocationMode)

Set the invocation mode for a specific handle.

Controls whether the callback fires on every spin (Always) or only when new data arrives (OnNewData, the default).

Source

pub fn arena_used(&self) -> usize

Arena bytes claimed by registered entities so far.

EXACT, not a worst case: the arena is a bump allocator and every arena_alloc charges size_of::<T>(), so nothing is reserved per slot. ARENA_SIZE itself is derived the other way — every slot budgeted at the ActionClient worst case — which is issue 0900, and this accessor is how an image can find out what it actually needs.

Source

pub fn arena_capacity(&self) -> usize

Total arena bytes this executor was given.

The arena is a borrowed slice; where its storage lives is the caller’s choice, not a property of this type. Three answers are real — a .bss static (the C/C++ entries, and the alloc constructors since phase-392 W6), the heap (those constructors’ fallback), and a stack-declared rclc_executor_t. See super::backing and report_arena_headroom.

Source

pub fn arena_released(&self) -> usize

Issue 1496 — bytes released back to the arena and not yet reused. Bytes handed back to the bump pointer are not counted: arena_used already went down by them.

Source

pub fn arena_holes(&self) -> usize

Issue 1667 — how many separate holes the free list holds. For tests and diagnostics.

Source

pub unsafe fn release_subscription(&mut self, handle: HandleId) -> bool

Issue 1631 — release a SUBSCRIPTION entry, whatever registration shape made it (typed, raw, in-place, buffered, with-info). See release_entry (crate-private): the entry is dropped, so its subscriber leaves the graph; its slot and arena bytes are reused by the next registration. false, touching nothing, when handle is not a live subscription.

§Safety

No copy of handle may be used afterwards — the slot is handed to the next registration, which may be of any kind.

Source

pub unsafe fn release_timer(&mut self, handle: HandleId) -> bool

Issue 1631 — release a TIMER entry; the timer’s callback (and anything its closure captured) is dropped and never fires again. As release_subscription.

§Safety

As release_subscription.

Source

pub unsafe fn release_service(&mut self, handle: HandleId) -> bool

Issue 1631 — release a SERVICE SERVER entry; the server leaves the graph. As release_subscription.

§Safety

As release_subscription.

Source

pub unsafe fn release_service_client(&mut self, handle: HandleId) -> bool

Issue 1631 — release a SERVICE CLIENT entry; the client leaves the graph and any reply it was waiting for is abandoned with it. As release_subscription.

§Safety

As release_subscription.

Source

pub unsafe fn release_guard_condition(&mut self, handle: HandleId) -> bool

Issue 1667 — release a GUARD CONDITION entry: its callback is dropped and spin_once stops polling it; the slot and its arena bytes go to the next registration.

Unlike the other release verbs this one has a contract a generation cannot discharge. The GuardCondition trigger handle holds a pointer to the flag INSIDE the entry, and it is Clone + Send, so a clone on another thread (an ISR, a worker) would write into whatever reuses those bytes. The C++ GuardCondition is non-copyable and the one holder of its trigger handle, which is why its destructor can call this.

§Safety

No GuardCondition trigger handle for this entry — the one registration returned, or any clone of it — may be used afterwards.

Source

pub fn resolve_handle(&self, id: HandleId) -> Option<usize>

phase-476 W0 (issue 1667) — the slot id names, if and only if id was issued for the entry that occupies it NOW. A handle whose entry was released, or whose slot has since been taken by another registration of any kind, answers None, never the next occupant.

Source

pub fn set_entry_owner(&mut self, id: HandleId, node: NodeId) -> bool

phase-476 W0 — record that node registered the entry id names, so release_node can find it. false when id is stale.

Source

pub unsafe fn release_node(&mut self, node: NodeId) -> usize

phase-476 W0 — release every entry node registered, whatever its kind. Returns how many were released.

This is what a node’s destruction calls. A C++ callback’s capture is copied into the arena and commonly holds the node’s this, so an entry that outlived its node would dispatch into freed memory.

§Safety

No handle to any of those entries may be used afterwards except through a lookup that checks the generation (resolve_handle), which every public lookup on this type does.

Source

pub fn register_subscription_buffered_raw_info_on<F, const RX_BUF: usize>( &mut self, node_id: NodeId, topic_name: &str, type_name: &str, type_hash: &str, qos: QoSProfile, callback: F, ) -> Result<HandleId, NodeError>
where F: FnMut(&[u8], &RawMessageInfo<'_>) + 'static,

Register a raw (type-erased) buffered subscription whose callback also receives a RawMessageInfo carrying the sample’s wire attachment (Phase 189.M1).

Backs the node.subscription(t).generic(..).message_info().build(cb) builder — the cross-RMW bridge reads the bridge_origin tag from info.attachment() for echo suppression. One sample per spin_once; the attachment is staged in a flat per-entry buffer (cap RAW_INFO_ATT_CAP).

Source

pub fn add_arena_subscription_callback<F, const RX_BUF: usize>( &mut self, handle: RmwSubscriber, qos: QoSProfile, callback: F, ) -> Result<HandleId, NodeError>
where F: FnMut(&[u8]) + 'static,

Register a raw byte-shaped callback against a pre-built RmwSubscriber handle.

Backend-agnostic primitive — the caller is responsible for obtaining the handle by whatever route the active backend supports:

  • Generic ROS-typed flow: call Session::create_subscription on self.session_mut() with a TopicInfo. The node_mut(id).subscription(t).generic(ty, hash) builder is the convenience wrapper for this path.
  • Backend-specific flow (e.g. uORB needs &'static orb_metadata): reach into the concrete session via Self::session_mut and call its backend-specific create method, then hand the handle here. nros-px4::uorb::create_subscription_with_callback is the example.

The arena-store + vtable wiring is identical to register_subscription_buffered_raw; the only thing that varies is where the handle came from. Callback fires on every message delivery during spin_once; bytes are passed as &[u8].

Source

pub fn register_service<Svc, F>( &mut self, service_name: &str, callback: F, ) -> Result<HandleId, NodeError>
where Svc: RosService + 'static, Svc::Request: MessageForRmw, Svc::Reply: MessageForRmw, F: FnMut(&Svc::Request) -> Svc::Reply + 'static,

Register a service callback with the default buffer size.

The callback is stored in the arena and invoked during spin_once().

Source

pub fn register_service_sized<Svc, F, const REQ_BUF: usize, const REPLY_BUF: usize>( &mut self, service_name: &str, callback: F, ) -> Result<HandleId, NodeError>
where Svc: RosService + 'static, Svc::Request: MessageForRmw, Svc::Reply: MessageForRmw, F: FnMut(&Svc::Request) -> Svc::Reply + 'static,

Register a service callback with custom request/reply buffer sizes.

Source

pub fn register_service_sized_on<Svc, F, const REQ_BUF: usize, const REPLY_BUF: usize>( &mut self, node_id: NodeId, service_name: &str, qos: QoSProfile, callback: F, ) -> Result<HandleId, NodeError>
where Svc: RosService + 'static, Svc::Request: MessageForRmw, Svc::Reply: MessageForRmw, F: FnMut(&Svc::Request) -> Svc::Reply + 'static,

Phase 104.C.3.3.a — Node-aware variant of register_service_sized.

Source

pub fn register_service_on<Svc, F>( &mut self, node_id: NodeId, service_name: &str, callback: F, ) -> Result<HandleId, NodeError>
where Svc: RosService + 'static, Svc::Request: MessageForRmw, Svc::Reply: MessageForRmw, F: FnMut(&Svc::Request) -> Svc::Reply + 'static,

Phase 104.C.3.3.a — Node-aware variant of register_service.

Source

pub fn register_timer<F>( &mut self, period: TimerDuration, callback: F, ) -> Result<HandleId, NodeError>
where F: FnMut() + 'static,

Register a repeating timer callback.

The callback fires every period milliseconds during spin_once(). The timer delta is approximated by the timeout_ms argument to spin_once.

Source

pub fn register_timer_on_clock<F>( &mut self, period: TimerDuration, source: TimerClockSource, callback: F, ) -> Result<HandleId, NodeError>
where F: FnMut() + 'static,

Register a repeating timer driven by a CLOCK rather than by the spin delta — phase-425 W4, the shape rclcpp spells create_timer(node, clock, period, cb).

register_timer is the wall timer: it consumes the executor’s monotonic spin delta and no simulator can slow it down. This one reads source on every poll and advances by the difference, so a TimerClockSource::Ros timer follows /clock — it stops while the simulator is paused, halves with a bag replayed at 0.5x, and restarts its period on a backwards jump instead of stalling for the length of it.

With no /clock source installed, a Ros timer reads system time and behaves like a wall timer with NTP steps, which is the same fallback rclcpp::Clock has: a node written for simulation still runs standalone.

The node-level spellings of this are Node::create_timer_on_clock and the declarative DeclaredNode::create_timer_on_clock (phase-430 W4); all of them reach register_timer_entry, which is the only place a timer entry is built.

Source

pub fn register_timer_oneshot<F>( &mut self, delay: TimerDuration, callback: F, ) -> Result<HandleId, NodeError>
where F: FnMut() + 'static,

Register a one-shot timer callback.

The callback fires once after delay milliseconds, then becomes inert.

Source

pub fn register_timer_c_capturing( &mut self, node_id: Option<NodeId>, period: TimerDuration, source: TimerClockSource, callback: unsafe extern "C" fn(*mut c_void), capture: Option<&[u8]>, capture_drop: Option<unsafe extern "C" fn(*mut c_void)>, ) -> Result<HandleId, NodeError>

phase-476 W2 — register a timer whose callback is a C function pointer and whose CAPTURE the arena holds: the nros-cpp road for a C++ lambda, the timer twin of add_arena_subscription_c_callback_with_capture.

capture’s bytes are copied into the timer entry’s trailing region and callback is called with a pointer to that copy, so the caller keeps nothing — which is what lets the C++ side hand back a two-word handle instead of a heap cell the node had to keep alive. Releasing the timer releases the capture with it.

source picks the clock: TimerClockSource::Steady is the wall timer that advances with the spin delta.

capture_drop, when given, is called with the same pointer when the entry is dropped (released, or the executor torn down), BEFORE its bytes go back to the arena. A C++ capture is a callable that may own something (a std::shared_ptr, a std::string), and its bytes were MOVED here by copy, so this is the only place its destructor can run.

Source

pub fn register_timer_on<F>( &mut self, node_id: Option<NodeId>, period: TimerDuration, callback: F, group: Option<&str>, ) -> Result<HandleId, NodeError>
where F: FnMut() + 'static,

Phase 273 (RFC-0047) — register a repeating timer callback bound to a specific node and optional callback group. The group name is threaded to apply_node_default_sched so the seeded group_sched_table assigns the timer’s callback to the group’s SchedContext. When group is None the node’s default_sched applies (phase-272 behavior).

This is the executor-level primitive called by the Rust _in_group API (Node::create_timer_in_group) and the C/C++ group-aware timer FFI.

Source

pub fn add_arena_subscription_c_callback<const RX_BUF: usize>( &mut self, node_id: Option<NodeId>, topic_name: &str, type_name: &str, type_hash: &str, qos: QoSProfile, callback: RawSubscriptionCallback, context: *mut c_void, group: Option<&str>, rx_buffer_hint: usize, ) -> Result<HandleId, NodeError>

The kept C-FFI subscription core (Phase 189.M2.b): registers a raw RawSubscriptionCallback fn-ptr + context against an optional node’s session. The Rust ergonomic surface is the node.subscription(t) builder (closures); this is the single primitive the nros-c thin wrapper lowers to. node_id == None is the legacy single-node path.

Source

pub fn add_arena_subscription_c_callback_with_capture<const RX_BUF: usize>( &mut self, node_id: Option<NodeId>, topic_name: &str, type_name: &str, type_hash: &str, qos: QoSProfile, callback: RawSubscriptionCallback, context: *mut c_void, group: Option<&str>, rx_buffer_hint: usize, capture: Option<&[u8]>, capture_drop: Option<unsafe extern "C" fn(*mut c_void)>, ) -> Result<HandleId, NodeError>

The same registration, with the caller’s CAPTURE held by the arena — phase-456 W1.

context is one pointer, which carries a C++ [this] capture and not a [this, state] or a [obj, method]. When capture is Some, its bytes are copied into the arena and context is repointed at that copy, so the caller keeps nothing and nothing the caller owns is referenced after this returns. When it is None the behaviour is exactly the pre-existing one, which is why the plain entry point above is a wrapper rather than a second implementation.

phase-456 W8 changed two things here. The capture is a RUNTIME length — see Self::place_capture — so there is no constant the C++ side has to match and no BufferTooSmall reachable only by drifting from it. And this path now reaches the in-place dispatch that the Rust typed path has had since phase-231: RawSubscriptionCallback is (const uint8_t*, size_t, void*), so the backend’s borrowed receive slot IS the argument, and a C or C++ subscription on zenoh or XRCE claims no receive region at all.

Source

pub unsafe fn add_arena_subscription_c_typed_callback<const RX_BUF: usize>( &mut self, node_id: Option<NodeId>, topic_name: &str, type_name: &str, type_hash: &str, qos: QoSProfile, msg: NonNull<c_void>, deserialize: RawMessageDeserializeFn, callback: TypedSubscriptionCallback, context: *mut c_void, group: Option<&str>, rx_buffer_hint: usize, ) -> Result<HandleId, NodeError>

phase-417 W5.a (RFC-0089 stage 5) — the TYPED C-FFI subscription core: the same registration as Self::add_arena_subscription_c_callback, plus caller-owned message storage and the erased deserialiser for its type, so the callback receives a DESERIALISED message instead of CDR bytes.

This is the Rust half of rclc’s rclc_executor_add_subscription(executor, subscription, msg, callback, invocation). rclc has no allocator on that path either — it makes the CALLER own the storage, which is exactly what msg is here. The C ledger recorded our byte-oriented delivery as forced by “no allocator”; the compared surface operates under the same constraint, so the reason was false and the divergence was avoidable (issue 1022 / W-C3).

The raw path is untouched and stays: a byte-oriented subscriber is a legitimate thing to want, and this is additive.

§Contract on failure

If deserialize returns non-zero the callback is not invoked, the sample is dropped, try_process reports TransportError::DeserializationError (so it lands in SpinOnceResult::subscription_errors), and a rate-limited nros_log error names the length and the return code. A message that does not fit the caller’s storage arrives here as exactly that failure: both the bounded-string and bounded-sequence decoders refuse rather than truncate, so there is no silent short read.

§Safety

msg must point to storage of the type deserialize writes, valid for as long as the subscription is registered. Nothing here drops it.

Source

pub fn add_arena_subscription_c_info_callback<const RX_BUF: usize>( &mut self, node_id: Option<NodeId>, topic_name: &str, type_name: &str, type_hash: &str, qos: QoSProfile, callback: RawSubscriptionInfoCallback, context: *mut c_void, rx_buffer_hint: usize, ) -> Result<HandleId, NodeError>

Phase 189.M3.4 — register a raw C-fn-ptr subscription whose callback also receives the sample’s wire attachment (RawSubscriptionInfoCallback: (data, len, attachment, att_len, context)) — the C analog of the Rust node.subscription(t).generic(..).message_info() builder. Backs the C FFI rclc_executor_add_subscription_raw_with_info. Flat per-entry payload + attachment buffers (cap RAW_INFO_ATT_CAP); one sample per spin_once.

Source

pub fn register_service_raw( &mut self, service_name: &str, service_type: &str, service_hash: &str, callback: RawServiceCallback, context: *mut c_void, ) -> Result<HandleId, NodeError>

Register a raw (untyped) service callback.

Register a raw (untyped) service callback with the default buffer size.

The callback receives and produces CDR bytes without typed deserialization/serialization. Used by the C API wrapper.

Source

pub fn register_service_raw_sized<const REQ_BUF: usize, const REPLY_BUF: usize>( &mut self, service_name: &str, service_type: &str, service_hash: &str, qos: QoSProfile, callback: RawServiceCallback, context: *mut c_void, ) -> Result<HandleId, NodeError>

Register a raw (untyped) service callback with custom buffer sizes + QoS.

REQ_BUF and REPLY_BUF set the stack-allocated CDR buffers for the request and reply respectively. Increase for services with large payloads (e.g., parameter services). qos applies to both the request + reply endpoints (Phase 193.2c).

Source

pub fn register_service_raw_sized_on<const REQ_BUF: usize, const REPLY_BUF: usize>( &mut self, node_id: NodeId, service_name: &str, service_type: &str, service_hash: &str, qos: QoSProfile, callback: RawServiceCallback, context: *mut c_void, ) -> Result<HandleId, NodeError>

Phase 104.C.3.3.a — Node-aware variant of Self::register_service_raw_sized. C-FFI path.

Source

pub fn register_service_client_raw( &mut self, service_name: &str, service_type: &str, service_hash: &str, callback: Option<RawResponseCallback>, context: *mut c_void, ) -> Result<HandleId, NodeError>

Register a raw (untyped) service client with the default reply buffer size.

The client is owned by the executor’s arena. Each spin_once dispatch polls the in-flight reply slot via take_response_raw and fires the registered callback when the response arrives. Used by the C API thin wrapper — see Phase 82.

Source

pub fn register_service_client_raw_sized<const REPLY_BUF: usize>( &mut self, service_name: &str, service_type: &str, service_hash: &str, qos: QoSProfile, callback: Option<RawResponseCallback>, context: *mut c_void, ) -> Result<HandleId, NodeError>

Register a raw service client with a custom reply buffer size + QoS.

qos applies to the client’s request + reply endpoints (Phase 193.3b); defaults to QoSProfile::services_default via the convenience wrapper.

Source

pub fn register_service_client_raw_sized_on<const REPLY_BUF: usize>( &mut self, node_id: NodeId, service_name: &str, service_type: &str, service_hash: &str, qos: QoSProfile, callback: Option<RawResponseCallback>, context: *mut c_void, ) -> Result<HandleId, NodeError>

Phase 104.C.3.3.a — Node-aware variant of Self::register_service_client_raw_sized. Routes the client creation through the named Node’s session.

Source

pub fn register_guard_condition<F>( &mut self, callback: F, ) -> Result<(HandleId, GuardCondition), NodeError>
where F: FnMut() + 'static,

Register a guard condition with a callback, on no particular node.

Returns both the HandleId for trigger configuration and a GuardCondition for triggering from other threads.

This is the ours-only PRIMITIVE, not the verb a user reaches for. phase-417 W4.e settled that the NODE owns a guard condition in all three languages, so the spelling an application writes is Node::create_guard_condition — which is this function with a node bound, so the guard inherits that node’s SchedContext like every other entity the node creates. This one stays because the executor is what owns the arena the flag lives in, and because an image with no node at all (a bare Executor::open plus a wake source) still has to be able to make one.

Source

pub fn register_guard_condition_on<F>( &mut self, node_id: Option<NodeId>, callback: F, ) -> Result<(HandleId, GuardCondition), NodeError>
where F: FnMut() + 'static,

The node-bound form of register_guard_condition — phase-417 W4.e.

node_id == None is the legacy nodeless path and behaves exactly as register_guard_condition always did. With a node, the registered slot takes that node’s default SchedContext, which is what “the node owns it” has to mean for a callback that shares one executor with every other tier in the image (RFC-0047).

Source

pub fn register_graph_change_guard_on<F>( &mut self, node_id: Option<NodeId>, callback: F, ) -> Result<(HandleId, GuardCondition), NodeError>
where F: FnMut() + 'static,

phase-467 Row 8 — a guard condition the BACKEND triggers when the ROS graph changes. Upstream’s rcl_node_get_graph_guard_condition, under this project’s guard-condition shape.

The verb is upstream’s, the shape is ours, and the difference is stated rather than silent. rcl_node_get_graph_guard_condition RETURNS a borrowed handle to a guard the node owns, which the caller then adds to a wait set. RFC-0018/0019 forbids returning an owned handle across the C seam, we decline wait sets (RFC-0002: one executor per RTOS task), and a guard condition here is created into CALLER storage with its callback bound at creation. So this CREATES one — exactly as register_guard_condition_on does — and differs from it only in who pulls the trigger.

Order matters and is the reason this is one verb rather than two. The backend is asked whether it HAS a graph before a handle slot is consumed, so a caller on XRCE or uORB gets Unsupported and an unchanged executor, never a guard condition nothing will ever fire.

Source

pub fn register_graph_change_guard<F>( &mut self, callback: F, ) -> Result<(HandleId, GuardCondition), NodeError>
where F: FnMut() + 'static,

The nodeless form of register_graph_change_guard_on, for an image with a session and no node — the same pairing register_guard_condition has.

Source

pub fn cancel_timer(&mut self, id: HandleId) -> Result<(), NodeError>

Cancel a timer. A cancelled timer will not fire but still accumulates elapsed time. The timer can be restarted with reset_timer().

Source

pub fn reset_timer(&mut self, id: HandleId) -> Result<(), NodeError>

Reset a timer. Clears the cancelled state and resets the elapsed time to zero, so the timer starts a fresh period.

Source

pub fn timer_is_canceled(&self, id: HandleId) -> bool

Check if a timer is cancelled.

Source

pub fn timer_period_ms(&self, id: HandleId) -> Option<u64>

Get the period of a timer in milliseconds (truncated — see Self::timer_period_us), or None if the handle is not a valid timer.

Source

pub fn timer_period_us(&self, id: HandleId) -> Option<u64>

Get the period of a timer in microseconds, or None if the handle is not a valid timer.

Source

pub fn exchange_timer_period_us( &mut self, id: HandleId, new_period_us: u64, ) -> Option<u64>

Swap a registered timer’s period, in microseconds, returning the one it had — or None if the handle is not a valid timer.

TimerHeader::period_us is the number arena::timer_try_process compares elapsed_us against, so this writes the period the DISPATCHER honours rather than a second copy of it (RFC-0019). The C entry point rcl_timer_exchange_period also keeps rcl_timer_t::period_ns in step, because that field is the registration input a later rclc_executor_add_timer would re-read.

elapsed_us is deliberately NOT rewound, which is rcl’s own behaviour: rcl_timer_exchange_period swaps the period and touches nothing else, so a timer shortened below its accumulated elapsed time becomes ready at once and one lengthened past it simply waits longer. A caller that wants the new period to start fresh calls reset_timer after it.

phase-417 W5.c — the Rust half of c:timer_exchange_period, the last timer row the C ledger filed as gap. The answer lives here and only the spelling lives in the wrapper, the same split timer_elapsed_us made.

Source

pub fn set_timer_overrun_policy( &mut self, id: HandleId, policy: TimerOverrunPolicy, ) -> Result<(), NodeError>

Set a timer’s overrun policy (issue #505). Timers default to TimerOverrunPolicy::Skip; switch to TimerOverrunPolicy::CatchUp for timers whose every activation is a unit of work that must not be lost.

Source

pub fn timer_overruns(&self, id: HandleId) -> Option<u32>

Periods this timer missed and dropped under TimerOverrunPolicy::Skip (issue #505), or None if the handle is not a valid timer.

Monotonic and saturating. A growing count is the on-target signal that a tier is not keeping up with its declared cadence — the symptom an external observer would otherwise have to infer by differencing timestamps.

Source

pub fn timer_elapsed_us(&self, id: HandleId) -> Option<u64>

Microseconds accumulated since this timer last fired, or None if the handle is not a valid timer.

This is TimerHeader::elapsed_us — the SAME counter arena::timer_try_process compares against the period and rewinds on every activation, so it is the executor’s own answer rather than a second one derived from a clock. None is not “zero elapsed”: a caller that cannot tell them apart reports a timer that has just fired and a handle that is not a timer identically.

phase-417 W5.c — added because the state was already here and nothing exposed it, which is why c:timer_get_time_since_last_call and c:timer_is_ready were both filed as gaps. The C forwarders (rcl_timer_get_time_since_last_call, nros_timer_is_ready) read this; RFC-0019 puts the answer in Rust and only the spelling in the wrapper.

Source

pub fn timer_is_ready(&self, id: HandleId) -> Option<bool>

Would this timer fire on the next dispatch pass? None if the handle is not a valid timer.

The predicate is arena::timer_try_process’s own guard, in the same order: a cancelled timer is never ready, a one-shot that already fired is never ready again, and otherwise readiness is elapsed >= period. Keeping the two in one shape is the point — a readiness answer that disagrees with the dispatcher is worse than no answer at all.

Source

pub fn timer_time_until_next_call_ns(&self, id: HandleId) -> Option<i64>

Nanoseconds until this timer next fires — NEGATIVE when it is overdue. None if the handle is not a valid timer.

phase-417 G6. THE computation, and it has to be: rcl’s own pair are next_call_time and next_call_time - now, so upstream cannot have them disagree either. Before this it lived in nros-c (remaining_period_ns + time_until_next_call_ns_of), and the C++ half of cpp:Timer::time_until_trigger would have been a second derivation of the same subtraction in a second crate — the shape CLAUDE.md’s “fix the CLASS, add ONE shared helper rather than a second spelling” rule names. nros-c forwards here now; so does nros-cpp.

The unit is nanoseconds and the RESOLUTION is microseconds. The arena’s timer accounting is microsecond-based (issue #505), so this is a microsecond quantity scaled by 1000. The unit is rcl’s; the resolution is ours.

Saturating rather than wrapping: period_us and elapsed_us are both u64, so their difference in nanoseconds can leave i64 in either direction, and a wrap would turn “overdue by a lot” into “due in a long time” — the sign is the whole answer here.

Source

pub fn session_io_failures(&self) -> u32

Drive I/O and dispatch registered callbacks once.

Three-phase execution:

  1. Readiness scan — query each handle’s has_data().
  2. Trigger evaluation — check if the executor-level trigger passes.
  3. Dispatch — invoke callbacks according to their InvocationMode.

Returns a SpinOnceResult with counts of processed items and errors.

§Arguments
  • timeout — upper bound on the I/O wait. Saturated at i32::MAX ms (~24 days) for the underlying transport call.

Phase 84.D7: unified on core::time::Duration. The previous timeout_ms: i32 signature had a latent footgun where spin_once(-1) silently froze timers while still polling I/O; Duration has no negative sentinel. Consecutive drive_io failures on the primary session (issue 0324).

0 means the last drive succeeded. A value that keeps climbing across spins is a session that is no longer doing I/O — router gone, lease expired, socket closed — which otherwise presents as a node that spins Ok(()) forever while publishing nowhere and firing no callbacks.

Transient failures happen, so a single non-zero reading is not a fault; a threshold (say, “more than a few consecutive spins”) is the useful signal. Extra (bridge / multi-domain) sessions are best-effort and are deliberately NOT counted here.

Source

pub fn session_io_healthy(&self) -> bool

Whether the primary session drove I/O successfully on the last spin.

Convenience over Self::session_io_failures for the common “is my transport alive?” check.

Source

pub fn spin_once(&mut self, timeout: Duration) -> SpinOnceResult

Drive I/O once, then dispatch EVERY callback that is ready — the tick primitive the rest of the family is built from.

OURS-ONLY (RFC-0089): rclcpp’s spin_once executes the NEXT ready item and has no timeout; rclpy’s is spin_once(node, timeout_sec). Ours is a blocking wait with a budget, which an RTOS task that must not spin-poll needs and which neither upstream has a verb for. What it DOES when work is ready is rclcpp’s spin_some — the whole ready set, one pass (RFC-0002 §3) — so spin_some is the drain verb and this is the waiting one.

timeout is the upper bound on the I/O wait. Since phase-436 W2 it is carried in MICROSECONDS all the way to the park (issue 1193 — an as_millis() here truncated from_micros(500) to 0, and a zero timeout selects the non-blocking path, so a sub-millisecond spin() was a 100 % CPU loop with no warning); the conversion to whatever the platform primitive accepts happens once, at the park, and rounds UP. Duration has no negative sentinel, which is what the pre-phase-84 timeout_ms: i32 signature used to silently mean “freeze the timers and poll I/O forever”.

Source

pub fn spin_forever(&mut self, opts: SpinOptions) -> !

Drive I/O and dispatch callbacks forever — the body of an RTOS task.

EXTENSION (RFC-0089). Not a rename of upstream’s spin: the ! is the point. A bare-metal entry has no shutdown source to return TO, and declaring that lets the compiler drop the epilogue and lets the caller write it as the last expression of a -> ! task body with no unreachable!(). spin is the verb that can end, and it is upstream’s.

phase-427 — was spin(Duration) -> !, which squatted on rclrs’s name with a different contract (a ported executor.spin(SpinOptions) line had to be renamed by hand, the one edit on the tutorial’s happy path that no real difference forced). spin_default() went with it: it named a 50 ms poll interval and had zero call sites, and the interval is now SpinOptions::poll_interval, defaulting to DEFAULT_SPIN_POLL_INTERVAL.

Envelope. Only poll_interval and stop_on_first_error can mean anything here; timeout, only_next and max_callbacks are ENDING conditions and this verb does not end. Passing one is a caller error and says so at LOG_ERR on entry rather than being quietly dropped — a discarded ending condition is precisely the “compiles and differs” case RFC-0089 exists to make loud, and a -> ! signature has no other channel. stop_on_first_error likewise cannot RETURN the error; it logs it and keeps spinning, because a task body has nowhere to put it.

ⓘ
let mut executor = Executor::open(&config)?;
executor.register_subscription::<Int32, _>("/topic", |msg| { /* ... */ })?;
executor.spin_forever(SpinOptions::default()); // never returns
Source

pub async fn spin_async(&mut self) -> !

Drive I/O and dispatch callbacks asynchronously.

Runs forever, yielding between poll cycles so that other async tasks (e.g., Promise) can make progress.

Uses only core::future — no external async runtime dependency.

ADOPT-BOUNDED against rclrs’s spin_async(SpinOptions) -> impl Future<Output = Vec<RclrsError>>:

  • It never returns and reports no errors — the output is !, and it takes no SpinOptions. To stop, select it against the thing you are waiting for (pattern 1 below).
  • It does not sleep on RMW I/O. Each cycle is spin_once(1 ms) and then a yield that wakes itself before returning Pending, so any runtime that honours wakes polls it again at once. It is CPU-bound by construction; the 1 ms spin_once budget is its only wait. Do not read async as idle.
§Usage patterns
ⓘ
// Pattern 1: select with a promise (embassy-futures)
use embassy_futures::select::{select, Either};
let promise = client.call(&req)?;
let Either::Second(reply) = select(executor.spin_async(), promise).await
    else { unreachable!() };

// Pattern 2: manual polling (no async runtime)
let mut promise = client.call(&req)?;
loop {
    executor.spin_once(core::time::Duration::from_millis(10));
    if let Ok(Some(r)) = promise.take() { break r; }
}
Source

pub fn spin_some(&mut self, max: Duration) -> Result<(), NodeError>

Execute the work that is ready NOW and return — rclcpp’s Executor::spin_some(max_duration).

ADOPT-BOUNDED (RFC-0089). Upstream’s drain verb: collect what is ready, execute it, come back. It never waits for new work — that is spin_once, which takes a budget and blocks on the transport until it expires.

Envelope, two ways it is weaker than rclcpp’s:

  • rclcpp collects the ready set ONCE and executes it. One spin_once(0) here is already that whole set (RFC-0002 §3), so this would be a single call — except a callback may enqueue work for another entity, which rclcpp’s spin_all re-collects and its spin_some leaves for the next call. Ours re-drains until a pass does NO work, which is spin_all’s answer, because the alternative is work sitting in the arena until someone spins again. Bounded by max, so a callback that feeds itself cannot wedge the caller.
  • max is checked BETWEEN passes, never inside one, so it is a floor on when this returns and not a ceiling: a single long callback overruns it. There is no preemption here to make it a ceiling.

max of Duration::ZERO means no limit — rclcpp’s own default — and is the spelling a build with no clock must use. A non-zero max on a clockless build is an ERROR rather than an unbounded drain (issue 0709, the same rule as spin’s timeout).

The Result is about that PRECONDITION and nothing else. A failed callback is counted in each pass’s SpinOnceResult and does not end the drain, matching spin’s default (SpinOptions::stop_on_first_error is false); rclcpp’s spin_some returns void and reports callback failures nowhere at all.

§Example
ⓘ
// Drain the ready set, then get on with the caller's own work.
executor.spin_some(core::time::Duration::ZERO)?;
Source

pub fn spin_one_period( &mut self, period_ms: u64, elapsed_ms: u64, ) -> SpinPeriodPollingResult

Process one iteration and return remaining sleep time.

This is no_std compatible — the caller is responsible for the actual delay using platform-specific sleep.

§Arguments
  • period_ms - Target period in milliseconds
  • elapsed_ms - Time elapsed since last call (used for timer ticking)
§Example
ⓘ
loop {
    let r = executor.spin_one_period(10, elapsed_ms);
    platform_sleep_ms(r.remaining_ms);
}
Source§

impl<'s> Executor<'s>

Source

pub fn cancel(&self)

Request the executor to stop spinning — rclcpp::Executor::cancel.

§ADOPT-BOUNDED (RFC-0089)

cancel sets a flag the spin loop observes at the NEXT POLL BOUNDARY, so it returns BEFORE spinning has actually stopped; is_spinning() is the observable that tells you when it has. rclcpp makes the same promise in the same shape, but the boundary here is one spin_once timeout wide (a backend’s drive_io blocks up to the poll interval), so “next boundary” is a duration a caller can measure rather than an implementation detail. The wake flag below shortens it; it does not remove it.

Nothing here tears down the session: an executor that has been cancelled is still initialised, still owns its entities, and can be spun again. Teardown is a different verb (fini / shutdown), and collapsing the two costs a full discovery round on every mode change.

Safe to call from another thread or a signal handler.

Also raises the Phase 104.C.6 wake flag so a spin_once already blocked inside a backend’s drive_io falls through to the cancel check on its next loop iteration instead of waiting out its full timeout_ms first.

Source

pub fn is_halted(&self) -> bool

Has a cancel() been REQUESTED?

Not the same question as is_spinning(), and both are kept because both have callers:

  • is_halted() — “someone asked this executor to stop.” True the instant cancel() returns, and it stays true until the next spin loop clears it on entry. This is the flag a hand-rolled while !exec.is_halted() loop polls, which is what a BSP that drives spin_once itself writes.
  • is_spinning() — “a spin loop is running right now.” Answers the other half of the envelope: whether the cancel has been ACTED on yet.

The two are true together for as long as one poll takes, which is exactly the window the ADOPT-BOUNDED envelope on cancel() describes. Neither is derivable from the other: an executor that was never spun is neither halted nor spinning, and one cancelled mid-poll is both.

The name is the pre-rename spelling and is deliberately NOT deprecated — cancel’s rclcpp pair is a spinning observable, not an is_cancel_requested one, so there is no ROS 2 name to adopt here and a third spelling would buy nothing.

Source

pub fn is_spinning(&self) -> bool

Is a spin loop running on this executor right now?

rclcpp spells this Executor::is_spinning(). True between the entry and the exit of spin, spin_period, the open_threaded worker loop, and the C / C++ API loops — every construct that owns its own iteration.

A bare spin_once does NOT set it: one poll is not a spin, and a caller running its own while around spin_once is the one who knows it is looping. Such a caller can say so with enter_spin_loop.

See is_halted() for why this is a different question from “was cancel requested”.

Source

pub fn enter_spin_loop(&self)

Declare that the caller is entering a spin loop it drives itself.

The seam the C and C++ APIs use so their loops answer is_spinning() without keeping a flag of their own (RFC-0019: ergonomics may live in the wrapper, state may not). Also CLEARS any pending cancel, matching what spin does on entry: a cancel requested before a spin started is not a cancel of THIS spin.

Pair it with exit_spin_loop on EVERY exit path, break and early return alike — a loop that leaves without it reports is_spinning() == true forever. It is a method pair rather than an RAII guard on purpose: a guard would have to borrow the executor for the whole loop, and every caller needs &mut inside the body to poll.

Takes &self (the flags are atomics), so the borrow ends at the call.

Source

pub fn exit_spin_loop(&self)

End the scope opened by enter_spin_loop.

Idempotent. Leaves the cancel flag alone: “someone asked me to stop” is the caller’s answer to keep or clear, and clearing it here would lose the reason the loop exited.

Source§

impl<'s> Executor<'s>

Source

pub fn spin(&mut self, opts: SpinOptions) -> Result<(), NodeError>

Blocking spin loop with configurable exit conditions — rclrs’s Executor::spin(SpinOptions).

ADOPT-BOUNDED (RFC-0089). Same name, same argument, same meaning: run until an exit condition in opts says stop. Two bounds, both visible in the signature:

  • The error channel is one error, not all of them. rclrs returns Vec<RclrsError> and the caller writes .first_error()?; there is no allocator here, so ours returns Result<(), NodeError> carrying the FIRST failing callback’s error and drops the rest (they are still counted in each pass’s SpinOnceResult). Ported line for line, the edit is .first_error()? -> ?, which the compiler points at.
  • The loop is a poll loop, so opts.poll_interval is the quantum at which every exit condition is observed — a cancel() from another task, the timeout expiring, max_callbacks being reached. Requesting a timeout on a build with no clock is an ERROR, not a silent never-ending loop (issue 0709).

phase-427 — this method was spin_blocking. spin was taken by the -> ! task body, which is now spin_forever.

Runs until one of:

  • cancel() is called (from another thread or signal handler)
  • opts.timeout expires (if set)
  • opts.max_callbacks is reached (if set)
  • opts.only_next is true (single iteration)
  • a callback failed and opts.stop_on_first_error is set
§Example
ⓘ
// Spin until cancelled
executor.spin(SpinOptions::default())?;

// Spin with 5-second timeout
executor.spin(SpinOptions::new().timeout(core::time::Duration::from_secs(5)))?;

// Single iteration
executor.spin(SpinOptions::spin_once())?;
Source

pub fn spin_one_period_timed(&mut self, period: Duration) -> SpinPeriodResult

Execute one period with wall-clock overrun detection.

Calls spin_once(), measures wall-clock time, sleeps for the remainder if under budget.

§Example
ⓘ
let period = core::time::Duration::from_millis(10);
let result = executor.spin_one_period_timed(period);
if result.overrun {
    log::warn!("Period overrun: {:?}", result.elapsed);
}
Source

pub fn spin_period(&mut self, period: Duration) -> Result<(), NodeError>

Spin at a fixed rate with drift compensation. Blocks until halted.

Uses wall-clock time to maintain the target rate. The next invocation time is accumulated (not reset to now + period) to prevent cumulative drift.

§Example
ⓘ
// 100Hz control loop — blocks until halt() is called
executor.spin_period(core::time::Duration::from_millis(10))?;
Source§

impl<'s> Executor<'s>

Source

pub unsafe fn open_threaded( self, policy: SchedPolicy, apply_policy: fn(SchedPolicy) -> Result<(), SchedError>, spin_period: Duration, ) -> ThreadHandle
where 's: 'static,

Phase 110.D.b — move this Executor onto a fresh OS thread, apply a per-thread scheduling policy via the caller-supplied apply_policy function, and run the spin loop until ThreadHandle::halt fires.

The function-pointer indirection on apply_policy lets the caller pass any platform’s PlatformScheduler::set_current_thread_policy without forcing Executor to be generic over the platform — keeps the existing Executor type stable.

Multi-executor preemption (the actual hard-RT win) comes from the OS scheduler — call open_threaded once per criticality tier, each with its own policy / priority. The kernel handles preemption across executors; within a single executor, dispatch remains non-preemptive (110.A–C bucketed sets).

§Safety

Moves self across thread boundaries. Executor contains a raw *mut session::ConcreteSession when constructed via from_session_ptr; the caller must ensure that pointer’s referent stays valid across the lifetime of the spawned thread and that no other thread mutates the session concurrently. from_session (Owned) is safer — ConcreteSession ownership transfers cleanly into the thread.

Source

pub fn wake(&self)

Phase 104.C.6 — wake the executor from another thread / ISR / signal handler.

Sets the shared wake_flag. The next spin_once swap-clears the flag, skips the blocking wait on the primary session, and polls every session non-blockingly so whatever queued the wake is observed in a single iteration. Idempotent — multiple wake() calls collapse into one observed wake per spin_once.

Source§

impl<'s> Executor<'s>

Source

pub fn halt_flag(&self) -> Arc<AtomicBool>

Get a clone of the halt flag for use in signal handlers or other threads.

§Example
ⓘ
let halt = executor.halt_flag();
std::thread::spawn(move || {
    std::thread::sleep(Duration::from_secs(5));
    halt.store(true, Ordering::SeqCst);
});
executor.spin(SpinOptions::default())?;
Source

pub fn wake_handle(&self) -> Arc<AtomicBool>

Phase 104.C.6 — clone of the shared wake flag for cross-thread use (signal handlers, foreign threads, future per-backend vtable wake hooks).

§Example
ⓘ
let wake = executor.wake_handle();
std::thread::spawn(move || {
    // ... compute something ...
    // hand off to executor by setting the flag.
    wake.store(true, Ordering::SeqCst);
});
loop { executor.spin_once(Duration::from_millis(100)); }
Source§

impl<'s> Executor<'s>

Source

pub fn register_action_server<A, GoalF, CancelF>( &mut self, action_name: &str, goal_callback: GoalF, cancel_callback: CancelF, ) -> Result<ActionServerHandle<A>, NodeError>

Register an action server with goal/cancel callbacks.

The executor automatically dispatches:

  • Goal acceptance via goal_callback
  • Cancel requests via cancel_callback
  • Result serving for completed goals

Use the returned ActionServerHandle to publish feedback and complete goals.

Uses default buffer sizes and max 4 concurrent goals.

Source

pub fn register_action_server_sized<A, GoalF, CancelF, const GOAL_BUF: usize, const RESULT_BUF: usize, const FEEDBACK_BUF: usize, const MAX_GOALS: usize>( &mut self, action_name: &str, goal_callback: GoalF, cancel_callback: CancelF, ) -> Result<ActionServerHandle<A>, NodeError>

Register an action server with custom buffer sizes.

Source§

impl<'s> Executor<'s>

Source

pub fn register_action_server_raw( &mut self, spec: RawActionServerSpec<'_>, ) -> Result<ActionServerRawHandle, NodeError>

Register a raw action server with raw-bytes callbacks.

Unlike register_action_server(), this does not require RosAction — the goal/cancel callbacks receive raw CDR bytes. This is used by the C API thin wrapper.

type_name and type_hash identify the action type for key expression construction and liveliness tokens.

Source

pub fn register_action_server_raw_sized<const GOAL_BUF: usize, const RESULT_BUF: usize, const FEEDBACK_BUF: usize, const MAX_GOALS: usize>( &mut self, spec: RawActionServerSpec<'_>, ) -> Result<ActionServerRawHandle, NodeError>

Register a raw action server with custom buffer sizes.

spec.node_id selects the target: None registers on the executor’s own node, Some(id) routes the server’s 5 underlying handles through the named Node’s session (Phase 104.C.3.3.a). spec.qos applies to the action’s three underlying service servers (send_goal / cancel_goal / get_result; Phase 193.4b); the feedback + status publishers keep their own profiles.

Source§

impl<'s> Executor<'s>

Source

pub unsafe fn detach_action_server_raw_sized<const GOAL_BUF: usize, const RESULT_BUF: usize, const FEEDBACK_BUF: usize, const MAX_GOALS: usize>( &mut self, entry_index: usize, ) -> bool

Detach a raw action SERVER’s arena entry from its owner’s storage — issue 1496.

This is not a release and does not pretend to be one. The entry keeps its slot, its arena bytes and its five RMW entities for the executor’s lifetime: detaching is not releasing. Resolution 2 of issue 1496 — release_action_server_raw, over the arena’s free list — is what gives those back; this keeps them. What it does is cut the one edge that outlives the owner: the registered context, and the two callbacks that dereference it.

After this call the entry answers a late send_goal with a REJECT and a late cancel_goal with an ACCEPT (detached_goal_callback / detached_cancel_callback), and the post-accept hook is cleared. Nothing reads context again, so the owner’s storage may die.

Returns false when entry_index holds no live entry or the entry is not an action server; the caller should treat that as “nothing to detach”, which is the normal state for a server that was created and never registered.

§Safety

entry_index must be the index of an entry registered by register_action_server_raw_sized with these same const parameters. The callbacks and the context sit behind ActionServerCore<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF, MAX_GOALS> in the entry, so their offsets are a function of those four numbers and a mismatch writes into the core’s buffers instead. The EntryKind check below catches a wrong KIND, never wrong sizes — the same contract action_client_core_mut and service_client_entry_mut carry.

Source

pub unsafe fn detach_action_server_raw( &mut self, handle: &ActionServerRawHandle, ) -> bool

detach_action_server_raw_sized for an entry registered through the default-sized register_action_server_raw.

Takes the handle rather than a bare index so the caller cannot name a subscription slot by accident: an ActionServerRawHandle is only ever produced by the raw registration path.

§Safety

handle must have come from register_action_server_raw — the DEFAULT-sized entry point. A handle from register_action_server_raw_sized::<…> with other const parameters must be detached through the sized call with the same ones.

Source

pub unsafe fn detach_action_client_raw(&mut self, entry_index: usize) -> bool

Detach a raw action CLIENT’s arena entry from its owner’s storage — issue 1496, the client half.

Same shape as detach_action_server_raw and one step simpler: the client entry’s three callbacks are already Options that action_client_raw_try_process guards with if let Some(cb), so clearing them is enough to make a late reply, feedback sample or result a no-op rather than a call through a dead context.

§Safety

entry_index must be the index of an entry registered by register_action_client_raw — the DEFAULT-sized entry point, which is the only one the C and C++ tiers use and the same assumption action_client_core_mut makes on every poll.

Source

pub unsafe fn retarget_action_client_raw( &mut self, entry_index: usize, context: *mut c_void, ) -> bool

RE-POINT a raw action client entry’s callback context — issue 1496.

The C++ client registers its own storage address as the entry’s context; a move relocates that storage, so the entry must follow it or a moved-from temporary leaves it naming dead bytes. false, touching nothing, when entry_index is not a live action client.

§Safety

The entry must have been registered through the default-sized register_action_client_raw, and context must be valid for as long as the entry may dispatch.

Source

pub unsafe fn release_action_server_raw_sized<const GOAL_BUF: usize, const RESULT_BUF: usize, const FEEDBACK_BUF: usize, const MAX_GOALS: usize>( &mut self, entry_index: usize, ) -> bool

RELEASE a raw action server — issue 1496’s resolution 2.

Where detach_action_server_raw_sized only cut the entry’s edge to its owner, this ends the entry: it is dropped in place, so its three service servers and two publishers are destroyed and the action leaves the graph; its callback slot is freed (NROS_EXECUTOR_MAX_CBS), and its arena bytes go back to the executor’s free list, where the next registration that fits reuses them. A program that creates and destroys action servers in a loop therefore runs forever instead of exhausting the slot table and then the arena.

false, touching nothing, when entry_index is not a live action server.

The const parameters are no longer load-bearing (issue 1631): the bytes released are the ones the registration RECORDED, so a mismatch cannot release the wrong length. They stay so existing callers compile.

§Safety

No ActionServerRawHandle naming the entry may be used afterwards — the slot is handed to the next registration.

Source

pub unsafe fn release_action_server_raw( &mut self, handle: &ActionServerRawHandle, ) -> bool

release_action_server_raw_sized for an entry registered through the default-sized register_action_server_raw.

§Safety

As the sized form; handle must not be used afterwards.

Source

pub unsafe fn release_action_client_raw(&mut self, entry_index: usize) -> bool

RELEASE a raw action client registered through the default-sized register_action_client_raw — issue 1496. Drops the entry (its three service clients and feedback subscription are destroyed), frees its callback slot and returns its arena bytes for reuse.

§Safety

As release_action_server_raw_sized, for the default-sized client entry.

Source

pub unsafe fn release_action_client_raw_sized<const GOAL_BUF: usize, const RESULT_BUF: usize, const FEEDBACK_BUF: usize>( &mut self, entry_index: usize, ) -> bool

The sized form of release_action_client_raw.

As for the server, the const parameters are no longer load-bearing (issue 1631): the released length is the recorded one.

§Safety

No copy of entry_index may be used afterwards.

Source§

impl<'s> Executor<'s>

Source

pub fn register_action_client_raw( &mut self, spec: RawActionClientSpec<'_>, ) -> Result<ActionClientRawHandle, NodeError>

Register a raw action client with the executor.

Creates service clients for send_goal, cancel_goal, get_result, and a feedback subscriber. The executor polls these during spin_once and invokes the provided callbacks when responses/feedback arrive.

§Arguments
  • action_name — action name (e.g., “/fibonacci”)
  • type_name — action type (e.g., “example_interfaces::action::dds_::Fibonacci_”)
  • type_hash — type hash (e.g., “TypeHashNotSupported”)
  • goal_response_callback — called when goal is accepted/rejected
  • feedback_callback — called when feedback is received
  • result_callback — called when result is received
  • context — opaque pointer passed to all callbacks
Source

pub fn register_action_client_raw_sized<const GOAL_BUF: usize, const RESULT_BUF: usize, const FEEDBACK_BUF: usize>( &mut self, spec: RawActionClientSpec<'_>, ) -> Result<ActionClientRawHandle, NodeError>

Register a raw action client with explicit buffer sizes.

spec.node_id selects the target: None registers on the executor’s own node, Some(id) routes the client’s 4 underlying handles through the named Node’s session (Phase 104.C.3.3.a).

Source§

impl<'s> Executor<'s>

Source

pub fn register_action_client_core<const GOAL_BUF: usize, const RESULT_BUF: usize, const FEEDBACK_BUF: usize>( &mut self, core: ActionClientCore<GOAL_BUF, RESULT_BUF, FEEDBACK_BUF>, goal_response_callback: Option<RawGoalResponseCallback>, feedback_callback: Option<RawFeedbackCallback>, result_callback: Option<RawResultCallback>, context: *mut c_void, ) -> Result<ActionClientRawHandle, NodeError>

Register an existing ActionClientCore with the executor for async polling.

Unlike register_action_client_raw (which creates new transport handles), this takes ownership of an existing core. Use this when the core was already created by the C/C++ action client init.

Trait Implementations§

Source§

impl<'s> Drop for Executor<'s>

Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more
Source§

impl<'s> Send for Executor<'s>

Available on crate feature alloc only.

Auto Trait Implementations§

§

impl<'s> !Freeze for Executor<'s>

§

impl<'s> !Sync for Executor<'s>

§

impl<'s> !UnwindSafe for Executor<'s>

§

impl<'s> RefUnwindSafe for Executor<'s>

§

impl<'s> Unpin for Executor<'s>

§

impl<'s> UnsafeUnpin for Executor<'s>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.