pub struct Executor<'s> { /* private fields */ }Implementations§
Source§impl<'s> Executor<'s>
impl<'s> Executor<'s>
Sourcepub unsafe fn open_in(
config: &ExecutorConfig<'_>,
backing: &'s mut [MaybeUninit<u64>],
sizing: ExecutorSizing,
) -> Result<Self, NodeError>
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>
impl Executor<'static>
Sourcepub fn open(config: &ExecutorConfig<'_>) -> Result<Self, NodeError>
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)?;Sourcepub fn open_sized(
config: &ExecutorConfig<'_>,
sizing: ExecutorSizing,
) -> Result<Self, NodeError>
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.
Sourcepub fn open_multi(specs: &[SessionSpec<'_>]) -> Result<Self, NodeError>
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).
Sourcepub fn open_with_rmw(
rmw_name: &str,
config: &ExecutorConfig<'_>,
) -> Result<Self, NodeError>
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>
impl<'s> Executor<'s>
Sourcepub unsafe fn open_multi_in(
specs: &[SessionSpec<'_>],
backing: &'s mut [MaybeUninit<u64>],
sizing: ExecutorSizing,
) -> Result<Self, NodeError>
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.
Sourcepub unsafe fn open_with_rmw_in(
rmw_name: &str,
config: &ExecutorConfig<'_>,
backing: &'s mut [MaybeUninit<u64>],
sizing: ExecutorSizing,
) -> Result<Self, NodeError>
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>
impl<'s> Executor<'s>
Sourcepub unsafe fn from_session_in(
session: CffiSession,
backing: &'s mut [MaybeUninit<u64>],
sizing: ExecutorSizing,
) -> Self
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).
Sourcepub unsafe fn from_session_ptr_in(
session_ptr: *mut CffiSession,
backing: &'s mut [MaybeUninit<u64>],
sizing: ExecutorSizing,
) -> Self
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_ptrmust point to a valid session that outlives the executor and is not moved/dropped while it exists.backingobligations as infrom_session_in.
Source§impl Executor<'static>
impl Executor<'static>
Sourcepub fn from_session(session: CffiSession) -> Self
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.
Sourcepub fn from_session_with(
session: CffiSession,
config: &ExecutorConfig<'_>,
) -> Self
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).
Sourcepub unsafe fn from_session_ptr(session_ptr: *mut CffiSession) -> Self
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_ptrmust 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>
impl<'s> Executor<'s>
Sourcepub unsafe fn open_with_session(session: *mut CffiSession) -> Executor<'static>
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.
Sourcepub unsafe fn open_with_session_in(
session: *mut CffiSession,
backing: &'s mut [MaybeUninit<u64>],
sizing: ExecutorSizing,
) -> Self
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.
Sourcepub fn session_ptr(&mut self) -> *mut CffiSession
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).
Sourcepub fn session_handle(&mut self) -> SessionHandle
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.
Sourcepub unsafe fn open_with_session_handle(
handle: SessionHandle,
) -> Executor<'static>
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.
Sourcepub unsafe fn open_with_session_handle_in(
handle: SessionHandle,
backing: &'s mut [MaybeUninit<u64>],
sizing: ExecutorSizing,
) -> Self
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
Sourcepub unsafe fn open_with_session_slot(
session: *mut CffiSession,
slot: &'s mut TierExecutorBackingSlot,
) -> Self
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.
Sourcepub unsafe fn open_with_session_handle_slot(
handle: SessionHandle,
slot: &'s mut TierExecutorBackingSlot,
) -> Self
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
Sourcepub fn set_active_groups(
&mut self,
groups: &[(&str, &str, &str)],
) -> Result<(), BindError>
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).
Sourcepub fn group_active(
&self,
node_name: &str,
node_namespace: &str,
group: &str,
) -> bool
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.
Sourcepub fn set_primary_identity(&mut self, rmw_name: &str, locator: &str)
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.
Sourcepub fn set_domain_id(&mut self, domain_id: u32)
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.
pub fn set_node_identity(&mut self, node_name: &str, namespace: &str)
Sourcepub fn declare_remap(
&mut self,
node_name: &str,
namespace: &str,
from: &str,
to: &str,
) -> Result<(), ()>
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.
Sourcepub fn resolve_entity_name_for(
&self,
node_name: &str,
namespace: &str,
source: &str,
) -> Result<ResolvedName, ()>
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).
Sourcepub fn resolve_entity_name(&self, source: &str) -> Result<ResolvedName, ()>
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.
Sourcepub fn bind_node_name_sched(
&mut self,
name: &str,
namespace: &str,
sc: SchedContextId,
) -> Result<(), BindError>
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.
Sourcepub fn bind_group_sched(
&mut self,
name: &str,
namespace: &str,
group: &str,
sc: SchedContextId,
) -> Result<(), BindError>
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.
Sourcepub fn default_sched_context_id(&self) -> SchedContextId
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.
Sourcepub fn create_sched_context(
&mut self,
sc: SchedContext,
) -> Result<SchedContextId, NodeError>
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.
Sourcepub fn epoch_now_us(&self) -> Option<u64>
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).
Sourcepub fn set_monitor_table(&mut self, table: &'static [MonitorSpec])
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.
Sourcepub fn try_set_monitor_tables(
&mut self,
table: &'static [MonitorSpec],
ages: &'static [AgeMonitorSpec],
) -> Result<(), MonitorTableFull>
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.
Sourcepub fn monitor_table(&self) -> &'static [MonitorSpec]
pub fn monitor_table(&self) -> &'static [MonitorSpec]
The installed monitor table (empty unless the entry set one).
Sourcepub fn set_age_table(&mut self, table: &'static [AgeMonitorSpec])
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.
Sourcepub fn age_table(&self) -> &'static [AgeMonitorSpec]
pub fn age_table(&self) -> &'static [AgeMonitorSpec]
The installed age table (empty unless the entry set one).
Sourcepub fn set_hazard_table(&mut self, table: &'static [HazardSpec])
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.
Sourcepub fn hazard_table(&self) -> &'static [HazardSpec]
pub fn hazard_table(&self) -> &'static [HazardSpec]
The installed hazard table (empty unless the entry set one).
Sourcepub fn set_epoch_clock(&mut self, epoch_us: fn() -> u64)
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.
Sourcepub fn skipped_releases(&self) -> u32
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.
Sourcepub fn release_jitter(&self) -> (u64, u32, u32)
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.
Sourcepub fn late_wakes_beyond_granularity(&self) -> u32
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.
Sourcepub fn clear_release_jitter_stats(&mut self)
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.
pub fn set_fault_handler(&mut self, f: fn(&Violation))
Sourcepub fn set_reaction_hook(&mut self, hook: Option<(ReactionHookFn, *mut c_void)>)
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.
Sourcepub fn route_next_due_us(&self) -> Option<u64>
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.
Sourcepub fn register_wake_source(
&mut self,
next: NextDeadlineFn,
ctx: *mut c_void,
) -> Result<WakeSourceId, NodeError>
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.
Sourcepub fn set_park_primitive(
&mut self,
park: ParkUntilFn,
ctx: *mut c_void,
granularity_us: u64,
)
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.
Sourcepub fn wake_raw_ptr(&self) -> *mut c_void
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.
Sourcepub fn has_park_primitive(&self) -> bool
pub fn has_park_primitive(&self) -> bool
Whether a platform park primitive is installed.
Sourcepub fn last_park(&self) -> (u64, WakeSourceId)
pub fn last_park(&self) -> (u64, WakeSourceId)
phase-436 — the bound the last park used, and which source won it.
Sourcepub fn last_park_achieved_us(&self) -> u64
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.
Sourcepub fn release_jitter_granularity_us(&self) -> u64
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 withplatform_sleep_usbetween 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.
Sourcepub fn park_granularity_us(&self) -> u64
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_timedwaitandpthread_cond_timedwaitare nanosecond-native, and only theuint32_t timeout_mssignature 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.
Sourcepub fn set_report_violations(&mut self, enabled: bool)
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.
Sourcepub fn set_violation_drain_report(&mut self, enabled: bool)
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.
Sourcepub fn violations_dropped(&self) -> u32
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.)
Sourcepub fn violations_total(&self) -> u32
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.
Sourcepub fn drain_violations(&mut self, f: impl FnMut(&Violation))
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.
Sourcepub fn drain_violations_numbered(&mut self, f: impl FnMut(u32, &Violation))
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.
Sourcepub fn set_monitor_arming(&mut self, on_call: bool, grace_ms: u32)
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”).
Sourcepub fn arm_monitors(&mut self)
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.
Sourcepub fn monitors_armed(&self) -> bool
pub fn monitors_armed(&self) -> bool
phase-474 I2 – whether this executor’s monitors are armed.
Sourcepub fn violations_suppressed_before_arm(&self) -> u32
pub fn violations_suppressed_before_arm(&self) -> u32
phase-474 I2 – verdicts reached before the monitors armed: counted, never stored, logged, sunk or traced.
Sourcepub unsafe fn set_violation_sink(
&mut self,
sink: Option<(ViolationSink, *mut c_void)>,
)
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).
Sourcepub fn violation_report_cost_us(&self) -> (u32, u32, usize, u32)
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).
Sourcepub fn set_spin_nominal_us(&mut self, us: u64)
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.
Sourcepub fn set_spin_nominal_wait_paced_us(&mut self, us: u64)
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.
pub fn set_min_stack_headroom_bytes(&mut self, bytes: usize)
Sourcepub fn stack_headroom_blind(&self) -> bool
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.
Sourcepub fn set_default_sched_context(&mut self, sc: SchedContext)
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.
Sourcepub fn bind_handle_to_sched_context(
&mut self,
handle: HandleId,
sc_id: SchedContextId,
) -> Result<(), NodeError>
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.
Sourcepub fn register_time_triggered_dispatcher(&mut self, major_frame_us: u32)
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.
Sourcepub fn apply_time_triggered_schedule<const N: usize>(
&mut self,
schedule: &TimeTriggeredSchedule<N>,
) -> Result<[SchedContextId; N], TimeTriggeredScheduleError>
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).
Sourcepub fn register_sporadic_timer(
&mut self,
sc_id: SchedContextId,
timer: OpaqueTimerHandle,
) -> Result<Arc<AtomicSporadicState>, NodeError>
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).
Sourcepub fn sched_context(&self, sc_id: SchedContextId) -> Option<&SchedContext>
pub fn sched_context(&self, sc_id: SchedContextId) -> Option<&SchedContext>
Inspect a registered scheduling context. Phase 110.B.
Sourcepub fn node_builder<'a, 'cfg>(
&'a mut self,
name: &'cfg str,
) -> NodeBuilder<'a, 'cfg, 's>
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.
Sourcepub fn nodes(&self) -> &[NodeRecord]
pub fn nodes(&self) -> &[NodeRecord]
Return the Node table — Phase 104.C.2 read accessor.
Sourcepub fn node(&self, id: NodeId) -> Option<&NodeRecord>
pub fn node(&self, id: NodeId) -> Option<&NodeRecord>
Borrow a Node’s metadata by id, returning None if the id
is out of range.
pub fn node_mut(&mut self, id: NodeId) -> Node<'_, 's>
Sourcepub fn set_node_qos_overrides(
&mut self,
node_id: NodeId,
overrides: &'static [QoSOverrideCode],
)
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.
Sourcepub fn node_session_mut(&mut self, node_id: NodeId) -> Option<&mut CffiSession>
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.
Sourcepub fn create_publisher_on<M: MessageForRmw>(
&mut self,
node_id: NodeId,
topic_name: &str,
qos: QoSProfile,
) -> Result<EmbeddedPublisher<M>, NodeError>
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.
Sourcepub fn create_publisher_raw_on(
&mut self,
node_id: NodeId,
topic_name: &str,
type_name: &str,
type_hash: &str,
qos: QoSProfile,
) -> Result<EmbeddedRawPublisher, NodeError>
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.
Sourcepub fn install_wake_signal_on_primary(&mut self)
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.
Sourcepub fn with_node_try<R>(
&mut self,
id: NodeId,
f: impl FnOnce(&mut NodeHandle<'_>) -> Result<R, NodeError>,
) -> Result<R, NodeError>
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(...))?;pub fn with_node<R>( &mut self, id: NodeId, f: impl FnOnce(&mut NodeHandle<'_>) -> R, ) -> Result<R, NodeError>
Sourcepub fn node_id_by_name(&self, name: &str, namespace: &str) -> Option<NodeId>
pub fn node_id_by_name(&self, name: &str, namespace: &str) -> Option<NodeId>
Find a registered executor node by final name and namespace.
Sourcepub fn create_node(&mut self, name: &str) -> Result<Node<'_, 's>, NodeError>
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.
Sourcepub fn create_node_on(
&mut self,
name: &str,
rmw: &str,
) -> Result<Node<'_, 's>, NodeError>
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.
Sourcepub fn create_node_on_with_domain(
&mut self,
name: &str,
rmw: &str,
domain_id: Option<u32>,
locator: Option<&str>,
) -> Result<Node<'_, 's>, NodeError>
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).
Sourcepub fn close(&mut self) -> Result<(), NodeError>
pub fn close(&mut self) -> Result<(), NodeError>
Close the underlying session, running the shutdown hooks around it.
Issue 0790. The order is the feature:
- 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;
- the session close;
- 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.
Sourcepub unsafe fn add_pre_shutdown_callback(
&mut self,
callback: ShutdownCallbackFn,
context: *mut c_void,
) -> Result<ShutdownCallbackHandle, NodeError>
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.
Sourcepub unsafe fn add_on_shutdown_callback(
&mut self,
callback: ShutdownCallbackFn,
context: *mut c_void,
) -> Result<ShutdownCallbackHandle, NodeError>
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.
Sourcepub fn remove_pre_shutdown_callback(
&mut self,
handle: ShutdownCallbackHandle,
) -> bool
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.
Sourcepub fn remove_on_shutdown_callback(
&mut self,
handle: ShutdownCallbackHandle,
) -> bool
pub fn remove_on_shutdown_callback( &mut self, handle: ShutdownCallbackHandle, ) -> bool
Remove an on-shutdown hook. See Self::remove_pre_shutdown_callback.
Sourcepub fn shutdown_callback_count(&self, phase: ShutdownPhase) -> usize
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.
Sourcepub 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<(), ()>
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).
Sourcepub fn dispatch_slot_count(&self) -> usize
pub fn dispatch_slot_count(&self) -> usize
Phase 216 follow-up — current registered dispatch-slot count. Diagnostic / test surface.
Sourcepub 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<(), ()>
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.
Sourcepub fn component_slot_count(&self) -> usize
pub fn component_slot_count(&self) -> usize
Phase 258 (Track 2, 2a) — current enrolled component-slot count. Diagnostic / test surface.
Sourcepub fn enrolled_component_states(
&self,
) -> impl Iterator<Item = *mut c_void> + '_
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.
Sourcepub fn dispatch_callback(&mut self, cb_id: &str, ctx: *mut c_void)
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().
Sourcepub fn session(&self) -> &CffiSession
pub fn session(&self) -> &CffiSession
Get a reference to the underlying session.
Sourcepub fn session_mut(&mut self) -> &mut CffiSession
pub fn session_mut(&mut self) -> &mut CffiSession
Get a mutable reference to the underlying session.
Sourcepub fn ping(&mut self, timeout_ms: i32) -> Result<(), NodeError>
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.
Sourcepub fn get_node_names(
&mut self,
visit: &mut dyn FnMut(&str, &str, Option<&str>) -> bool,
) -> Result<(), NodeError>
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.
Sourcepub fn get_topic_names_and_types(
&mut self,
visit: &mut dyn FnMut(&str, &[&str]) -> bool,
) -> Result<(), NodeError>
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.
Sourcepub fn get_service_names_and_types(
&mut self,
visit: &mut dyn FnMut(&str, &[&str]) -> bool,
) -> Result<(), NodeError>
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.
Sourcepub fn count_publishers(&mut self, topic_name: &str) -> Result<usize, NodeError>
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.
Sourcepub fn count_subscribers(
&mut self,
topic_name: &str,
) -> Result<usize, NodeError>
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.
Sourcepub 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>
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”.
Sourcepub 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>
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.
Sourcepub 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>
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.
Sourcepub 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>
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.
Sourcepub fn get_publishers_info_by_topic(
&mut self,
topic_name: &str,
visit: &mut dyn FnMut(&GraphEndpointInfo<'_>) -> bool,
) -> Result<(), NodeError>
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.
Sourcepub fn get_subscriptions_info_by_topic(
&mut self,
topic_name: &str,
visit: &mut dyn FnMut(&GraphEndpointInfo<'_>) -> bool,
) -> Result<(), NodeError>
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.
Sourcepub fn wait_for_publishers(
&mut self,
topic_name: &str,
count: usize,
timeout: Duration,
) -> Result<bool, NodeError>
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.
Sourcepub fn wait_for_subscribers(
&mut self,
topic_name: &str,
count: usize,
timeout: Duration,
) -> Result<bool, NodeError>
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.
Sourcepub unsafe fn action_client_core_mut(
&mut self,
entry_index: usize,
) -> Option<&mut ActionClientCore>
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.
Sourcepub unsafe fn service_client_entry_mut(
&mut self,
entry_index: usize,
) -> Option<&mut ServiceClientRawArenaEntry<{ crate::config::DEFAULT_RX_BUF_SIZE }>>
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.
Sourcepub unsafe fn subscription_handle(
&self,
entry_index: usize,
) -> Option<&RmwSubscriber>
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.
Sourcepub unsafe fn service_server_handle(
&self,
entry_index: usize,
) -> Option<&RmwServiceServer>
pub unsafe fn service_server_handle( &self, entry_index: usize, ) -> Option<&RmwServiceServer>
The RmwServiceServer of an arena-registered service, by entry index.
§Safety
Sourcepub unsafe fn service_client_handle(
&self,
entry_index: usize,
) -> Option<&RmwServiceClient>
pub unsafe fn service_client_handle( &self, entry_index: usize, ) -> Option<&RmwServiceClient>
The RmwServiceClient of an arena-registered client, by entry index.
§Safety
Sourcepub fn set_trigger(&mut self, trigger: Trigger)
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.
Sourcepub fn set_semantics(&mut self, semantics: ExecutorSemantics)
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.
Sourcepub fn set_invocation(&mut self, id: HandleId, mode: InvocationMode)
pub fn set_invocation(&mut self, id: HandleId, mode: InvocationMode)
Sourcepub fn arena_used(&self) -> usize
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.
Sourcepub fn arena_capacity(&self) -> usize
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.
Sourcepub fn arena_released(&self) -> usize
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.
Sourcepub fn arena_holes(&self) -> usize
pub fn arena_holes(&self) -> usize
Issue 1667 — how many separate holes the free list holds. For tests and diagnostics.
Sourcepub unsafe fn release_subscription(&mut self, handle: HandleId) -> bool
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.
Sourcepub unsafe fn release_timer(&mut self, handle: HandleId) -> bool
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
Sourcepub unsafe fn release_service(&mut self, handle: HandleId) -> bool
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
Sourcepub unsafe fn release_service_client(&mut self, handle: HandleId) -> bool
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
Sourcepub unsafe fn release_guard_condition(&mut self, handle: HandleId) -> bool
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.
Sourcepub fn resolve_handle(&self, id: HandleId) -> Option<usize>
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.
Sourcepub fn set_entry_owner(&mut self, id: HandleId, node: NodeId) -> bool
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.
Sourcepub unsafe fn release_node(&mut self, node: NodeId) -> usize
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.
Sourcepub 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>
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>
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).
Sourcepub fn add_arena_subscription_callback<F, const RX_BUF: usize>(
&mut self,
handle: RmwSubscriber,
qos: QoSProfile,
callback: F,
) -> Result<HandleId, NodeError>
pub fn add_arena_subscription_callback<F, const RX_BUF: usize>( &mut self, handle: RmwSubscriber, qos: QoSProfile, callback: F, ) -> Result<HandleId, NodeError>
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_subscriptiononself.session_mut()with aTopicInfo. Thenode_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 viaSelf::session_mutand call its backend-specific create method, then hand the handle here.nros-px4::uorb::create_subscription_with_callbackis 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].
Sourcepub 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,
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().
Sourcepub 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,
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.
Sourcepub 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,
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.
Sourcepub 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,
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.
Sourcepub fn register_timer<F>(
&mut self,
period: TimerDuration,
callback: F,
) -> Result<HandleId, NodeError>where
F: FnMut() + 'static,
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.
Sourcepub fn register_timer_on_clock<F>(
&mut self,
period: TimerDuration,
source: TimerClockSource,
callback: F,
) -> Result<HandleId, NodeError>where
F: FnMut() + 'static,
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.
Sourcepub fn register_timer_oneshot<F>(
&mut self,
delay: TimerDuration,
callback: F,
) -> Result<HandleId, NodeError>where
F: FnMut() + 'static,
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.
Sourcepub 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>
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.
Sourcepub 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,
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.
Sourcepub 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>
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.
Sourcepub 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>
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.
Sourcepub 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>
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.
Sourcepub 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>
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.
Sourcepub fn register_service_raw(
&mut self,
service_name: &str,
service_type: &str,
service_hash: &str,
callback: RawServiceCallback,
context: *mut c_void,
) -> Result<HandleId, NodeError>
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.
Sourcepub 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>
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).
Sourcepub 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>
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.
Sourcepub 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>
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.
Sourcepub 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>
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.
Sourcepub 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>
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.
Sourcepub fn register_guard_condition<F>(
&mut self,
callback: F,
) -> Result<(HandleId, GuardCondition), NodeError>where
F: FnMut() + 'static,
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.
Sourcepub fn register_guard_condition_on<F>(
&mut self,
node_id: Option<NodeId>,
callback: F,
) -> Result<(HandleId, GuardCondition), NodeError>where
F: FnMut() + 'static,
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).
Sourcepub fn register_graph_change_guard_on<F>(
&mut self,
node_id: Option<NodeId>,
callback: F,
) -> Result<(HandleId, GuardCondition), NodeError>where
F: FnMut() + 'static,
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.
Sourcepub fn register_graph_change_guard<F>(
&mut self,
callback: F,
) -> Result<(HandleId, GuardCondition), NodeError>where
F: FnMut() + 'static,
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.
Sourcepub fn cancel_timer(&mut self, id: HandleId) -> Result<(), NodeError>
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().
Sourcepub fn reset_timer(&mut self, id: HandleId) -> Result<(), NodeError>
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.
Sourcepub fn timer_is_canceled(&self, id: HandleId) -> bool
pub fn timer_is_canceled(&self, id: HandleId) -> bool
Check if a timer is cancelled.
Sourcepub fn timer_period_ms(&self, id: HandleId) -> Option<u64>
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.
Sourcepub fn timer_period_us(&self, id: HandleId) -> Option<u64>
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.
Sourcepub fn exchange_timer_period_us(
&mut self,
id: HandleId,
new_period_us: u64,
) -> Option<u64>
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.
Sourcepub fn set_timer_overrun_policy(
&mut self,
id: HandleId,
policy: TimerOverrunPolicy,
) -> Result<(), NodeError>
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.
Sourcepub fn timer_overruns(&self, id: HandleId) -> Option<u32>
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.
Sourcepub fn timer_elapsed_us(&self, id: HandleId) -> Option<u64>
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.
Sourcepub fn timer_is_ready(&self, id: HandleId) -> Option<bool>
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.
Sourcepub fn timer_time_until_next_call_ns(&self, id: HandleId) -> Option<i64>
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.
Sourcepub fn session_io_failures(&self) -> u32
pub fn session_io_failures(&self) -> u32
Drive I/O and dispatch registered callbacks once.
Three-phase execution:
- Readiness scan — query each handle’s
has_data(). - Trigger evaluation — check if the executor-level trigger passes.
- 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 ati32::MAXms (~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.
Sourcepub fn session_io_healthy(&self) -> bool
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.
Sourcepub fn spin_once(&mut self, timeout: Duration) -> SpinOnceResult
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”.
Sourcepub fn spin_forever(&mut self, opts: SpinOptions) -> !
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 returnsSourcepub async fn spin_async(&mut self) -> !
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 noSpinOptions. To stop,selectit 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 returningPending, so any runtime that honours wakes polls it again at once. It is CPU-bound by construction; the 1 msspin_oncebudget is its only wait. Do not readasyncas 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; }
}Sourcepub fn spin_some(&mut self, max: Duration) -> Result<(), NodeError>
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’sspin_allre-collects and itsspin_someleaves for the next call. Ours re-drains until a pass does NO work, which isspin_all’s answer, because the alternative is work sitting in the arena until someone spins again. Bounded bymax, so a callback that feeds itself cannot wedge the caller. maxis 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)?;Sourcepub fn spin_one_period(
&mut self,
period_ms: u64,
elapsed_ms: u64,
) -> SpinPeriodPollingResult
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 millisecondselapsed_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>
impl<'s> Executor<'s>
Sourcepub fn cancel(&self)
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.
Sourcepub fn is_halted(&self) -> bool
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 instantcancel()returns, and it stays true until the next spin loop clears it on entry. This is the flag a hand-rolledwhile !exec.is_halted()loop polls, which is what a BSP that drivesspin_onceitself 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.
Sourcepub fn is_spinning(&self) -> bool
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”.
Sourcepub fn enter_spin_loop(&self)
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.
Sourcepub fn exit_spin_loop(&self)
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>
impl<'s> Executor<'s>
Sourcepub fn spin(&mut self, opts: SpinOptions) -> Result<(), NodeError>
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 returnsResult<(), NodeError>carrying the FIRST failing callback’s error and drops the rest (they are still counted in each pass’sSpinOnceResult). Ported line for line, the edit is.first_error()?->?, which the compiler points at. - The loop is a poll loop, so
opts.poll_intervalis the quantum at which every exit condition is observed — acancel()from another task, thetimeoutexpiring,max_callbacksbeing reached. Requesting atimeouton 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.timeoutexpires (if set)opts.max_callbacksis reached (if set)opts.only_nextis true (single iteration)- a callback failed and
opts.stop_on_first_erroris 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())?;Sourcepub fn spin_one_period_timed(&mut self, period: Duration) -> SpinPeriodResult
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);
}Sourcepub fn spin_period(&mut self, period: Duration) -> Result<(), NodeError>
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>
impl<'s> Executor<'s>
Sourcepub unsafe fn open_threaded(
self,
policy: SchedPolicy,
apply_policy: fn(SchedPolicy) -> Result<(), SchedError>,
spin_period: Duration,
) -> ThreadHandlewhere
's: 'static,
pub unsafe fn open_threaded(
self,
policy: SchedPolicy,
apply_policy: fn(SchedPolicy) -> Result<(), SchedError>,
spin_period: Duration,
) -> ThreadHandlewhere
'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.
Sourcepub fn wake(&self)
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>
impl<'s> Executor<'s>
Sourcepub fn halt_flag(&self) -> Arc<AtomicBool>
pub fn halt_flag(&self) -> Arc<AtomicBool>
Sourcepub fn wake_handle(&self) -> Arc<AtomicBool>
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>
impl<'s> Executor<'s>
Sourcepub fn register_action_server<A, GoalF, CancelF>(
&mut self,
action_name: &str,
goal_callback: GoalF,
cancel_callback: CancelF,
) -> Result<ActionServerHandle<A>, NodeError>where
A: RosAction + 'static,
A::Goal: Clone + MessageForRmw,
A::Result: Clone + Default + MessageForRmw,
A::Feedback: MessageForRmw,
A::SendGoalRequest: MessageForRmw,
A::SendGoalResponse: MessageForRmw,
A::GetResultRequest: MessageForRmw,
A::GetResultResponse: MessageForRmw,
A::FeedbackMessage: MessageForRmw,
GoalF: FnMut(&GoalId, &A::Goal) -> GoalResponse + 'static,
CancelF: FnMut(&GoalId, GoalStatus) -> CancelResponse + 'static,
pub fn register_action_server<A, GoalF, CancelF>(
&mut self,
action_name: &str,
goal_callback: GoalF,
cancel_callback: CancelF,
) -> Result<ActionServerHandle<A>, NodeError>where
A: RosAction + 'static,
A::Goal: Clone + MessageForRmw,
A::Result: Clone + Default + MessageForRmw,
A::Feedback: MessageForRmw,
A::SendGoalRequest: MessageForRmw,
A::SendGoalResponse: MessageForRmw,
A::GetResultRequest: MessageForRmw,
A::GetResultResponse: MessageForRmw,
A::FeedbackMessage: MessageForRmw,
GoalF: FnMut(&GoalId, &A::Goal) -> GoalResponse + 'static,
CancelF: FnMut(&GoalId, GoalStatus) -> CancelResponse + 'static,
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.
Sourcepub 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>where
A: RosAction + 'static,
A::Goal: Clone + MessageForRmw,
A::Result: Clone + Default + MessageForRmw,
A::Feedback: MessageForRmw,
A::SendGoalRequest: MessageForRmw,
A::SendGoalResponse: MessageForRmw,
A::GetResultRequest: MessageForRmw,
A::GetResultResponse: MessageForRmw,
A::FeedbackMessage: MessageForRmw,
GoalF: FnMut(&GoalId, &A::Goal) -> GoalResponse + 'static,
CancelF: FnMut(&GoalId, GoalStatus) -> CancelResponse + 'static,
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>where
A: RosAction + 'static,
A::Goal: Clone + MessageForRmw,
A::Result: Clone + Default + MessageForRmw,
A::Feedback: MessageForRmw,
A::SendGoalRequest: MessageForRmw,
A::SendGoalResponse: MessageForRmw,
A::GetResultRequest: MessageForRmw,
A::GetResultResponse: MessageForRmw,
A::FeedbackMessage: MessageForRmw,
GoalF: FnMut(&GoalId, &A::Goal) -> GoalResponse + 'static,
CancelF: FnMut(&GoalId, GoalStatus) -> CancelResponse + 'static,
Register an action server with custom buffer sizes.
Source§impl<'s> Executor<'s>
impl<'s> Executor<'s>
Sourcepub fn register_action_server_raw(
&mut self,
spec: RawActionServerSpec<'_>,
) -> Result<ActionServerRawHandle, NodeError>
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.
Sourcepub 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>
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>
impl<'s> Executor<'s>
Sourcepub 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
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.
Sourcepub unsafe fn detach_action_server_raw(
&mut self,
handle: &ActionServerRawHandle,
) -> bool
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.
Sourcepub unsafe fn detach_action_client_raw(&mut self, entry_index: usize) -> bool
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.
Sourcepub unsafe fn retarget_action_client_raw(
&mut self,
entry_index: usize,
context: *mut c_void,
) -> bool
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.
Sourcepub 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
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.
Sourcepub unsafe fn release_action_server_raw(
&mut self,
handle: &ActionServerRawHandle,
) -> bool
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.
Sourcepub unsafe fn release_action_client_raw(&mut self, entry_index: usize) -> bool
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.
Sourcepub 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
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>
impl<'s> Executor<'s>
Sourcepub fn register_action_client_raw(
&mut self,
spec: RawActionClientSpec<'_>,
) -> Result<ActionClientRawHandle, NodeError>
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/rejectedfeedback_callback— called when feedback is receivedresult_callback— called when result is receivedcontext— opaque pointer passed to all callbacks
Sourcepub 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>
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>
impl<'s> Executor<'s>
Sourcepub 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>
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§
impl<'s> Send for Executor<'s>
alloc only.