Skip to main content

nros/
node.rs

1//! Rust component API shared by metadata discovery and generated runtimes.
2
3// phase-483 W3 — the component API (`Component`, `NodeContext`,
4// `DeclarativeNode`) registers into a live executor and exists only with
5// `rmw-cffi`. Without it this module still carries the metadata vocabulary the
6// C/C++ adapters use, and the declaration helpers the component API calls go
7// unused.
8#![cfg_attr(not(feature = "rmw-cffi"), allow(unused_imports, dead_code))]
9
10use core::marker::PhantomData;
11
12// issue 0413 — the descriptor-registration bound. `MessageForRmw` is
13// `RosMessage` alone unless a descriptor-needing backend is linked, in which
14// case it also requires `nros_serdes::schema::Message` (the schema the Cyclone
15// descriptor builder walks). Generated message crates implement both.
16use nros_node::rmw_type_registry::MessageForRmw;
17
18use crate::{
19    ActionTag, CallbackId, CancelResponse, EntityId, GoalId, GoalResponse, GoalStatus,
20    ParameterType, QoSProfile, RosAction, RosMessage, RosService, ServiceTag, SubscriptionTag,
21    TimerClockSource, TimerDuration,
22    heapless::Vec,
23    node_metadata::{
24        CallbackEffectKind, EntityKind, EntityMetadata, EntityMetadataSpec, MetadataRecorder,
25        MetadataString, NodeId, NodeMetadataError, ParameterDefault, SourceLocationMetadata,
26        copy_str, entity_metadata,
27    },
28};
29
30// Phase 212.N.7 step-6 closing sweep — `component_register_symbol`
31// removed. It built the legacy `__nros_component_<pkg>_register`
32// symbol name for the M.5.a BSP baker to look up by literal. step-6
33// retired the macro emit + step-4 deleted the FreeRTOS BSP baker
34// crate that was the sole live consumer. The Phase 212.N Entry pkg
35// path calls `<pkg>::register(runtime)` through the path API, so this
36// helper has no live callers.
37
38/// Result type for component declarations.
39pub type NodeResult<T = ()> = Result<T, NodeDeclError>;
40
41/// Register `M`'s runtime type descriptor with a descriptor-needing backend
42/// (Cyclone DDS), from the DECLARATIVE path — issue 0413.
43///
44/// The imperative API's typed creators (`Node::create_publisher_with_qos::<M>`
45/// in `nros-node`) already call `register_type::<M>()` before asking the cffi
46/// vtable for the entity, because Cyclone resolves topic types through a
47/// RUNTIME registry and `dds_create_topic` fails without it.
48///
49/// The declarative Node API does not reach those creators. `NodeContext`
50/// records `EntityMetadata` and the sink calls the type-ERASED
51/// `create_generic_publisher_with_qos(topic, type_name, type_hash, qos)` —
52/// which has a type NAME and no `M`, so it cannot register anything. The
53/// descriptor was therefore never built, `find_descriptor` returned null in
54/// `publisher.cpp`, and the entity failed with `NROS_RMW_RET_UNSUPPORTED` ->
55/// `TransportError::PublisherCreationFailed` -> `NodeDeclError::Runtime` ->
56/// `RuntimeError::NodeRegister("<pkg>")`, four collapses away from the cause.
57///
58/// So it is registered HERE, at the last point that still knows `M`. A no-op
59/// unless a descriptor-needing backend installed a registrar
60/// (`nros_rmw::register_type_descriptor` returns `Ok` when the slot is empty),
61/// so zenoh / XRCE builds are unaffected.
62///
63/// Why this only surfaced now: every native Rust example was
64/// `[package.metadata.nros.application]` (imperative, typed creators) until
65/// phase-338 W3 made them Node-class. C and C++ were never affected — they use
66/// the static `descriptors.cpp` table, which is why `c/talker` published
67/// normally against the same backend while `rust/talker` could not.
68#[inline]
69fn register_declared_type<M: nros_node::rmw_type_registry::MessageForRmw>() -> NodeResult<()> {
70    nros_node::rmw_type_registry::register_type::<M>().map_err(|_| NodeDeclError::Runtime)
71}
72
73/// issue 0413, service half — register both payload types of `S`.
74///
75/// Services reach the same type-erased sink path as publishers, so the
76/// descriptor has to be built here too. The bound is a WHERE-CLAUSE on the
77/// declarative methods rather than on `RosService` itself: `RosService` lives in
78/// `nros-core`, which cannot depend on `nros-node` where `MessageForRmw` is
79/// defined. Generated service crates satisfy it (their payloads implement
80/// `schema::Message`); a hand-rolled service used only with zenoh/XRCE is
81/// unaffected, because `MessageForRmw` collapses to `RosMessage` when no
82/// descriptor-needing backend is linked.
83#[inline]
84fn register_declared_service<S: RosService>() -> NodeResult<()>
85where
86    S::Request: nros_node::rmw_type_registry::MessageForRmw,
87    S::Reply: nros_node::rmw_type_registry::MessageForRmw,
88{
89    register_declared_type::<S::Request>()?;
90    register_declared_type::<S::Reply>()
91}
92
93/// Node declaration error.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum NodeDeclError {
96    /// Metadata recorder rejected the declaration.
97    Metadata(NodeMetadataError),
98    /// Host/runtime discovery could not find `nros::node!` export.
99    MissingExport,
100    /// Generated runtime rejected the declaration.
101    Runtime,
102    /// The executor's fixed callback-entry table is full — a timer /
103    /// subscription / service / action could not claim a slot (issue 0095).
104    /// Carries the capacity cause through the `NodeError → NodeDeclError`
105    /// collapse so the register seam can name `NROS_EXECUTOR_MAX_CBS`.
106    ExecutorFull,
107    /// A publish named an entity this component has no publisher for — the
108    /// LOOKUP failed, nothing was ever handed to the transport.
109    ///
110    /// issue 0736 — split out of [`Self::Runtime`], which the publish path
111    /// returned for BOTH "the transport rejected the sample" and "there is no
112    /// such publisher": `lookup_publisher(...).unwrap_or(Err(Runtime))`. Those
113    /// are different bugs in different layers with different fixes, and from a
114    /// serial console they were the same line. Exactly the conflation #572
115    /// removed one level out, where discarding the result made "the timer never
116    /// fired" and "every publish failed" the same observation.
117    UnknownPublisher,
118    /// The parameter store refused a declaration and the name is still absent:
119    /// the store is full, or it rejected the value. A name already declared is
120    /// NOT this — the store answers `false` for that too, but the parameter is
121    /// there.
122    ///
123    /// Split out of [`Self::Runtime`] for the reason [`Self::UnknownPublisher`]
124    /// was (issue 0736): a refused parameter and a rejected transport handle are
125    /// different faults with different fixes, and one opaque variant made them
126    /// the same line on a serial console.
127    ParameterRejected,
128    /// A component CELL registry is full -- the class creates more entities of
129    /// one kind (publishers, service servers/clients, action servers/clients)
130    /// than its registry holds.
131    ///
132    /// issue 1130 -- split out of [`Self::Runtime`] BEFORE that capacity became
133    /// derivable. phase-412's rule: a derived count is safe only where
134    /// exhaustion NAMES the knob, and a bare "component runtime rejected
135    /// declaration" named nothing. The capacity is the class's `ENTITY_BOUNDS`
136    /// when it states one, else `NROS_RUNTIME_MAX_CELL_ENTITIES`.
137    CellRegistryFull,
138}
139
140impl NodeDeclError {
141    /// Human-readable static message for diagnostics that cross FFI/plugin boundaries.
142    pub const fn message(self) -> &'static str {
143        match self {
144            Self::Metadata(NodeMetadataError::Capacity) => "component metadata capacity exceeded",
145            Self::Metadata(NodeMetadataError::NameTooLong) => "component metadata name too long",
146            Self::Metadata(NodeMetadataError::UnknownNode) => {
147                "component entity references an unknown node"
148            }
149            Self::Metadata(NodeMetadataError::UnknownEntity) => {
150                "component callback effect references an unknown entity"
151            }
152            Self::Metadata(NodeMetadataError::DuplicateId) => {
153                "component metadata contains a duplicate stable ID"
154            }
155            Self::MissingExport => "package has no exported nros component",
156            Self::Runtime => "component runtime rejected declaration",
157            Self::UnknownPublisher => "no publisher declared for that entity",
158            Self::ParameterRejected => {
159                "the parameter store refused the declaration (the store is full, or it rejected the value)"
160            }
161            Self::ExecutorFull => {
162                "executor callback table full — raise NROS_EXECUTOR_MAX_CBS \
163                 (build-time, default 4)"
164            }
165            Self::CellRegistryFull => {
166                "component cell registry full — raise the class's ENTITY_BOUNDS, \
167                 or NROS_RUNTIME_MAX_CELL_ENTITIES (build-time, default 8) for a \
168                 class that states none"
169            }
170        }
171    }
172}
173
174impl From<NodeMetadataError> for NodeDeclError {
175    fn from(value: NodeMetadataError) -> Self {
176        Self::Metadata(value)
177    }
178}
179
180#[cfg(test)]
181mod cell_registry_full_tests {
182    use super::NodeDeclError;
183
184    /// Issue 1130 -- the precondition for deriving the cell capacity: running
185    /// out NAMES both remedies, where it used to read "component runtime
186    /// rejected declaration".
187    #[test]
188    fn a_full_cell_registry_names_the_knob_and_the_class_bound() {
189        let m = NodeDeclError::CellRegistryFull.message();
190        assert!(m.contains("NROS_RUNTIME_MAX_CELL_ENTITIES"), "{m}");
191        assert!(m.contains("ENTITY_BOUNDS"), "{m}");
192        assert_ne!(m, NodeDeclError::Runtime.message());
193    }
194}
195
196/// issue 0413, action half — register every wire payload type of `A`.
197///
198/// Mirrors the eight `register_type::<A::…>()` calls the IMPERATIVE action
199/// creator makes (`nros-node/src/executor/action.rs`); the declarative path
200/// reaches the same type-erased sink and would otherwise register none of them.
201/// `A::register_protocol_types()` covers the fixed `action_msgs` types the
202/// cancel/status plumbing serializes, exactly as the imperative path does.
203#[inline]
204fn register_declared_action<A: RosAction>() -> NodeResult<()>
205where
206    A::Goal: nros_node::rmw_type_registry::MessageForRmw,
207    A::Result: nros_node::rmw_type_registry::MessageForRmw,
208    A::Feedback: nros_node::rmw_type_registry::MessageForRmw,
209    A::SendGoalRequest: nros_node::rmw_type_registry::MessageForRmw,
210    A::SendGoalResponse: nros_node::rmw_type_registry::MessageForRmw,
211    A::GetResultRequest: nros_node::rmw_type_registry::MessageForRmw,
212    A::GetResultResponse: nros_node::rmw_type_registry::MessageForRmw,
213    A::FeedbackMessage: nros_node::rmw_type_registry::MessageForRmw,
214{
215    register_declared_type::<A::Goal>()?;
216    register_declared_type::<A::Result>()?;
217    register_declared_type::<A::Feedback>()?;
218    register_declared_type::<A::SendGoalRequest>()?;
219    register_declared_type::<A::SendGoalResponse>()?;
220    register_declared_type::<A::GetResultRequest>()?;
221    register_declared_type::<A::GetResultResponse>()?;
222    register_declared_type::<A::FeedbackMessage>()?;
223    A::register_protocol_types().map_err(|()| NodeDeclError::Runtime)
224}
225
226/// phase-391 W5-endgame step 2c (issue 0857) — a component class's declared
227/// upper bounds, PER ENTITY KIND, for sizing its cell registries at compile
228/// time.
229///
230/// The runtime's per-class cell storage is static, so its registries pay
231/// their CAPACITY whether or not entities fill it — and one publisher slot
232/// costs ~1.35 KiB (the loan arena rides inside). The default is the
233/// `NROS_RUNTIME_MAX_CELL_ENTITIES` knob per kind, which always works;
234/// a class that declares its real bounds pays exactly what it uses.
235/// Declaring FEWER than `register()` creates is a loud registration error
236/// (registry full), never a silent drop.
237///
238/// Public and non-generic on purpose (the `ExecutorSizing` rule): the const
239/// generics this feeds stay behind the `nros::node!` macro emission, so no
240/// other language ever sees them.
241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
242pub struct EntityBounds {
243    /// Publishers this class creates (each slot ~1.35 KiB — the big one).
244    pub publishers: usize,
245    /// Service servers (sizes the trampoline-context slab, not a registry).
246    pub service_servers: usize,
247    /// Service clients.
248    pub service_clients: usize,
249    /// Action clients.
250    pub action_clients: usize,
251    /// Action servers.
252    pub action_servers: usize,
253}
254
255impl EntityBounds {
256    /// The knob-capped default (`NROS_RUNTIME_MAX_CELL_ENTITIES` per kind).
257    pub const fn knob_caps() -> Self {
258        Self {
259            publishers: crate::config::MAX_CELL_ENTITIES,
260            service_servers: crate::config::MAX_CELL_ENTITIES,
261            service_clients: crate::config::MAX_CELL_ENTITIES,
262            action_clients: crate::config::MAX_CELL_ENTITIES,
263            action_servers: crate::config::MAX_CELL_ENTITIES,
264        }
265    }
266
267    /// Exact bounds, spelled positionally:
268    /// `(publishers, service_servers, service_clients, action_clients, action_servers)`.
269    pub const fn exact(
270        publishers: usize,
271        service_servers: usize,
272        service_clients: usize,
273        action_clients: usize,
274        action_servers: usize,
275    ) -> Self {
276        Self {
277            publishers,
278            service_servers,
279            service_clients,
280            action_clients,
281            action_servers,
282        }
283    }
284}
285
286/// Rust component entry point.
287#[cfg(feature = "rmw-cffi")]
288pub trait Component {
289    /// Source component name used in metadata and diagnostics.
290    const NAME: &'static str;
291
292    /// Phase 216.A.3 — declares which dispatch strategy this Node
293    /// requires from the runtime. Defaults to
294    /// [`crate::DispatchStrategy::Inline`] so every existing component
295    /// keeps compiling without source change; the substrate (Phase
296    /// 216.A.2) and `nros check` (Phase 216.D.1) consume it to
297    /// pick / validate the board-side dispatch path.
298    const DISPATCH: crate::DispatchStrategy = crate::DispatchStrategy::Inline;
299
300    /// phase-391 W5-endgame step 2c — this class's per-kind entity bounds,
301    /// sizing its static cell registries. Defaults to the knob caps so every
302    /// existing component keeps compiling; declare [`EntityBounds::exact`]
303    /// to stop paying for capacity `register()` never fills.
304    const ENTITY_BOUNDS: EntityBounds = EntityBounds::knob_caps();
305
306    /// Declare nodes, entities, callbacks, params, and optional effects.
307    fn register(context: &mut NodeContext<'_>) -> NodeResult<()>;
308}
309
310/// Runtime-neutral node construction options.
311///
312/// ADOPT-BOUNDED against rclrs's `NodeOptions` / `IntoNodeOptions` builder: three
313/// public fields (`name`, `namespace`, `domain_id`) and nothing else. There are no
314/// node ARGUMENTS and no rosout toggle — both are fixed at build time here
315/// (RFC-0045), so an rclrs builder call for either is a compile error.
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub struct NodeOptions<'a> {
318    /// Source node name. Launch planning may remap/namespace later.
319    pub name: &'a str,
320    /// Source namespace. Defaults to `/`.
321    pub namespace: &'a str,
322    /// ROS domain ID hint. Defaults to `0`.
323    pub domain_id: u32,
324}
325
326/// Runtime callback event delivered to an executable Node.
327///
328/// The value carries the source callback name declared by the component, but
329/// does not expose the generated/internal callback ID type to product code.
330#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
331pub struct Callback<'a> {
332    id: CallbackId<'a>,
333}
334
335impl<'a> Callback<'a> {
336    /// Borrow the source callback name.
337    pub const fn as_str(self) -> &'a str {
338        self.id.as_str()
339    }
340
341    /// Return true when this callback matches `name`.
342    pub fn is_named(self, name: &str) -> bool {
343        self.as_str() == name
344    }
345
346    /// Build a callback event from the internal/generated callback ID.
347    #[doc(hidden)]
348    pub const fn __from_id(id: CallbackId<'a>) -> Self {
349        Self { id }
350    }
351}
352
353impl<'a> NodeOptions<'a> {
354    /// Create node options with default namespace and domain.
355    pub const fn new(name: &'a str) -> Self {
356        Self {
357            name,
358            namespace: "/",
359            domain_id: 0,
360        }
361    }
362
363    /// Set source namespace.
364    pub const fn namespace(mut self, namespace: &'a str) -> Self {
365        self.namespace = namespace;
366        self
367    }
368
369    /// Set ROS domain ID hint.
370    pub const fn domain_id(mut self, domain_id: u32) -> Self {
371        self.domain_id = domain_id;
372        self
373    }
374}
375
376/// Declaration sink implemented by metadata recorders and generated runtimes.
377pub trait NodeRuntime {
378    /// Declare a component node.
379    fn create_node(&mut self, id: NodeId<'_>, options: NodeOptions<'_>) -> NodeResult<()>;
380
381    /// Declare a publisher, subscription, timer, service, action, or parameter.
382    fn create_entity(&mut self, metadata: EntityMetadata) -> NodeResult<()>;
383
384    /// Add optional callback effect metadata.
385    fn record_callback_effect(
386        &mut self,
387        callback_id: CallbackId<'_>,
388        kind: CallbackEffectKind,
389        entity_id: EntityId<'_>,
390    ) -> NodeResult<()>;
391}
392
393impl<const MAX_NODES: usize, const MAX_ENTITIES: usize, const MAX_CALLBACKS: usize> NodeRuntime
394    for MetadataRecorder<MAX_NODES, MAX_ENTITIES, MAX_CALLBACKS>
395{
396    fn create_node(&mut self, id: NodeId<'_>, options: NodeOptions<'_>) -> NodeResult<()> {
397        self.push_node(id, options.name, options.namespace, options.domain_id)?;
398        Ok(())
399    }
400
401    fn create_entity(&mut self, mut metadata: EntityMetadata) -> NodeResult<()> {
402        // phase-457 W5 (issue 1522) — state this subscription's registration
403        // path, because on THIS road nothing will observe it.
404        //
405        // `Executor::open_subscription` reports `in_place_capable` to
406        // `registration_observer`, which is how the C/C++ probes get theirs.
407        // The Rust probe (`record_node_metadata::<C>`) runs `register()`
408        // against this recorder and opens no executor, so that seam never
409        // fires and every Rust endpoint's row refused — leaving every consumer
410        // on the receive-region budget for a fact the declaration determines.
411        //
412        // This is the ONE `NodeRuntime` seam a Rust declaration crosses; the
413        // C/C++ adapters reach the recorder through `push_entity` and are
414        // untouched, so their observed fact still wins its own way.
415        //
416        // Not a second opinion: `declared_subscription_shape` is the same call
417        // the declarative registrar branches on, and its
418        // `in_place_capable()` is the same expression the entry point writes
419        // into its `SubscriptionRequest`. Gated by
420        // `check-declared-subscription-shape`.
421        if metadata.kind == EntityKind::Subscription && metadata.in_place_capable.is_none() {
422            metadata.in_place_capable =
423                Some(metadata.declared_subscription_shape().in_place_capable());
424        }
425        self.push_entity(metadata)?;
426        Ok(())
427    }
428
429    fn record_callback_effect(
430        &mut self,
431        callback_id: CallbackId<'_>,
432        kind: CallbackEffectKind,
433        entity_id: EntityId<'_>,
434    ) -> NodeResult<()> {
435        self.push_callback_effect(callback_id, kind, entity_id)?;
436        Ok(())
437    }
438}
439
440/// phase-483 W3 — where a component's declarations go while its `register`
441/// runs: the live component runtime (`ExecutorSink`, which owns the
442/// component's cell) or the metadata recorder (the host probe). Both receive
443/// the executor explicitly, because the [`crate::Node`] a component holds
444/// borrows that same executor; a sink that held it too would alias it.
445#[cfg(feature = "rmw-cffi")]
446pub(crate) trait FrameSink {
447    /// Create the executor node a component asked for and return its id.
448    fn create_node(
449        &mut self,
450        executor: &mut crate::Executor<'static>,
451        id: NodeId<'_>,
452        options: NodeOptions<'_>,
453    ) -> NodeResult<nros_node::executor::NodeId>;
454
455    /// Declare one entity.
456    fn create_entity(
457        &mut self,
458        executor: &mut crate::Executor<'static>,
459        metadata: EntityMetadata,
460    ) -> NodeResult<()>;
461
462    /// Record one callback effect.
463    fn record_callback_effect(
464        &mut self,
465        executor: &mut crate::Executor<'static>,
466        callback_id: CallbackId<'_>,
467        kind: CallbackEffectKind,
468        entity_id: EntityId<'_>,
469    ) -> NodeResult<()>;
470}
471
472/// The most nodes one component registration may create. A component
473/// declares one in every shipped shape.
474#[cfg(feature = "rmw-cffi")]
475const MAX_FRAME_NODES: usize = 4;
476
477#[cfg(feature = "rmw-cffi")]
478struct FrameNode {
479    exec_id: nros_node::executor::NodeId,
480    stable: MetadataString,
481    group: Option<MetadataString>,
482}
483
484/// phase-483 W3 — one component registration in progress. Installed on the
485/// executor (`Executor::__set_component_frame`) for exactly the lifetime of
486/// the [`NodeContext`] that owns it, which is how a [`DeclarativeNode`]
487/// method on a plain [`crate::Node`] finds the sink.
488#[cfg(feature = "rmw-cffi")]
489pub(crate) struct ComponentFrame<'a> {
490    pub(crate) sink: &'a mut dyn FrameSink,
491    nodes: Vec<FrameNode, MAX_FRAME_NODES>,
492}
493
494#[cfg(feature = "rmw-cffi")]
495impl<'a> ComponentFrame<'a> {
496    pub(crate) fn new(sink: &'a mut dyn FrameSink) -> Self {
497        Self {
498            sink,
499            nodes: Vec::new(),
500        }
501    }
502
503    fn push(&mut self, exec_id: nros_node::executor::NodeId, stable: &str) -> NodeResult<()> {
504        self.nodes
505            .push(FrameNode {
506                exec_id,
507                stable: copy_str(stable)?,
508                group: None,
509            })
510            .map_err(|_| NodeDeclError::Metadata(NodeMetadataError::Capacity))
511    }
512
513    fn node(&self, exec_id: nros_node::executor::NodeId) -> NodeResult<&FrameNode> {
514        self.nodes
515            .iter()
516            .find(|n| n.exec_id == exec_id)
517            .ok_or(NodeDeclError::Metadata(NodeMetadataError::UnknownNode))
518    }
519
520    fn stable_id_of(&self, exec_id: nros_node::executor::NodeId) -> NodeResult<NodeId<'_>> {
521        Ok(NodeId::new(self.node(exec_id)?.stable.as_str()))
522    }
523
524    fn group_of(&self, exec_id: nros_node::executor::NodeId) -> Option<MetadataString> {
525        self.node(exec_id).ok().and_then(|n| n.group.clone())
526    }
527
528    fn set_group(&mut self, stable: &str, group: Option<MetadataString>) -> NodeResult<()> {
529        let node = self
530            .nodes
531            .iter_mut()
532            .find(|n| n.stable.as_str() == stable)
533            .ok_or(NodeDeclError::Metadata(NodeMetadataError::UnknownNode))?;
534        node.group = group;
535        Ok(())
536    }
537}
538
539/// The registration frame installed on `executor`, or `Runtime` when no
540/// component registration is in progress.
541///
542/// The `'static` is a lie told to the borrow checker and kept honest by
543/// [`NodeContext`]: the frame is installed when the context is built and
544/// cleared when it drops, and every caller here runs inside that window and
545/// drops the reference before returning.
546#[cfg(feature = "rmw-cffi")]
547fn frame_mut(
548    executor: &crate::Executor<'static>,
549) -> NodeResult<&'static mut ComponentFrame<'static>> {
550    let ptr = executor.__component_frame().ok_or(NodeDeclError::Runtime)?;
551    // SAFETY: installed by `NodeContext::new` from a `&mut ComponentFrame`
552    // that outlives the context, and cleared by its `Drop`; the frame is
553    // separate memory from the executor, so this does not alias the
554    // executor borrow the caller also holds.
555    Ok(unsafe { &mut *ptr.cast::<ComponentFrame<'static>>().as_ptr() })
556}
557
558/// The metadata recorder as a registration sink — the host probe's road.
559///
560/// It records every declaration and ALSO creates the node on the executor
561/// the probe opened on the `metadata` backend, so the component's `register`
562/// gets a real [`crate::Node`] exactly as it does on a board.
563#[cfg(feature = "rmw-cffi")]
564pub(crate) struct RecordSink<'r> {
565    pub(crate) recorder: &'r mut dyn NodeRuntime,
566}
567
568#[cfg(feature = "rmw-cffi")]
569impl FrameSink for RecordSink<'_> {
570    fn create_node(
571        &mut self,
572        executor: &mut crate::Executor<'static>,
573        id: NodeId<'_>,
574        options: NodeOptions<'_>,
575    ) -> NodeResult<nros_node::executor::NodeId> {
576        self.recorder.create_node(id, options)?;
577        executor
578            .node_builder(options.name)
579            .namespace(options.namespace)
580            .domain_id(options.domain_id)
581            .build()
582            .map_err(|_| NodeDeclError::Runtime)
583    }
584
585    fn create_entity(
586        &mut self,
587        _executor: &mut crate::Executor<'static>,
588        metadata: EntityMetadata,
589    ) -> NodeResult<()> {
590        self.recorder.create_entity(metadata)
591    }
592
593    fn record_callback_effect(
594        &mut self,
595        _executor: &mut crate::Executor<'static>,
596        callback_id: CallbackId<'_>,
597        kind: CallbackEffectKind,
598        entity_id: EntityId<'_>,
599    ) -> NodeResult<()> {
600        self.recorder
601            .record_callback_effect(callback_id, kind, entity_id)
602    }
603}
604
605/// Node declaration context — what a component's
606/// [`register`](Component::register) receives.
607///
608/// phase-483 W3: it creates REAL nodes. [`create_node`](Self::create_node)
609/// returns a [`crate::Node`], the same type `Executor::create_node` gives a
610/// standalone program, so a component's node code and a program's node code
611/// are one code. The component-only declarations are the
612/// [`DeclarativeNode`] methods on that node.
613#[cfg(feature = "rmw-cffi")]
614pub struct NodeContext<'a> {
615    component_name: &'static str,
616    executor: &'a mut crate::Executor<'static>,
617    /// Phase 264 W4a — this node instance's parameters, the COMPILE-BAKED initial
618    /// values from the launch `<param name=… value=…/>` entries (`nros::main!`
619    /// bakes them + threads them through `install_node_typed_with_params`). Empty
620    /// for the metadata-recorder / no-launch paths. A node reads them in
621    /// `register()` via [`param`](Self::param) and stashes the typed value on its
622    /// `State` (RFC-0004 §10 — baked initials; runtime reconfig is W4b).
623    params: &'a [(&'a str, &'a str)],
624}
625
626#[cfg(feature = "rmw-cffi")]
627impl<'a> NodeContext<'a> {
628    /// Build a context over `executor`, installing `frame` on it until this
629    /// context drops.
630    pub(crate) fn new(
631        component_name: &'static str,
632        executor: &'a mut crate::Executor<'static>,
633        frame: &'a mut ComponentFrame<'_>,
634    ) -> Self {
635        executor.__set_component_frame(Some(core::ptr::NonNull::from(frame).cast()));
636        Self {
637            component_name,
638            executor,
639            params: &[],
640        }
641    }
642
643    /// Phase 264 W4a — seed this node instance's baked launch parameters (called by
644    /// `install_node_typed_with_params` before `Component::register`).
645    pub fn set_params(&mut self, params: &'a [(&'a str, &'a str)]) {
646        self.params = params;
647    }
648
649    /// Phase 264 W4a — the baked initial value of launch parameter `name`, or
650    /// `None` if the launch declared no `<param name="…"/>` for this node
651    /// instance. Read in `register()` and parse/stash on `State` (RFC-0004 §10).
652    pub fn param(&self, name: &str) -> Option<&'a str> {
653        self.params
654            .iter()
655            .find(|(k, _)| *k == name)
656            .map(|(_, v)| *v)
657    }
658
659    /// Source component name.
660    pub const fn component_name(&self) -> &'static str {
661        self.component_name
662    }
663
664    /// Create a node with an explicit stable node ID.
665    ///
666    /// Generated/internal form; product code should use
667    /// [`create_node`](Self::create_node).
668    #[doc(hidden)]
669    pub fn create_node_with_id(
670        &mut self,
671        id: NodeId<'_>,
672        options: NodeOptions<'_>,
673    ) -> NodeResult<crate::Node<'_, 'static>> {
674        let frame = frame_mut(self.executor)?;
675        let exec_id = frame.sink.create_node(self.executor, id, options)?;
676        frame.push(exec_id, id.as_str())?;
677        Ok(self.executor.node_mut(exec_id))
678    }
679
680    /// Create this component's node — rclrs's `executor.create_node(options)`.
681    ///
682    /// The node's name is the launch file's when it names one, otherwise
683    /// `options.name`, which is also the component-stable node id the
684    /// declarations are keyed on.
685    pub fn create_node(
686        &mut self,
687        options: NodeOptions<'_>,
688    ) -> NodeResult<crate::Node<'_, 'static>> {
689        self.create_node_with_id(NodeId::new(options.name), options)
690    }
691
692    /// Record optional effects for a callback not tied to a node.
693    #[doc(hidden)]
694    pub fn callback<'id>(&mut self, id: CallbackId<'id>) -> CallbackEffects<'_, 'id> {
695        CallbackEffects {
696            executor: self.executor,
697            id,
698        }
699    }
700}
701
702#[cfg(feature = "rmw-cffi")]
703impl Drop for NodeContext<'_> {
704    fn drop(&mut self) {
705        self.executor.__set_component_frame(None);
706    }
707}
708
709/// phase-483 W3 — the DECLARATIVE entity constructors, on the one node type.
710///
711/// A component's `register` creates its node with
712/// [`NodeContext::create_node`] and gets a [`crate::Node`], the same type a
713/// standalone program gets from `Executor::create_node`. Everything rclrs's
714/// node has is an inherent method there. The constructors here are the RTOS
715/// extension a component adds on top: each one DECLARES an entity — a stable
716/// id, a callback NAME, effects — to the component runtime, which owns the
717/// entity in the component's static cell and dispatches its callback by name
718/// to [`ExecutableNode::on_callback`]. That is what framework dispatch
719/// (RTIC/Embassy) and the metadata probe need, and what a closure cannot give
720/// them.
721///
722/// Only callable inside a component's `register`: outside one there is no
723/// component to declare into, and every method answers
724/// [`NodeDeclError::Runtime`].
725///
726/// The explicit-id forms that would collide with an rclrs-named inherent
727/// method on `Node` are spelled `declare_*` (`declare_publisher`,
728/// `declare_subscription`, `declare_timer`, …).
729#[cfg(feature = "rmw-cffi")]
730pub trait DeclarativeNode {
731    /// The component-stable id of this node. Not a user API.
732    #[doc(hidden)]
733    fn __node_id(&mut self) -> NodeResult<NodeId<'static>>;
734
735    /// Hand one entity declaration to the component runtime. Not a user API.
736    #[doc(hidden)]
737    fn __declare_entity(&mut self, metadata: EntityMetadata) -> NodeResult<()>;
738
739    /// The executor the node borrows. Not a user API.
740    #[doc(hidden)]
741    fn __executor_mut(&mut self) -> &mut crate::Executor<'static>;
742
743    /// Set the sticky callback-group label applied to every entity
744    /// declared after this call (until changed again). The group is the
745    /// symbolic name the node author exposes; `system.toml` maps it to a
746    /// scheduling tier (RFC-0015). Entities declared while no group is set
747    /// remain unlabeled (wildcard-eligible).
748    #[track_caller]
749    fn callback_group(&mut self, group: &str) -> NodeResult<&mut Self>
750    where
751        Self: Sized,
752    {
753        let group = copy_str(group)?;
754        let executor = self.__executor_mut() as *mut crate::Executor<'static>;
755        // SAFETY: `executor` is the node's own borrow, live for this call.
756        let frame = frame_mut(unsafe { &*executor })?;
757        let node = self.__node_id()?;
758        frame.set_group(node.as_str(), Some(group))?;
759        Ok(self)
760    }
761
762    /// Declare a publisher with default QoS. Stable publisher ID is required.
763    #[track_caller]
764    #[doc(hidden)]
765    fn declare_publisher<'entity, M: MessageForRmw>(
766        &mut self,
767        id: EntityId<'entity>,
768        topic: &str,
769    ) -> NodeResult<NodePublisher<'entity, M>> {
770        self.declare_publisher_with_qos::<M>(id, topic, QoSProfile::default())
771    }
772
773    /// Declare a publisher using `topic` as the stable entity ID.
774    ///
775    /// Use the explicit [`declare_publisher`](Self::declare_publisher) form when
776    /// a node declares more than one publisher on the same topic or needs a
777    /// stable metadata ID that differs from the ROS topic name.
778    #[track_caller]
779    fn create_publisher_for_topic<'entity, M: MessageForRmw>(
780        &mut self,
781        topic: &'entity str,
782    ) -> NodeResult<NodePublisher<'entity, M>> {
783        self.create_publisher_for_topic_with_qos::<M>(topic, QoSProfile::default())
784    }
785
786    /// Declare a publisher with explicit QoS, using `topic` as the stable entity ID.
787    #[track_caller]
788    fn create_publisher_for_topic_with_qos<'entity, M: MessageForRmw>(
789        &mut self,
790        topic: &'entity str,
791        qos: QoSProfile,
792    ) -> NodeResult<NodePublisher<'entity, M>> {
793        self.declare_publisher_with_qos::<M>(EntityId::new(topic), topic, qos)
794    }
795
796    /// Declare a publisher with explicit QoS.
797    #[track_caller]
798    #[doc(hidden)]
799    fn declare_publisher_with_qos<'entity, M: MessageForRmw>(
800        &mut self,
801        id: EntityId<'entity>,
802        topic: &str,
803        qos: QoSProfile,
804    ) -> NodeResult<NodePublisher<'entity, M>> {
805        register_declared_type::<M>()?;
806        let mut metadata = entity_metadata(EntityMetadataSpec {
807            id,
808            node_id: self.__node_id()?,
809            kind: EntityKind::Publisher,
810            source_name: topic,
811            // issue 0413 — `MessageForRmw` can pull `schema::Message` into scope,
812            // which also has a `TYPE_NAME`; disambiguate to the ROS-facing one.
813            type_name: <M as RosMessage>::TYPE_NAME,
814            type_hash: <M as RosMessage>::TYPE_HASH,
815            qos,
816        })?;
817        metadata.source = SourceLocationMetadata::caller()?;
818        self.__declare_entity(metadata)?;
819        Ok(NodePublisher::new(id))
820    }
821
822    /// Declare a subscription. Stable subscription and callback IDs are required.
823    #[track_caller]
824    #[doc(hidden)]
825    fn declare_subscription<'entity, 'callback, M: MessageForRmw>(
826        &mut self,
827        id: EntityId<'entity>,
828        callback_id: CallbackId<'callback>,
829        topic: &str,
830    ) -> NodeResult<NodeSubscription<'entity, M>> {
831        self.declare_subscription_with_qos::<M>(id, callback_id, topic, QoSProfile::default())
832    }
833
834    /// Declare a subscription using `callback_id` as the stable entity ID.
835    ///
836    /// Generated/internal form; product code should use
837    /// [`create_subscription_for_callback_name`](Self::create_subscription_for_callback_name).
838    #[track_caller]
839    #[doc(hidden)]
840    fn create_subscription_for_callback<'callback, M: MessageForRmw>(
841        &mut self,
842        callback_id: CallbackId<'callback>,
843        topic: &str,
844    ) -> NodeResult<NodeSubscription<'callback, M>> {
845        self.create_subscription_for_callback_with_qos::<M>(
846            callback_id,
847            topic,
848            QoSProfile::default(),
849        )
850    }
851
852    /// Declare a subscription using `callback_name` as the source callback
853    /// name and synthesized entity ID.
854    #[track_caller]
855    fn create_subscription_for_callback_name<'callback, M: MessageForRmw>(
856        &mut self,
857        callback_name: &'callback str,
858        topic: &str,
859    ) -> NodeResult<NodeSubscription<'callback, M>> {
860        self.create_subscription_for_callback::<M>(CallbackId::new(callback_name), topic)
861    }
862
863    /// Phase 250 (Wave 2b) — declare a subscription with E2E message-integrity
864    /// validation enabled (the declarative `.safety()` opt-in). Identical to
865    /// [`create_subscription_for_callback_name`](Self::create_subscription_for_callback_name)
866    /// but flags the entity so the runtime registers it via
867    /// `create_generic_subscription_with_integrity`; the callback then reads
868    /// [`CallbackCtx::integrity`](CallbackCtx::integrity) alongside the message.
869    /// The config-driven `[safety]` axis (Wave 4 codegen) emits this call; it is
870    /// also usable by hand. Ungated — when `safety-e2e` is off the flag is simply
871    /// ignored and the subscription registers as a basic one.
872    #[track_caller]
873    fn create_subscription_for_callback_name_with_safety<'callback, M: MessageForRmw>(
874        &mut self,
875        callback_name: &'callback str,
876        topic: &str,
877    ) -> NodeResult<NodeSubscription<'callback, M>> {
878        let callback_id = CallbackId::new(callback_name);
879        let id = EntityId::new(callback_id.as_str());
880        register_declared_type::<M>()?;
881        let mut metadata = entity_metadata(EntityMetadataSpec {
882            id,
883            node_id: self.__node_id()?,
884            kind: EntityKind::Subscription,
885            source_name: topic,
886            // issue 0413 — `MessageForRmw` can pull `schema::Message` into scope,
887            // which also has a `TYPE_NAME`; disambiguate to the ROS-facing one.
888            type_name: <M as RosMessage>::TYPE_NAME,
889            type_hash: <M as RosMessage>::TYPE_HASH,
890            qos: QoSProfile::default(),
891        })?;
892        metadata.callback_id = Some(copy_str(callback_id.as_str())?);
893        metadata.callback_source = SourceLocationMetadata::caller()?;
894        metadata.source = metadata.callback_source.clone();
895        metadata.safety = true;
896        self.__declare_entity(metadata)?;
897        Ok(NodeSubscription::new(id))
898    }
899
900    /// Declare a subscription with explicit QoS, using `callback_id` as the stable entity ID.
901    #[track_caller]
902    #[doc(hidden)]
903    fn create_subscription_for_callback_with_qos<'callback, M: MessageForRmw>(
904        &mut self,
905        callback_id: CallbackId<'callback>,
906        topic: &str,
907        qos: QoSProfile,
908    ) -> NodeResult<NodeSubscription<'callback, M>> {
909        self.declare_subscription_with_qos::<M>(
910            EntityId::new(callback_id.as_str()),
911            callback_id,
912            topic,
913            qos,
914        )
915    }
916
917    /// Declare a subscription using `topic` as both the stable entity ID and callback ID.
918    #[track_caller]
919    fn create_subscription_for_topic<'entity, M: MessageForRmw>(
920        &mut self,
921        topic: &'entity str,
922    ) -> NodeResult<NodeSubscription<'entity, M>> {
923        self.create_subscription_for_topic_with_qos::<M>(topic, QoSProfile::default())
924    }
925
926    /// Declare a subscription with explicit QoS, using `topic` as both IDs.
927    #[track_caller]
928    fn create_subscription_for_topic_with_qos<'entity, M: MessageForRmw>(
929        &mut self,
930        topic: &'entity str,
931        qos: QoSProfile,
932    ) -> NodeResult<NodeSubscription<'entity, M>> {
933        self.declare_subscription_with_qos::<M>(
934            EntityId::new(topic),
935            CallbackId::new(topic),
936            topic,
937            qos,
938        )
939    }
940
941    /// Declare a subscription with explicit QoS.
942    #[track_caller]
943    #[doc(hidden)]
944    fn declare_subscription_with_qos<'entity, 'callback, M: MessageForRmw>(
945        &mut self,
946        id: EntityId<'entity>,
947        callback_id: CallbackId<'callback>,
948        topic: &str,
949        qos: QoSProfile,
950    ) -> NodeResult<NodeSubscription<'entity, M>> {
951        // Phase 380 W4 — a subscription whose receive buffer provably cannot
952        // hold its own message type fails the BUILD, not the field.
953        //
954        // Without this the sample is received, ACKed, and then dropped, and
955        // `report_dropped_take` can only say "raise the knob" because nothing
956        // knows what value would have worked (issues 0757, 0776). The number is
957        // known at compile time for any BOUNDED type, so the check costs
958        // nothing at runtime and cannot be forgotten at a call site.
959        //
960        // `bound_fits`, not `buffer_fits`: an UNBOUNDED type passes, because
961        // there is nothing to prove and no finite buffer fits a `String` — the
962        // other predicate would refuse the most common message in ROS. Both
963        // encodings are checked and the larger taken; the peer chooses the
964        // encoding at runtime, so sizing from XCDR1 alone is a trap.
965        // Only where `MessageForRmw` guarantees a schema. The other arm accepts a
966        // hand-written `RosMessage` with none (see `rmw_type_registry`), and
967        // requiring one there would make codegen mandatory for a user's own
968        // message type — too large a price for a build assertion. Backends that
969        // register type descriptors already demand the schema, so this is free
970        // there and absent elsewhere; `size::bound_fits` stays public so a
971        // caller can assert it explicitly.
972        const {
973            assert!(
974                nros_node::rmw_type_registry::subscription_buffer_ok::<M>(),
975                "this message type's maximum serialized size exceeds \
976                 NROS_SUBSCRIPTION_BUFFER_SIZE — every sample would be received, \
977                 ACKed and then DROPPED. Raise the knob to at least the type's \
978                 bound (`<M as nros_serdes::schema::Message>::MAX_SERIALIZED_SIZE_XCDR2`)."
979            )
980        }
981        register_declared_type::<M>()?;
982        let mut metadata = entity_metadata(EntityMetadataSpec {
983            id,
984            node_id: self.__node_id()?,
985            kind: EntityKind::Subscription,
986            source_name: topic,
987            // issue 0413 — `MessageForRmw` can pull `schema::Message` into scope,
988            // which also has a `TYPE_NAME`; disambiguate to the ROS-facing one.
989            type_name: <M as RosMessage>::TYPE_NAME,
990            type_hash: <M as RosMessage>::TYPE_HASH,
991            qos,
992        })?;
993        metadata.callback_id = Some(copy_str(callback_id.as_str())?);
994        metadata.callback_source = SourceLocationMetadata::caller()?;
995        metadata.source = metadata.callback_source.clone();
996        self.__declare_entity(metadata)?;
997        Ok(NodeSubscription::new(id))
998    }
999
1000    /// Declare a subscription whose stable entity and callback IDs are
1001    /// both synthesized from the topic literal, returning a
1002    /// [`SubscriptionTag`] the Node author stores on `Self::State` and
1003    /// matches against the `Callback<'_>` delivered to
1004    /// [`ExecutableNode::on_callback`].
1005    ///
1006    /// Use this on the Phase 216.A Deferred Node path where the Node
1007    /// author does not need to invent a separate stable entity ID — the
1008    /// topic literal becomes both the entity ID and the callback ID,
1009    /// and the returned tag preserves that identifier for compile-time
1010    /// `state.sub_chatter == cb` matches in `on_callback`.
1011    #[track_caller]
1012    fn create_subscription_static<M: MessageForRmw>(
1013        &mut self,
1014        topic: &'static str,
1015    ) -> NodeResult<SubscriptionTag> {
1016        let id = EntityId::new(topic);
1017        let callback_id = CallbackId::new(topic);
1018        register_declared_type::<M>()?;
1019        let mut metadata = entity_metadata(EntityMetadataSpec {
1020            id,
1021            node_id: self.__node_id()?,
1022            kind: EntityKind::Subscription,
1023            source_name: topic,
1024            // issue 0413 — `MessageForRmw` can pull `schema::Message` into scope,
1025            // which also has a `TYPE_NAME`; disambiguate to the ROS-facing one.
1026            type_name: <M as RosMessage>::TYPE_NAME,
1027            type_hash: <M as RosMessage>::TYPE_HASH,
1028            qos: QoSProfile::default(),
1029        })?;
1030        metadata.callback_id = Some(copy_str(callback_id.as_str())?);
1031        metadata.callback_source = SourceLocationMetadata::caller()?;
1032        metadata.source = metadata.callback_source.clone();
1033        self.__declare_entity(metadata)?;
1034        Ok(SubscriptionTag::new(topic))
1035    }
1036
1037    /// Declare a timer. Stable timer and callback IDs are required.
1038    #[track_caller]
1039    #[doc(hidden)]
1040    fn declare_timer<'entity, 'callback>(
1041        &mut self,
1042        id: EntityId<'entity>,
1043        callback_id: CallbackId<'callback>,
1044        period: TimerDuration,
1045    ) -> NodeResult<NodeTimer<'entity>> {
1046        // phase-430 W4 — the wall spelling IS the `Steady` clock, so it is the
1047        // same declaration with the axis defaulted rather than a second body.
1048        // `#[track_caller]` on both hops keeps `SourceLocationMetadata::caller`
1049        // pointing at the component, not at this line.
1050        self.declare_timer_on_clock(id, callback_id, period, TimerClockSource::Steady)
1051    }
1052
1053    /// Declare a timer using `callback_id` as the stable timer entity ID.
1054    #[track_caller]
1055    #[doc(hidden)]
1056    fn create_timer_for_callback<'callback>(
1057        &mut self,
1058        callback_id: CallbackId<'callback>,
1059        period: TimerDuration,
1060    ) -> NodeResult<NodeTimer<'callback>> {
1061        self.declare_timer(EntityId::new(callback_id.as_str()), callback_id, period)
1062    }
1063
1064    /// Declare a timer using `callback_name` as the source callback name and
1065    /// synthesized entity ID.
1066    #[track_caller]
1067    fn create_timer_for_callback_name<'callback>(
1068        &mut self,
1069        callback_name: &'callback str,
1070        period: TimerDuration,
1071    ) -> NodeResult<NodeTimer<'callback>> {
1072        self.create_timer_for_callback(CallbackId::new(callback_name), period)
1073    }
1074
1075    /// Declare a timer on a chosen CLOCK — phase-430 W4, the declarative
1076    /// spelling of [`Executor::register_timer_on_clock`](crate::Executor::register_timer_on_clock)
1077    /// and the counterpart of C++'s `create_timer(clock, period, cb)`.
1078    ///
1079    /// [`create_timer`](Self::create_timer) is this with
1080    /// [`TimerClockSource::Steady`], which is the WALL case and is what every
1081    /// timer declared before W4 gets: the runtime registers it exactly as it
1082    /// always did. A [`TimerClockSource::Ros`] timer instead follows `/clock` —
1083    /// it stops while the simulator is paused and tracks a bag's replay rate —
1084    /// and with no `/clock` source installed it reads system time, the same
1085    /// fallback `rclcpp::Clock` has.
1086    #[track_caller]
1087    #[doc(hidden)]
1088    fn declare_timer_on_clock<'entity, 'callback>(
1089        &mut self,
1090        id: EntityId<'entity>,
1091        callback_id: CallbackId<'callback>,
1092        period: TimerDuration,
1093        clock: TimerClockSource,
1094    ) -> NodeResult<NodeTimer<'entity>> {
1095        let mut metadata = entity_metadata(EntityMetadataSpec {
1096            id,
1097            node_id: self.__node_id()?,
1098            kind: EntityKind::Timer,
1099            source_name: "",
1100            type_name: "",
1101            type_hash: "",
1102            qos: QoSProfile::default(),
1103        })?;
1104        metadata.callback_id = Some(copy_str(callback_id.as_str())?);
1105        metadata.callback_source = SourceLocationMetadata::caller()?;
1106        metadata.source = metadata.callback_source.clone();
1107        metadata.period_ms = Some(period.as_millis());
1108        metadata.period_us = Some(period.as_micros());
1109        metadata.timer_clock = clock;
1110        self.__declare_entity(metadata)?;
1111        Ok(NodeTimer::new(id))
1112    }
1113
1114    /// [`declare_timer_on_clock`](Self::declare_timer_on_clock) using
1115    /// `callback_id` as the stable timer entity ID.
1116    #[track_caller]
1117    #[doc(hidden)]
1118    fn create_timer_for_callback_on_clock<'callback>(
1119        &mut self,
1120        callback_id: CallbackId<'callback>,
1121        period: TimerDuration,
1122        clock: TimerClockSource,
1123    ) -> NodeResult<NodeTimer<'callback>> {
1124        self.declare_timer_on_clock(
1125            EntityId::new(callback_id.as_str()),
1126            callback_id,
1127            period,
1128            clock,
1129        )
1130    }
1131
1132    /// [`declare_timer_on_clock`](Self::declare_timer_on_clock) using
1133    /// `callback_name` as the source callback name and synthesized entity ID —
1134    /// the spelling a `nros::main!` component writes:
1135    ///
1136    /// ```ignore
1137    /// node.create_timer_for_callback_name_on_clock(
1138    ///     "on_tick",
1139    ///     TimerDuration::from_millis(100),
1140    ///     TimerClockSource::Ros,
1141    /// )?;
1142    /// ```
1143    #[track_caller]
1144    fn create_timer_for_callback_name_on_clock<'callback>(
1145        &mut self,
1146        callback_name: &'callback str,
1147        period: TimerDuration,
1148        clock: TimerClockSource,
1149    ) -> NodeResult<NodeTimer<'callback>> {
1150        self.create_timer_for_callback_on_clock(CallbackId::new(callback_name), period, clock)
1151    }
1152
1153    /// Declare a service server. Stable service and callback IDs are required.
1154    #[track_caller]
1155    #[doc(hidden)]
1156    fn create_service_server<
1157        'entity,
1158        'callback,
1159        S: RosService<
1160                Request: nros_node::rmw_type_registry::MessageForRmw,
1161                Reply: nros_node::rmw_type_registry::MessageForRmw,
1162            >,
1163    >(
1164        &mut self,
1165        id: EntityId<'entity>,
1166        callback_id: CallbackId<'callback>,
1167        service_name: &str,
1168    ) -> NodeResult<NodeServiceServer<'entity, S>> {
1169        register_declared_service::<S>()?;
1170        let mut metadata = entity_metadata(EntityMetadataSpec {
1171            id,
1172            node_id: self.__node_id()?,
1173            kind: EntityKind::ServiceServer,
1174            source_name: service_name,
1175            type_name: S::SERVICE_NAME,
1176            type_hash: S::SERVICE_HASH,
1177            qos: QoSProfile::default(),
1178        })?;
1179        metadata.callback_id = Some(copy_str(callback_id.as_str())?);
1180        metadata.callback_source = SourceLocationMetadata::caller()?;
1181        metadata.source = metadata.callback_source.clone();
1182        self.__declare_entity(metadata)?;
1183        Ok(NodeServiceServer::new(id))
1184    }
1185
1186    /// Declare a service server using `name` as both the stable entity ID
1187    /// and callback ID.
1188    #[track_caller]
1189    fn create_service_server_for_name<
1190        'entity,
1191        S: RosService<
1192                Request: nros_node::rmw_type_registry::MessageForRmw,
1193                Reply: nros_node::rmw_type_registry::MessageForRmw,
1194            >,
1195    >(
1196        &mut self,
1197        name: &'entity str,
1198    ) -> NodeResult<NodeServiceServer<'entity, S>> {
1199        self.create_service_server::<S>(EntityId::new(name), CallbackId::new(name), name)
1200    }
1201
1202    /// Declare a service server using `name` as the stable entity ID and
1203    /// `callback_name` as the source callback name.
1204    #[track_caller]
1205    fn create_service_server_for_name_with_callback<
1206        'entity,
1207        S: RosService<
1208                Request: nros_node::rmw_type_registry::MessageForRmw,
1209                Reply: nros_node::rmw_type_registry::MessageForRmw,
1210            >,
1211    >(
1212        &mut self,
1213        name: &'entity str,
1214        callback_name: &str,
1215    ) -> NodeResult<NodeServiceServer<'entity, S>> {
1216        self.create_service_server::<S>(EntityId::new(name), CallbackId::new(callback_name), name)
1217    }
1218
1219    /// Declare a service server whose stable entity and callback IDs are
1220    /// both synthesized from the service-name literal, returning a
1221    /// [`ServiceTag`] the Node author stores on `Self::State` and matches
1222    /// against the `Callback<'_>` delivered to
1223    /// [`ExecutableNode::on_callback`].
1224    ///
1225    /// Tag-only registration is restricted to the SERVER side: clients
1226    /// need a USABLE handle (`NodeServiceClient`) to issue requests, so
1227    /// use the existing
1228    /// [`create_service_client_for_name`](Self::create_service_client_for_name) builder
1229    /// for the client side.
1230    #[track_caller]
1231    fn create_service_static<
1232        S: RosService<
1233                Request: nros_node::rmw_type_registry::MessageForRmw,
1234                Reply: nros_node::rmw_type_registry::MessageForRmw,
1235            >,
1236    >(
1237        &mut self,
1238        name: &'static str,
1239    ) -> NodeResult<ServiceTag> {
1240        self.create_service_server_for_name::<S>(name)?;
1241        Ok(ServiceTag::new(name))
1242    }
1243
1244    /// Declare a service client. Stable service client ID is required.
1245    #[track_caller]
1246    #[doc(hidden)]
1247    fn create_service_client<
1248        'entity,
1249        S: RosService<
1250                Request: nros_node::rmw_type_registry::MessageForRmw,
1251                Reply: nros_node::rmw_type_registry::MessageForRmw,
1252            >,
1253    >(
1254        &mut self,
1255        id: EntityId<'entity>,
1256        service_name: &str,
1257    ) -> NodeResult<NodeServiceClient<'entity, S>> {
1258        register_declared_service::<S>()?;
1259        let mut metadata = entity_metadata(EntityMetadataSpec {
1260            id,
1261            node_id: self.__node_id()?,
1262            kind: EntityKind::ServiceClient,
1263            source_name: service_name,
1264            type_name: S::SERVICE_NAME,
1265            type_hash: S::SERVICE_HASH,
1266            qos: QoSProfile::default(),
1267        })?;
1268        metadata.source = SourceLocationMetadata::caller()?;
1269        self.__declare_entity(metadata)?;
1270        Ok(NodeServiceClient::new(id))
1271    }
1272
1273    /// Declare a service client using `name` as the stable entity ID.
1274    #[track_caller]
1275    fn create_service_client_for_name<
1276        'entity,
1277        S: RosService<
1278                Request: nros_node::rmw_type_registry::MessageForRmw,
1279                Reply: nros_node::rmw_type_registry::MessageForRmw,
1280            >,
1281    >(
1282        &mut self,
1283        name: &'entity str,
1284    ) -> NodeResult<NodeServiceClient<'entity, S>> {
1285        self.create_service_client::<S>(EntityId::new(name), name)
1286    }
1287
1288    /// Declare an action server. Stable action and callback IDs are required.
1289    #[track_caller]
1290    #[doc(hidden)]
1291    fn declare_action_server<
1292        'entity,
1293        'callback,
1294        A: RosAction<
1295                Goal: nros_node::rmw_type_registry::MessageForRmw,
1296                Result: nros_node::rmw_type_registry::MessageForRmw,
1297                Feedback: nros_node::rmw_type_registry::MessageForRmw,
1298                SendGoalRequest: nros_node::rmw_type_registry::MessageForRmw,
1299                SendGoalResponse: nros_node::rmw_type_registry::MessageForRmw,
1300                GetResultRequest: nros_node::rmw_type_registry::MessageForRmw,
1301                GetResultResponse: nros_node::rmw_type_registry::MessageForRmw,
1302                FeedbackMessage: nros_node::rmw_type_registry::MessageForRmw,
1303            >,
1304    >(
1305        &mut self,
1306        id: EntityId<'entity>,
1307        callback_id: CallbackId<'callback>,
1308        action_name: &str,
1309    ) -> NodeResult<NodeActionServer<'entity, A>> {
1310        self.create_action_server_with_callbacks::<A>(
1311            id,
1312            callback_id,
1313            callback_id,
1314            callback_id,
1315            action_name,
1316        )
1317    }
1318
1319    /// Declare an action server with distinct goal/cancel/accepted callbacks.
1320    #[track_caller]
1321    #[doc(hidden)]
1322    fn create_action_server_with_callbacks<
1323        'entity,
1324        'goal,
1325        'cancel,
1326        'accepted,
1327        A: RosAction<
1328                Goal: nros_node::rmw_type_registry::MessageForRmw,
1329                Result: nros_node::rmw_type_registry::MessageForRmw,
1330                Feedback: nros_node::rmw_type_registry::MessageForRmw,
1331                SendGoalRequest: nros_node::rmw_type_registry::MessageForRmw,
1332                SendGoalResponse: nros_node::rmw_type_registry::MessageForRmw,
1333                GetResultRequest: nros_node::rmw_type_registry::MessageForRmw,
1334                GetResultResponse: nros_node::rmw_type_registry::MessageForRmw,
1335                FeedbackMessage: nros_node::rmw_type_registry::MessageForRmw,
1336            >,
1337    >(
1338        &mut self,
1339        id: EntityId<'entity>,
1340        goal_callback_id: CallbackId<'goal>,
1341        cancel_callback_id: CallbackId<'cancel>,
1342        accepted_callback_id: CallbackId<'accepted>,
1343        action_name: &str,
1344    ) -> NodeResult<NodeActionServer<'entity, A>> {
1345        register_declared_action::<A>()?;
1346        let mut metadata = entity_metadata(EntityMetadataSpec {
1347            id,
1348            node_id: self.__node_id()?,
1349            kind: EntityKind::ActionServer,
1350            source_name: action_name,
1351            type_name: A::ACTION_NAME,
1352            type_hash: A::ACTION_HASH,
1353            qos: QoSProfile::default(),
1354        })?;
1355        metadata.action_hashes = Some(nros_node::ActionEndpointHashes::of::<A>());
1356        metadata.callback_id = Some(copy_str(goal_callback_id.as_str())?);
1357        metadata.callback_source = SourceLocationMetadata::caller()?;
1358        metadata.action_cancel_callback_id = Some(copy_str(cancel_callback_id.as_str())?);
1359        metadata.action_cancel_source = metadata.callback_source.clone();
1360        metadata.action_accepted_callback_id = Some(copy_str(accepted_callback_id.as_str())?);
1361        metadata.action_accepted_source = metadata.callback_source.clone();
1362        metadata.source = metadata.callback_source.clone();
1363        self.__declare_entity(metadata)?;
1364        Ok(NodeActionServer::new(id))
1365    }
1366
1367    /// Declare an action server using `name` as the stable entity ID and
1368    /// default goal/cancel/accepted callback ID.
1369    #[track_caller]
1370    fn create_action_server_for_name<
1371        'entity,
1372        A: RosAction<
1373                Goal: nros_node::rmw_type_registry::MessageForRmw,
1374                Result: nros_node::rmw_type_registry::MessageForRmw,
1375                Feedback: nros_node::rmw_type_registry::MessageForRmw,
1376                SendGoalRequest: nros_node::rmw_type_registry::MessageForRmw,
1377                SendGoalResponse: nros_node::rmw_type_registry::MessageForRmw,
1378                GetResultRequest: nros_node::rmw_type_registry::MessageForRmw,
1379                GetResultResponse: nros_node::rmw_type_registry::MessageForRmw,
1380                FeedbackMessage: nros_node::rmw_type_registry::MessageForRmw,
1381            >,
1382    >(
1383        &mut self,
1384        name: &'entity str,
1385    ) -> NodeResult<NodeActionServer<'entity, A>> {
1386        self.declare_action_server::<A>(EntityId::new(name), CallbackId::new(name), name)
1387    }
1388
1389    /// Declare an action server using `name` as the stable entity ID and
1390    /// explicit source callback names for goal, cancel, and accepted events.
1391    #[track_caller]
1392    fn create_action_server_for_name_with_callbacks<
1393        'entity,
1394        A: RosAction<
1395                Goal: nros_node::rmw_type_registry::MessageForRmw,
1396                Result: nros_node::rmw_type_registry::MessageForRmw,
1397                Feedback: nros_node::rmw_type_registry::MessageForRmw,
1398                SendGoalRequest: nros_node::rmw_type_registry::MessageForRmw,
1399                SendGoalResponse: nros_node::rmw_type_registry::MessageForRmw,
1400                GetResultRequest: nros_node::rmw_type_registry::MessageForRmw,
1401                GetResultResponse: nros_node::rmw_type_registry::MessageForRmw,
1402                FeedbackMessage: nros_node::rmw_type_registry::MessageForRmw,
1403            >,
1404    >(
1405        &mut self,
1406        name: &'entity str,
1407        goal_callback_name: &str,
1408        cancel_callback_name: &str,
1409        accepted_callback_name: &str,
1410    ) -> NodeResult<NodeActionServer<'entity, A>> {
1411        self.create_action_server_with_callbacks::<A>(
1412            EntityId::new(name),
1413            CallbackId::new(goal_callback_name),
1414            CallbackId::new(cancel_callback_name),
1415            CallbackId::new(accepted_callback_name),
1416            name,
1417        )
1418    }
1419
1420    /// Declare an action server whose stable entity and callback IDs are
1421    /// both synthesized from the action-name literal, returning an
1422    /// [`ActionTag`] the Node author stores on `Self::State` and matches
1423    /// against the `Callback<'_>` delivered to
1424    /// [`ExecutableNode::on_callback`].
1425    ///
1426    /// The synthesized callback ID is shared by the goal / cancel /
1427    /// accepted callbacks (matching the default behavior of
1428    /// [`declare_action_server`](Self::declare_action_server)).
1429    ///
1430    /// Tag-only registration is restricted to the SERVER side: clients
1431    /// need a USABLE handle (`NodeActionClient`) to dispatch goals, so
1432    /// use the existing
1433    /// [`create_action_client_for_name`](Self::create_action_client_for_name) builder
1434    /// for the client side.
1435    #[track_caller]
1436    fn create_action_static<
1437        A: RosAction<
1438                Goal: nros_node::rmw_type_registry::MessageForRmw,
1439                Result: nros_node::rmw_type_registry::MessageForRmw,
1440                Feedback: nros_node::rmw_type_registry::MessageForRmw,
1441                SendGoalRequest: nros_node::rmw_type_registry::MessageForRmw,
1442                SendGoalResponse: nros_node::rmw_type_registry::MessageForRmw,
1443                GetResultRequest: nros_node::rmw_type_registry::MessageForRmw,
1444                GetResultResponse: nros_node::rmw_type_registry::MessageForRmw,
1445                FeedbackMessage: nros_node::rmw_type_registry::MessageForRmw,
1446            >,
1447    >(
1448        &mut self,
1449        name: &'static str,
1450    ) -> NodeResult<ActionTag> {
1451        self.create_action_server_for_name::<A>(name)?;
1452        Ok(ActionTag::new(name))
1453    }
1454
1455    /// Declare an action client. Stable action client ID is required.
1456    #[track_caller]
1457    #[doc(hidden)]
1458    fn declare_action_client<
1459        'entity,
1460        A: RosAction<
1461                Goal: nros_node::rmw_type_registry::MessageForRmw,
1462                Result: nros_node::rmw_type_registry::MessageForRmw,
1463                Feedback: nros_node::rmw_type_registry::MessageForRmw,
1464                SendGoalRequest: nros_node::rmw_type_registry::MessageForRmw,
1465                SendGoalResponse: nros_node::rmw_type_registry::MessageForRmw,
1466                GetResultRequest: nros_node::rmw_type_registry::MessageForRmw,
1467                GetResultResponse: nros_node::rmw_type_registry::MessageForRmw,
1468                FeedbackMessage: nros_node::rmw_type_registry::MessageForRmw,
1469            >,
1470    >(
1471        &mut self,
1472        id: EntityId<'entity>,
1473        action_name: &str,
1474    ) -> NodeResult<NodeActionClient<'entity, A>> {
1475        register_declared_action::<A>()?;
1476        let mut metadata = entity_metadata(EntityMetadataSpec {
1477            id,
1478            node_id: self.__node_id()?,
1479            kind: EntityKind::ActionClient,
1480            source_name: action_name,
1481            type_name: A::ACTION_NAME,
1482            type_hash: A::ACTION_HASH,
1483            qos: QoSProfile::default(),
1484        })?;
1485        metadata.action_hashes = Some(nros_node::ActionEndpointHashes::of::<A>());
1486        metadata.source = SourceLocationMetadata::caller()?;
1487        self.__declare_entity(metadata)?;
1488        Ok(NodeActionClient::new(id))
1489    }
1490
1491    /// Declare an action client using `name` as the stable entity ID.
1492    #[track_caller]
1493    fn create_action_client_for_name<
1494        'entity,
1495        A: RosAction<
1496                Goal: nros_node::rmw_type_registry::MessageForRmw,
1497                Result: nros_node::rmw_type_registry::MessageForRmw,
1498                Feedback: nros_node::rmw_type_registry::MessageForRmw,
1499                SendGoalRequest: nros_node::rmw_type_registry::MessageForRmw,
1500                SendGoalResponse: nros_node::rmw_type_registry::MessageForRmw,
1501                GetResultRequest: nros_node::rmw_type_registry::MessageForRmw,
1502                GetResultResponse: nros_node::rmw_type_registry::MessageForRmw,
1503                FeedbackMessage: nros_node::rmw_type_registry::MessageForRmw,
1504            >,
1505    >(
1506        &mut self,
1507        name: &'entity str,
1508    ) -> NodeResult<NodeActionClient<'entity, A>> {
1509        self.declare_action_client::<A>(EntityId::new(name), name)
1510    }
1511
1512    /// Declare an action client that delivers the goal RESULT + FEEDBACK to
1513    /// named callbacks (Phase 212.M-F.23). `name` is the stable entity ID. The
1514    /// executor auto-drives accept → feedback stream → result during spin and
1515    /// dispatches `ExecutableNode::on_callback` with `result_callback_name`
1516    /// (payload = result CDR) on completion, and with `feedback_callback_name`
1517    /// (payload = feedback CDR) per feedback message. Read either with
1518    /// `CallbackCtx::message::<A::Result>()` / `::<A::Feedback>()`. Without
1519    /// these the client can only `send_goal`; result + feedback are dropped.
1520    ///
1521    /// (Layout note: the action-client variant reuses the server-side
1522    /// `action_accepted_callback_id` metadata slot for the feedback callback —
1523    /// that field is unused on a client, so no new schema field is needed.)
1524    #[track_caller]
1525    fn create_action_client_with_callbacks_for_name<
1526        'entity,
1527        A: RosAction<
1528                Goal: nros_node::rmw_type_registry::MessageForRmw,
1529                Result: nros_node::rmw_type_registry::MessageForRmw,
1530                Feedback: nros_node::rmw_type_registry::MessageForRmw,
1531                SendGoalRequest: nros_node::rmw_type_registry::MessageForRmw,
1532                SendGoalResponse: nros_node::rmw_type_registry::MessageForRmw,
1533                GetResultRequest: nros_node::rmw_type_registry::MessageForRmw,
1534                GetResultResponse: nros_node::rmw_type_registry::MessageForRmw,
1535                FeedbackMessage: nros_node::rmw_type_registry::MessageForRmw,
1536            >,
1537    >(
1538        &mut self,
1539        name: &'entity str,
1540        result_callback_name: &str,
1541        feedback_callback_name: &str,
1542    ) -> NodeResult<NodeActionClient<'entity, A>> {
1543        register_declared_action::<A>()?;
1544        let mut metadata = entity_metadata(EntityMetadataSpec {
1545            id: EntityId::new(name),
1546            node_id: self.__node_id()?,
1547            kind: EntityKind::ActionClient,
1548            source_name: name,
1549            type_name: A::ACTION_NAME,
1550            type_hash: A::ACTION_HASH,
1551            qos: QoSProfile::default(),
1552        })?;
1553        metadata.action_hashes = Some(nros_node::ActionEndpointHashes::of::<A>());
1554        metadata.callback_id = Some(copy_str(result_callback_name)?);
1555        metadata.action_accepted_callback_id = Some(copy_str(feedback_callback_name)?);
1556        metadata.callback_source = SourceLocationMetadata::caller()?;
1557        metadata.source = metadata.callback_source.clone();
1558        self.__declare_entity(metadata)?;
1559        Ok(NodeActionClient::new(EntityId::new(name)))
1560    }
1561
1562    /// Declare a parameter. Stable parameter ID is required.
1563    #[track_caller]
1564    #[doc(hidden)]
1565    fn declare_parameter<'entity>(
1566        &mut self,
1567        id: EntityId<'entity>,
1568        name: &str,
1569        parameter_type: ParameterType,
1570    ) -> NodeResult<NodeParameter<'entity>> {
1571        self.declare_parameter_with_default(id, name, ParameterDefault::for_type(parameter_type)?)
1572    }
1573
1574    /// Declare a parameter with a concrete source default.
1575    #[track_caller]
1576    #[doc(hidden)]
1577    fn declare_parameter_with_default<'entity>(
1578        &mut self,
1579        id: EntityId<'entity>,
1580        name: &str,
1581        default: ParameterDefault,
1582    ) -> NodeResult<NodeParameter<'entity>> {
1583        let mut metadata = entity_metadata(EntityMetadataSpec {
1584            id,
1585            node_id: self.__node_id()?,
1586            kind: EntityKind::Parameter,
1587            source_name: name,
1588            type_name: "",
1589            type_hash: "",
1590            qos: QoSProfile::default(),
1591        })?;
1592        metadata.parameter_type = Some(default.parameter_type());
1593        metadata.parameter_default = Some(default);
1594        metadata.source = SourceLocationMetadata::caller()?;
1595        self.__declare_entity(metadata)?;
1596        Ok(NodeParameter::new(id))
1597    }
1598
1599    /// Declare a parameter using `name` as the generated stable entity ID.
1600    #[track_caller]
1601    fn declare_parameter_for_name<'entity>(
1602        &mut self,
1603        name: &'entity str,
1604        parameter_type: ParameterType,
1605    ) -> NodeResult<NodeParameter<'entity>> {
1606        self.declare_parameter(EntityId::new(name), name, parameter_type)
1607    }
1608
1609    /// Declare a parameter with a concrete source default, using `name` as
1610    /// the generated stable entity ID.
1611    #[track_caller]
1612    fn declare_parameter_for_name_with_default<'entity>(
1613        &mut self,
1614        name: &'entity str,
1615        default: ParameterDefault,
1616    ) -> NodeResult<NodeParameter<'entity>> {
1617        self.declare_parameter_with_default(EntityId::new(name), name, default)
1618    }
1619
1620    /// Record optional effects for a callback.
1621    #[doc(hidden)]
1622    fn callback<'callback>(&mut self, id: CallbackId<'callback>) -> CallbackEffects<'_, 'callback> {
1623        CallbackEffects {
1624            executor: self.__executor_mut(),
1625            id,
1626        }
1627    }
1628
1629    /// Record optional effects for a named callback without exposing
1630    /// `CallbackId` at the declaration site.
1631    fn callback_for_name<'callback>(
1632        &mut self,
1633        name: &'callback str,
1634    ) -> CallbackEffects<'_, 'callback> {
1635        self.callback(CallbackId::new(name))
1636    }
1637}
1638
1639#[cfg(feature = "rmw-cffi")]
1640impl DeclarativeNode for crate::Node<'_, 'static> {
1641    fn __node_id(&mut self) -> NodeResult<NodeId<'static>> {
1642        let exec_id = self.id();
1643        frame_mut(self.__executor())?.stable_id_of(exec_id)
1644    }
1645
1646    fn __declare_entity(&mut self, mut metadata: EntityMetadata) -> NodeResult<()> {
1647        let exec_id = self.id();
1648        let executor = self.__executor();
1649        let frame = frame_mut(executor)?;
1650        if metadata.callback_group.is_none() {
1651            metadata.callback_group = frame.group_of(exec_id);
1652        }
1653        frame.sink.create_entity(executor, metadata)
1654    }
1655
1656    fn __executor_mut(&mut self) -> &mut crate::Executor<'static> {
1657        self.__executor()
1658    }
1659}
1660
1661/// Builder for optional callback effect metadata.
1662#[cfg(feature = "rmw-cffi")]
1663pub struct CallbackEffects<'ctx, 'id> {
1664    executor: &'ctx mut crate::Executor<'static>,
1665    id: CallbackId<'id>,
1666}
1667
1668#[cfg(feature = "rmw-cffi")]
1669impl<'ctx, 'id> CallbackEffects<'ctx, 'id> {
1670    /// Record that callback reads from an entity.
1671    #[doc(hidden)]
1672    pub fn reads(self, entity_id: EntityId<'_>) -> NodeResult<Self> {
1673        let frame = frame_mut(self.executor)?;
1674        frame.sink.record_callback_effect(
1675            self.executor,
1676            self.id,
1677            CallbackEffectKind::Reads,
1678            entity_id,
1679        )?;
1680        Ok(self)
1681    }
1682
1683    /// Record that callback reads from a declared entity handle.
1684    pub fn reads_entity(self, entity: &impl DeclaredEntity) -> NodeResult<Self> {
1685        self.reads(entity.entity_id())
1686    }
1687
1688    /// Record that callback publishes via an entity.
1689    #[doc(hidden)]
1690    pub fn publishes(self, entity_id: EntityId<'_>) -> NodeResult<Self> {
1691        let frame = frame_mut(self.executor)?;
1692        frame.sink.record_callback_effect(
1693            self.executor,
1694            self.id,
1695            CallbackEffectKind::Publishes,
1696            entity_id,
1697        )?;
1698        Ok(self)
1699    }
1700
1701    /// Record that callback publishes via a declared entity handle.
1702    pub fn publishes_entity(self, entity: &impl DeclaredEntity) -> NodeResult<Self> {
1703        self.publishes(entity.entity_id())
1704    }
1705
1706    /// Record that callback writes to an entity or parameter.
1707    #[doc(hidden)]
1708    pub fn writes(self, entity_id: EntityId<'_>) -> NodeResult<Self> {
1709        let frame = frame_mut(self.executor)?;
1710        frame.sink.record_callback_effect(
1711            self.executor,
1712            self.id,
1713            CallbackEffectKind::Writes,
1714            entity_id,
1715        )?;
1716        Ok(self)
1717    }
1718
1719    /// Record that callback writes to a declared entity handle.
1720    pub fn writes_entity(self, entity: &impl DeclaredEntity) -> NodeResult<Self> {
1721        self.writes(entity.entity_id())
1722    }
1723}
1724
1725/// A declared source-level entity handle that can be referenced by callback effects.
1726#[doc(hidden)]
1727pub trait DeclaredEntity {
1728    /// Stable entity ID for metadata and generated runtime lookup.
1729    fn entity_id(&self) -> EntityId<'_>;
1730}
1731
1732macro_rules! component_handle {
1733    ($name:ident $(, $type_param:ident)?) => {
1734        /// Source-level component entity handle.
1735        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1736        pub struct $name<'id $(, $type_param)?> {
1737            id: EntityId<'id>,
1738            _marker: PhantomData<($($type_param,)?)>,
1739        }
1740
1741        impl<'id $(, $type_param)?> $name<'id $(, $type_param)?> {
1742            const fn new(id: EntityId<'id>) -> Self {
1743                Self {
1744                    id,
1745                    _marker: PhantomData,
1746                }
1747            }
1748
1749            /// Stable entity ID.
1750            #[doc(hidden)]
1751            pub const fn id(&self) -> EntityId<'id> {
1752                self.id
1753            }
1754        }
1755
1756        impl<'id $(, $type_param)?> DeclaredEntity for $name<'id $(, $type_param)?> {
1757            fn entity_id(&self) -> EntityId<'_> {
1758                self.id
1759            }
1760        }
1761    };
1762}
1763
1764component_handle!(NodePublisher, M);
1765component_handle!(NodeSubscription, M);
1766component_handle!(NodeServiceServer, S);
1767component_handle!(NodeServiceClient, S);
1768component_handle!(NodeActionServer, A);
1769component_handle!(NodeActionClient, A);
1770component_handle!(NodeTimer);
1771component_handle!(NodeParameter);
1772
1773// ============================================================================
1774// Phase 172 W.5.1 — executable component layer (callback bodies)
1775// ============================================================================
1776//
1777// The declarative `Node::register` above stays the planning/metadata SSOT.
1778// This layer binds *runnable* bodies: the generated runtime builds the
1779// component `State` once, then routes each fired callback to `on_callback` with
1780// a `CallbackCtx` that exposes the triggering payload + an immediate publish
1781// path. Publishers are self-contained transport handles
1782// (`EmbeddedRawPublisher::publish_raw(&self)`), so a body publishes immediately
1783// mid-spin with no executor re-entrancy and no deferred queue (causality
1784// preserved). Shared state across a component's callbacks is `&mut State`
1785// behind the generated runtime's `'static` storage — `no_std`, no `alloc`.
1786
1787/// Resolves a component publisher by its stable [`EntityId`] for the
1788/// callback-body publish path (W.5.1).
1789///
1790/// The generated runtime implements this over its owned `'static` publishers;
1791/// metadata/discovery mode never constructs a [`CallbackCtx`], so it need not
1792/// implement this.
1793pub trait PublisherResolver {
1794    /// Publish raw CDR bytes through the publisher with this stable entity id.
1795    /// `Err(NodeDeclError::Runtime)` if no such publisher is registered or the
1796    /// transport rejects the write.
1797    fn publish_raw(&self, entity_id: &str, data: &[u8]) -> NodeResult<()>;
1798}
1799
1800/// Where a service / action-result callback body writes its reply (W.5.3): the
1801/// generated trampoline lends a `buf`; the body fills it via
1802/// [`CallbackCtx::reply`] and the trampoline reads `*written` back out.
1803struct ReplySink<'a> {
1804    buf: &'a mut [u8],
1805    written: &'a mut usize,
1806}
1807
1808/// Where an action goal / cancel-decision callback writes its accept/reject
1809/// (W.5.3): the generated trampoline lends the out-slot, the body fills it via
1810/// [`CallbackCtx::set_goal_response`] / [`set_cancel_response`](CallbackCtx::set_cancel_response),
1811/// and the trampoline returns it. Decisions need no executor — unlike feedback /
1812/// result, which do (see the action-execution note in Phase 172 W.5.3).
1813enum DecisionSink<'a> {
1814    Goal(&'a mut GoalResponse),
1815    Cancel(&'a mut CancelResponse),
1816}
1817
1818/// Context handed to an executable component callback body (W.5.1).
1819///
1820/// Carries the triggering payload (raw CDR — empty for timers) plus the
1821/// publisher resolver, so a body can read its message and publish immediately.
1822/// Service / action-result callbacks additionally carry a `ReplySink` the body
1823/// fills via [`reply`](Self::reply); action goal / cancel callbacks carry a
1824/// `DecisionSink` the body fills via
1825/// [`set_goal_response`](Self::set_goal_response) /
1826/// [`set_cancel_response`](Self::set_cancel_response) (W.5.3).
1827/// issue 0461 — bytes of `unique_identifier_msgs/UUID` that precede the goal
1828/// fields in a SendGoal request. A fixed `uint8[16]` array: no length prefix.
1829const GOAL_UUID_LEN: usize = 16;
1830
1831pub struct CallbackCtx<'a> {
1832    payload: &'a [u8],
1833    publishers: &'a dyn PublisherResolver,
1834    reply: Option<ReplySink<'a>>,
1835    decision: Option<DecisionSink<'a>>,
1836    /// Phase 250 (Wave 2) — E2E message-integrity status for a subscription that
1837    /// opted in via `.safety()`; `None` for every other callback (timers,
1838    /// services, non-safety subscriptions). Read with
1839    /// [`integrity`](Self::integrity). Gated with the capability so it is
1840    /// zero-cost when `safety-e2e` is off.
1841    #[cfg(feature = "safety-e2e")]
1842    integrity: Option<&'a crate::IntegrityStatus>,
1843    /// Phase 264 W4c — the executor's volatile parameter store, threaded by the dispatch
1844    /// site from the component cell (`None` until `[param_services]` registers the store).
1845    /// Read with [`parameter`](Self::parameter). Gated so it is zero-cost when
1846    /// `param-services` is off.
1847    #[cfg(feature = "param-store")]
1848    params: Option<&'a crate::ParameterServer<'a>>,
1849    /// phase-426 W3 — WHICH node's parameters `parameter()` reads.
1850    ///
1851    /// One set of six parameter services is registered per node and the store
1852    /// is keyed the same way, so a context that carries the store without the
1853    /// key can only ever answer for one node — which is how every component on
1854    /// a multi-node executor came to read the primary's values.
1855    #[cfg(feature = "param-store")]
1856    param_node: nros_params::NodeKey,
1857}
1858
1859impl<'a> CallbackCtx<'a> {
1860    /// Build a callback context with no reply sink (timer / subscription).
1861    /// `payload` is the entity's raw CDR (empty slice for timers).
1862    pub fn new(payload: &'a [u8], publishers: &'a dyn PublisherResolver) -> Self {
1863        Self {
1864            payload,
1865            publishers,
1866            reply: None,
1867            decision: None,
1868            #[cfg(feature = "safety-e2e")]
1869            integrity: None,
1870            #[cfg(feature = "param-store")]
1871            params: None,
1872            // phase-426 W3 — the store is keyed by node and this context does
1873            // not know which one until the dispatch site says so
1874            // (`set_param_server`). PRIMARY until then, which is the executor's
1875            // first node and what a single-node image means.
1876            #[cfg(feature = "param-store")]
1877            param_node: nros_params::NodeKey::PRIMARY,
1878        }
1879    }
1880
1881    /// Phase 250 (Wave 2) — build a subscription context carrying E2E
1882    /// [`IntegrityStatus`](crate::IntegrityStatus) (the declarative `.safety()`
1883    /// path). The body reads both the message ([`message`](Self::message)) and
1884    /// the status ([`integrity`](Self::integrity)) in one callback, mirroring the
1885    /// imperative `FnMut(&M, &IntegrityStatus)` shape.
1886    #[cfg(feature = "safety-e2e")]
1887    pub fn new_with_integrity(
1888        payload: &'a [u8],
1889        publishers: &'a dyn PublisherResolver,
1890        integrity: &'a crate::IntegrityStatus,
1891    ) -> Self {
1892        Self {
1893            payload,
1894            publishers,
1895            reply: None,
1896            decision: None,
1897            integrity: Some(integrity),
1898            #[cfg(feature = "param-store")]
1899            params: None,
1900            // phase-426 W3 — the store is keyed by node and this context does
1901            // not know which one until the dispatch site says so
1902            // (`set_param_server`). PRIMARY until then, which is the executor's
1903            // first node and what a single-node image means.
1904            #[cfg(feature = "param-store")]
1905            param_node: nros_params::NodeKey::PRIMARY,
1906        }
1907    }
1908
1909    /// Build a callback context with a reply sink (service / action-result;
1910    /// W.5.3). The body fills `reply_buf` via [`reply`](Self::reply); the
1911    /// generated trampoline reads `*reply_written` back as the response length.
1912    pub fn with_reply(
1913        payload: &'a [u8],
1914        publishers: &'a dyn PublisherResolver,
1915        reply_buf: &'a mut [u8],
1916        reply_written: &'a mut usize,
1917    ) -> Self {
1918        *reply_written = 0;
1919        Self {
1920            payload,
1921            publishers,
1922            reply: Some(ReplySink {
1923                buf: reply_buf,
1924                written: reply_written,
1925            }),
1926            decision: None,
1927            #[cfg(feature = "safety-e2e")]
1928            integrity: None,
1929            #[cfg(feature = "param-store")]
1930            params: None,
1931            // phase-426 W3 — the store is keyed by node and this context does
1932            // not know which one until the dispatch site says so
1933            // (`set_param_server`). PRIMARY until then, which is the executor's
1934            // first node and what a single-node image means.
1935            #[cfg(feature = "param-store")]
1936            param_node: nros_params::NodeKey::PRIMARY,
1937        }
1938    }
1939
1940    /// Build a context for an action **goal** callback (W.5.3): the body decides
1941    /// accept/reject via [`set_goal_response`](Self::set_goal_response); the
1942    /// generated trampoline returns `*out`. `payload` is the goal CDR.
1943    pub fn with_goal_decision(
1944        payload: &'a [u8],
1945        publishers: &'a dyn PublisherResolver,
1946        out: &'a mut GoalResponse,
1947    ) -> Self {
1948        Self {
1949            payload,
1950            publishers,
1951            reply: None,
1952            decision: Some(DecisionSink::Goal(out)),
1953            #[cfg(feature = "safety-e2e")]
1954            integrity: None,
1955            #[cfg(feature = "param-store")]
1956            params: None,
1957            // phase-426 W3 — the store is keyed by node and this context does
1958            // not know which one until the dispatch site says so
1959            // (`set_param_server`). PRIMARY until then, which is the executor's
1960            // first node and what a single-node image means.
1961            #[cfg(feature = "param-store")]
1962            param_node: nros_params::NodeKey::PRIMARY,
1963        }
1964    }
1965
1966    /// Build a context for an action **cancel** callback (W.5.3): the body decides
1967    /// accept/reject via [`set_cancel_response`](Self::set_cancel_response).
1968    pub fn with_cancel_decision(
1969        payload: &'a [u8],
1970        publishers: &'a dyn PublisherResolver,
1971        out: &'a mut CancelResponse,
1972    ) -> Self {
1973        Self {
1974            payload,
1975            publishers,
1976            reply: None,
1977            decision: Some(DecisionSink::Cancel(out)),
1978            #[cfg(feature = "safety-e2e")]
1979            integrity: None,
1980            #[cfg(feature = "param-store")]
1981            params: None,
1982            // phase-426 W3 — the store is keyed by node and this context does
1983            // not know which one until the dispatch site says so
1984            // (`set_param_server`). PRIMARY until then, which is the executor's
1985            // first node and what a single-node image means.
1986            #[cfg(feature = "param-store")]
1987            param_node: nros_params::NodeKey::PRIMARY,
1988        }
1989    }
1990
1991    /// Phase 264 W4c — thread the executor's volatile parameter store into this context.
1992    /// The dispatch site calls this after construction (the store reaches the callback via
1993    /// the component cell, not the constructor args). No-op-equivalent when `None`.
1994    #[cfg(feature = "param-store")]
1995    pub fn set_param_server(
1996        &mut self,
1997        params: Option<&'a crate::ParameterServer<'a>>,
1998        node: nros_params::NodeKey,
1999    ) {
2000        self.params = params;
2001        self.param_node = node;
2002    }
2003
2004    /// Phase 264 W4c — read this node's parameter `name` as `T`, or `None` if the
2005    /// parameter is undeclared, the wrong type, or `[param_services]` is not enabled.
2006    /// Returns the **live** value — the launch-baked initial, or whatever a `ros2 param
2007    /// set` last wrote (RFC-0004 §10; values are volatile, lost at the next boot).
2008    #[cfg(feature = "param-store")]
2009    pub fn parameter<T: crate::ParameterVariant>(&self, name: &str) -> Option<T> {
2010        self.params
2011            // phase-426 W3 — the store is keyed by node, and the key is the
2012            // component's OWN node, threaded in beside the store by the
2013            // dispatch site. Reading PRIMARY here (which is what this did
2014            // until W3) makes the second node on an executor read its
2015            // sibling's values.
2016            .and_then(|server| server.get(self.param_node, name))
2017            .and_then(T::from_parameter_value)
2018    }
2019
2020    /// Set the action goal-callback's accept/reject decision (W.5.3). `Err` when
2021    /// the callback is not a goal decision.
2022    pub fn set_goal_response(&mut self, response: GoalResponse) -> NodeResult<()> {
2023        match &mut self.decision {
2024            Some(DecisionSink::Goal(slot)) => {
2025                **slot = response;
2026                Ok(())
2027            }
2028            _ => Err(NodeDeclError::Runtime),
2029        }
2030    }
2031
2032    /// Set the action cancel-callback's accept/reject decision (W.5.3). `Err` when
2033    /// the callback is not a cancel decision.
2034    ///
2035    /// Issue 0796 — `CancelResponse` here is the PER-GOAL decision
2036    /// (`Reject` / `Accept`), the twin of [`GoalResponse`] and the same two
2037    /// values C's `rclc_cancel_response_t` and C++'s `nros::CancelResponse`
2038    /// carry. It used to be the `action_msgs/srv/CancelGoal` RPC return code
2039    /// (now `nros_core::CancelReturnCode`), so answering "cancel this goal"
2040    /// was spelled `CancelResponse::Ok` — a whole-request status code used to
2041    /// decide one goal.
2042    pub fn set_cancel_response(&mut self, response: CancelResponse) -> NodeResult<()> {
2043        match &mut self.decision {
2044            Some(DecisionSink::Cancel(slot)) => {
2045                **slot = response;
2046                Ok(())
2047            }
2048            _ => Err(NodeDeclError::Runtime),
2049        }
2050    }
2051
2052    /// Write the service / action reply as raw CDR bytes (W.5.3). `Err` when the
2053    /// callback has no reply sink (timer / subscription) or the reply exceeds the
2054    /// lent buffer.
2055    pub fn reply_raw(&mut self, data: &[u8]) -> NodeResult<()> {
2056        let sink = self.reply.as_mut().ok_or(NodeDeclError::Runtime)?;
2057        if data.len() > sink.buf.len() {
2058            return Err(NodeDeclError::Runtime);
2059        }
2060        sink.buf[..data.len()].copy_from_slice(data);
2061        *sink.written = data.len();
2062        Ok(())
2063    }
2064
2065    /// Serialize `msg` and write it as the service / action reply (W.5.3).
2066    pub fn reply<M: RosMessage, const N: usize>(&mut self, msg: &M) -> NodeResult<()> {
2067        let mut buf = [0u8; N];
2068        let mut writer =
2069            crate::CdrWriter::new_with_header(&mut buf).map_err(|_| NodeDeclError::Runtime)?;
2070        msg.serialize(&mut writer)
2071            .map_err(|_| NodeDeclError::Runtime)?;
2072        let len = writer.position();
2073        self.reply_raw(&buf[..len])
2074    }
2075
2076    /// Raw CDR payload of the triggering message / request. Empty for timers.
2077    pub fn payload(&self) -> &[u8] {
2078        self.payload
2079    }
2080
2081    /// Phase 250 (Wave 2) — E2E message-integrity status (CRC + sequence gap/dup)
2082    /// for this dispatch. `Some` only when the firing subscription opted in via
2083    /// `.safety()`; `None` for timers, services, and non-safety subscriptions.
2084    /// Read it alongside [`message`](Self::message) — the status describes the
2085    /// message you just received.
2086    #[cfg(feature = "safety-e2e")]
2087    pub fn integrity(&self) -> Option<&crate::IntegrityStatus> {
2088        self.integrity
2089    }
2090
2091    /// Deserialize the triggering payload as `M` (subscription / service-request
2092    /// bodies). `Err` if the payload is malformed for `M`.
2093    pub fn message<M: RosMessage>(&self) -> NodeResult<M> {
2094        let mut reader =
2095            crate::CdrReader::new_with_header(self.payload).map_err(|_| NodeDeclError::Runtime)?;
2096        // issue 0461 — an action GOAL callback's payload is the whole SendGoal
2097        // request, `[CDR header][goal_id uuid][goal fields]`. Without this skip
2098        // the reader is sitting on the uuid and a goal type decodes its first
2099        // four bytes — the goal counter, so every goal looked like `order = 1`.
2100        //
2101        // The goal_id reaches the callback by other means (the server's
2102        // `for_each_active_goal_for_name`), so it is framing here, not data.
2103        // Same shape as the typed `try_accept_goal` path, which has always
2104        // skipped it and has always decoded correctly.
2105        if matches!(self.decision, Some(DecisionSink::Goal(_))) {
2106            for _ in 0..GOAL_UUID_LEN {
2107                let _ = reader.read_u8();
2108            }
2109        }
2110        M::deserialize(&mut reader).map_err(|_| NodeDeclError::Runtime)
2111    }
2112
2113    /// Publish raw CDR bytes through the named publisher entity (immediate).
2114    #[doc(hidden)]
2115    pub fn publish_raw(&self, publisher: EntityId<'_>, data: &[u8]) -> NodeResult<()> {
2116        self.publishers.publish_raw(publisher.as_str(), data)
2117    }
2118
2119    /// Serialize `msg` into an `N`-byte stack buffer and publish it (immediate).
2120    /// `N` must be ≥ the CDR-encoded size of `msg`; the generated runtime picks
2121    /// it from the message type.
2122    #[doc(hidden)]
2123    pub fn publish<M: RosMessage, const N: usize>(
2124        &self,
2125        publisher: EntityId<'_>,
2126        msg: &M,
2127    ) -> NodeResult<()> {
2128        let mut buf = [0u8; N];
2129        let mut writer =
2130            crate::CdrWriter::new_with_header(&mut buf).map_err(|_| NodeDeclError::Runtime)?;
2131        msg.serialize(&mut writer)
2132            .map_err(|_| NodeDeclError::Runtime)?;
2133        let len = writer.position();
2134        self.publish_raw(publisher, &buf[..len])
2135    }
2136
2137    /// Serialize `msg` and publish through the entity synthesized from `topic`.
2138    ///
2139    /// This pairs with
2140    /// [`DeclarativeNode::create_publisher_for_topic`], allowing simple callback
2141    /// bodies to use the ROS topic literal instead of restating an unrelated
2142    /// stable entity ID.
2143    pub fn publish_to_topic<M: RosMessage, const N: usize>(
2144        &self,
2145        topic: &str,
2146        msg: &M,
2147    ) -> NodeResult<()> {
2148        self.publish::<M, N>(EntityId::new(topic), msg)
2149    }
2150}
2151
2152/// The executable counterpart of [`Component`] (W.5.1).
2153///
2154/// `register` (declarative) stays the planning SSOT; this binds runnable
2155/// bodies. The generated runtime builds [`State`](ExecutableNode::State) once via
2156/// [`init`](ExecutableNode::init), then routes every fired callback to
2157/// [`on_callback`](ExecutableNode::on_callback). Trait-dispatch (no boxed `dyn`, no
2158/// `alloc`) keeps it `no_std`.
2159/// Executor-backed action operations a [`TickCtx`] drives (W.5.6).
2160///
2161/// Action result/feedback need `&mut Executor` (`complete_goal_raw` /
2162/// `publish_feedback_raw`), which a mid-spin *callback* can't hold (the executor
2163/// is borrowed) — so they run from [`ExecutableNode::tick`], between spins.
2164/// The generated runtime implements this over the real executor + the action
2165/// servers' handles (resolved by stable action entity id); the component never
2166/// sees the executor directly. Kept as a trait so [`TickCtx`] stays `no_std` +
2167/// free of the `rmw-cffi`-gated `Executor` type.
2168pub trait ActionExecutor {
2169    /// Complete the goal `goal_id` on action `action_entity` with raw CDR result.
2170    fn complete_goal_raw(
2171        &mut self,
2172        action_entity: &str,
2173        goal_id: &GoalId,
2174        status: GoalStatus,
2175        result: &[u8],
2176    ) -> NodeResult<()>;
2177
2178    /// Publish raw CDR feedback for `goal_id` on action `action_entity`.
2179    fn publish_feedback_raw(
2180        &mut self,
2181        action_entity: &str,
2182        goal_id: &GoalId,
2183        feedback: &[u8],
2184    ) -> NodeResult<()>;
2185
2186    /// Visit every goal on `action_entity` that has been accepted but not yet
2187    /// completed, with its id + current status. The execution seam: a `tick` body
2188    /// has no other way to learn an accepted goal's id (the goal-decision callback
2189    /// doesn't surface it), so it iterates here to drive feedback / completion.
2190    fn for_each_active_goal(&self, action_entity: &str, visit: &mut dyn FnMut(&GoalId, GoalStatus));
2191}
2192
2193/// Executor-backed CLIENT operations a [`TickCtx`] drives (Phase 212.M-F.4).
2194///
2195/// Service-client `call` + action-client `send_goal` need `&mut Executor`
2196/// (the W.5.6 client handles live on the executor), which a mid-spin
2197/// callback can't hold. They run from [`ExecutableNode::tick`], between
2198/// spins. The generated runtime impls this over the real executor + the
2199/// service/action client handles (resolved by stable client entity id); the
2200/// component never sees the executor directly. Kept as a trait so [`TickCtx`]
2201/// stays `no_std` + free of the `rmw-cffi`-gated `Executor` type.
2202///
2203/// Mirrors the sibling [`ActionExecutor`] (server-side ops). Splitting
2204/// client vs server keeps each trait small + lets the codegen-side
2205/// `GenClientDispatch` impl resolve client handles independently from
2206/// server handles.
2207pub trait ClientDispatch {
2208    /// Issue a service-client request on `service_entity` carrying CDR
2209    /// `request_cdr`; block on the reply, write the response CDR into
2210    /// `response_buf`, return the response length in bytes.
2211    ///
2212    /// The synchronous block is built on the executor-driven
2213    /// `send_request_raw` + `take_response_raw` pair (phase-301: the
2214    /// RMW layer has no blocking call) — the tick hook drives the
2215    /// executor between callback dispatch, so a blocked `call_raw`
2216    /// does not starve other callbacks (each tick yields back to the
2217    /// runtime after returning).
2218    fn call_raw(
2219        &mut self,
2220        service_entity: &str,
2221        request_cdr: &[u8],
2222        response_buf: &mut [u8],
2223    ) -> NodeResult<usize>;
2224
2225    /// Send an action-client goal request on `action_entity` carrying
2226    /// CDR `goal_cdr`; return the assigned [`GoalId`] (server-stamped on
2227    /// the goal-accept response). Result + feedback streams arrive via
2228    /// callback dispatch — not this method.
2229    fn send_goal_raw(&mut self, action_entity: &str, goal_cdr: &[u8]) -> NodeResult<GoalId>;
2230
2231    /// Whether a server for the service client `service_entity` is CURRENTLY
2232    /// discoverable — `rclcpp::ClientBase::service_is_ready`, for a tick.
2233    ///
2234    /// phase-428 W13. Three answers: `Ok(true)`, `Ok(false)`, or `Err` when
2235    /// the backend cannot say (no discovery channel, or the entity is
2236    /// unknown). A tick that gates its first `call_raw` on `Ok(true)` and
2237    /// treats `Err` as "call anyway" keeps the request-is-the-probe behaviour
2238    /// on backends without discovery and stops sending into the void on the
2239    /// ones with it.
2240    ///
2241    /// Default `Err(NodeDeclError::Runtime)` — "cannot say" — so an
2242    /// implementor that predates this method (the orchestration codegen's
2243    /// dispatch) keeps its shape.
2244    fn service_is_ready(&self, service_entity: &str) -> NodeResult<bool> {
2245        let _ = service_entity;
2246        Err(NodeDeclError::Runtime)
2247    }
2248
2249    /// Whether a server for the action client `action_entity` is CURRENTLY
2250    /// discoverable — `rclcpp_action::Client::action_server_is_ready`, for a
2251    /// tick. Same three answers as [`Self::service_is_ready`], read off the
2252    /// action's `send_goal` service (issue 1918): a goal sent before the server
2253    /// has declared it is answered by nobody, and the declarative client sends
2254    /// ONCE.
2255    fn action_server_is_ready(&self, action_entity: &str) -> NodeResult<bool> {
2256        let _ = action_entity;
2257        Err(NodeDeclError::Runtime)
2258    }
2259}
2260
2261/// Context handed to [`ExecutableNode::tick`] (W.5.6 + M-F.4): the per-spin
2262/// hook that runs *between* callback dispatch, where the executor is free.
2263/// Exposes the immediate publish path (like `CallbackCtx`) plus executor-backed
2264/// action-server ops (complete goal / publish feedback) AND executor-backed
2265/// client-side ops (service `call` / action-client `send_goal`). Callbacks
2266/// can't perform any of these since they don't hold the executor.
2267pub struct TickCtx<'a> {
2268    publishers: &'a dyn PublisherResolver,
2269    actions: &'a mut dyn ActionExecutor,
2270    clients: &'a mut dyn ClientDispatch,
2271    /// Phase 264 W4c — the executor's volatile parameter store, threaded by the tick
2272    /// driver (`tick_one_cell` already holds the executor). `None` until
2273    /// `[param_services]` registers the store. Read with [`parameter`](Self::parameter).
2274    #[cfg(feature = "param-store")]
2275    params: Option<&'a crate::ParameterServer<'a>>,
2276    /// phase-426 W3 — WHICH node's parameters `parameter()` reads.
2277    ///
2278    /// One set of six parameter services is registered per node and the store
2279    /// is keyed the same way, so a context that carries the store without the
2280    /// key can only ever answer for one node — which is how every component on
2281    /// a multi-node executor came to read the primary's values.
2282    #[cfg(feature = "param-store")]
2283    param_node: nros_params::NodeKey,
2284}
2285
2286impl<'a> TickCtx<'a> {
2287    /// Build a tick context (called by the generated runtime each spin).
2288    pub fn new(
2289        publishers: &'a dyn PublisherResolver,
2290        actions: &'a mut dyn ActionExecutor,
2291        clients: &'a mut dyn ClientDispatch,
2292    ) -> Self {
2293        Self {
2294            publishers,
2295            actions,
2296            clients,
2297            #[cfg(feature = "param-store")]
2298            params: None,
2299            // phase-426 W3 — the store is keyed by node and this context does
2300            // not know which one until the dispatch site says so
2301            // (`set_param_server`). PRIMARY until then, which is the executor's
2302            // first node and what a single-node image means.
2303            #[cfg(feature = "param-store")]
2304            param_node: nros_params::NodeKey::PRIMARY,
2305        }
2306    }
2307
2308    /// Phase 264 W4c — thread the executor's volatile parameter store in (the tick
2309    /// driver holds the executor directly). No-op-equivalent when `None`.
2310    #[cfg(feature = "param-store")]
2311    pub fn set_param_server(
2312        &mut self,
2313        params: Option<&'a crate::ParameterServer<'a>>,
2314        node: nros_params::NodeKey,
2315    ) {
2316        self.params = params;
2317        self.param_node = node;
2318    }
2319
2320    /// Phase 264 W4c — read this node's parameter `name` as `T` during `tick`, or `None`
2321    /// if undeclared, the wrong type, or `[param_services]` is off. Returns the live
2322    /// value (baked initial or last `ros2 param set`; volatile — RFC-0004 §10).
2323    #[cfg(feature = "param-store")]
2324    pub fn parameter<T: crate::ParameterVariant>(&self, name: &str) -> Option<T> {
2325        self.params
2326            // phase-426 W3 — the store is keyed by node, and the key is the
2327            // component's OWN node, threaded in beside the store by the
2328            // dispatch site. Reading PRIMARY here (which is what this did
2329            // until W3) makes the second node on an executor read its
2330            // sibling's values.
2331            .and_then(|server| server.get(self.param_node, name))
2332            .and_then(T::from_parameter_value)
2333    }
2334
2335    /// Publish raw CDR bytes through the named publisher entity (immediate).
2336    #[doc(hidden)]
2337    pub fn publish_raw(&self, publisher: EntityId<'_>, data: &[u8]) -> NodeResult<()> {
2338        self.publishers.publish_raw(publisher.as_str(), data)
2339    }
2340
2341    /// Serialize `msg` into an `N`-byte stack buffer and publish it (immediate).
2342    #[doc(hidden)]
2343    pub fn publish<M: RosMessage, const N: usize>(
2344        &self,
2345        publisher: EntityId<'_>,
2346        msg: &M,
2347    ) -> NodeResult<()> {
2348        let mut buf = [0u8; N];
2349        let mut writer =
2350            crate::CdrWriter::new_with_header(&mut buf).map_err(|_| NodeDeclError::Runtime)?;
2351        msg.serialize(&mut writer)
2352            .map_err(|_| NodeDeclError::Runtime)?;
2353        let len = writer.position();
2354        self.publish_raw(publisher, &buf[..len])
2355    }
2356
2357    /// Serialize `msg` and publish through the entity synthesized from `topic`.
2358    ///
2359    /// This pairs with [`DeclarativeNode::create_publisher_for_topic`] for
2360    /// executable tick hooks.
2361    pub fn publish_to_topic<M: RosMessage, const N: usize>(
2362        &self,
2363        topic: &str,
2364        msg: &M,
2365    ) -> NodeResult<()> {
2366        self.publish::<M, N>(EntityId::new(topic), msg)
2367    }
2368
2369    /// Complete an action goal with a typed result (W.5.6 — needs the executor,
2370    /// hence tick-only).
2371    #[doc(hidden)]
2372    pub fn complete_goal<R: RosMessage, const N: usize>(
2373        &mut self,
2374        action: EntityId<'_>,
2375        goal_id: &GoalId,
2376        status: GoalStatus,
2377        result: &R,
2378    ) -> NodeResult<()> {
2379        // RFC-0069 / issue 0418 — NO inner encapsulation header. ROS 2's
2380        // `<Action>_GetResult_Response` is ONE CDR message: `[header][status][result
2381        // fields]`. This used to write a second header inside the envelope, which
2382        // made the payload `[outer][status][pad][INNER][fields]` — self-consistent
2383        // with nano-ros's own raw consumer and undecodable by any `rcl_action` peer
2384        // or by nano-ros's TYPED path.
2385        //
2386        // The issue-#35 corruption that motivated the old header ("the reader eats
2387        // the first data word … `sequence` deserialized to len 0") is now prevented
2388        // on the READ side instead: the executor splices the envelope's encap onto
2389        // the body before the callback sees it, so `CallbackCtx::message` still
2390        // receives a well-formed CDR message. Producer and consumer changed
2391        // together — either alone reproduces #35.
2392        let mut buf = [0u8; N];
2393        let mut writer = crate::CdrWriter::new(&mut buf);
2394        result
2395            .serialize(&mut writer)
2396            .map_err(|_| NodeDeclError::Runtime)?;
2397        let len = writer.position();
2398        self.actions
2399            .complete_goal_raw(action.as_str(), goal_id, status, &buf[..len])
2400    }
2401
2402    /// Complete an action goal on the action entity synthesized from `name`.
2403    ///
2404    /// This pairs with
2405    /// [`DeclarativeNode::create_action_server_for_name`] and
2406    /// [`DeclarativeNode::create_action_server_for_name_with_callbacks`].
2407    pub fn complete_goal_for_name<R: RosMessage, const N: usize>(
2408        &mut self,
2409        name: &str,
2410        goal_id: &GoalId,
2411        status: GoalStatus,
2412        result: &R,
2413    ) -> NodeResult<()> {
2414        self.complete_goal::<R, N>(EntityId::new(name), goal_id, status, result)
2415    }
2416
2417    /// Visit each active (accepted, not yet completed) goal on `action` with its
2418    /// id + status — how a `tick` body discovers goals to feed / complete. Collect
2419    /// the ids you want to act on, then call [`Self::publish_feedback`] /
2420    /// [`Self::complete_goal`] after the visit returns (those borrow `self`
2421    /// mutably, so they can't run inside `visit`).
2422    #[doc(hidden)]
2423    pub fn for_each_active_goal(
2424        &self,
2425        action: EntityId<'_>,
2426        visit: &mut dyn FnMut(&GoalId, GoalStatus),
2427    ) {
2428        self.actions.for_each_active_goal(action.as_str(), visit);
2429    }
2430
2431    /// Visit active goals on the action entity synthesized from `name`.
2432    pub fn for_each_active_goal_for_name(
2433        &self,
2434        name: &str,
2435        visit: &mut dyn FnMut(&GoalId, GoalStatus),
2436    ) {
2437        self.for_each_active_goal(EntityId::new(name), visit);
2438    }
2439
2440    /// Publish typed feedback for an active action goal (W.5.6 — tick-only).
2441    #[doc(hidden)]
2442    pub fn publish_feedback<F: RosMessage, const N: usize>(
2443        &mut self,
2444        action: EntityId<'_>,
2445        goal_id: &GoalId,
2446        feedback: &F,
2447    ) -> NodeResult<()> {
2448        // RFC-0069 / issue 0418 — NO inner encapsulation header; see
2449        // `complete_goal` above. ROS 2's `<Action>_FeedbackMessage` is ONE CDR
2450        // message: `[header][goal_id][feedback fields]`. The executor frames
2451        // `[header][goal_id]` and this payload is the fields alone.
2452        let mut buf = [0u8; N];
2453        let mut writer = crate::CdrWriter::new(&mut buf);
2454        feedback
2455            .serialize(&mut writer)
2456            .map_err(|_| NodeDeclError::Runtime)?;
2457        let len = writer.position();
2458        self.actions
2459            .publish_feedback_raw(action.as_str(), goal_id, &buf[..len])
2460    }
2461
2462    /// Publish feedback on the action entity synthesized from `name`.
2463    pub fn publish_feedback_for_name<F: RosMessage, const N: usize>(
2464        &mut self,
2465        name: &str,
2466        goal_id: &GoalId,
2467        feedback: &F,
2468    ) -> NodeResult<()> {
2469        self.publish_feedback::<F, N>(EntityId::new(name), goal_id, feedback)
2470    }
2471
2472    /// Issue a service-client raw-CDR request and block on the reply
2473    /// (M-F.4 — tick-only). Writes the response CDR into `response_buf`
2474    /// and returns the response length in bytes.
2475    #[doc(hidden)]
2476    pub fn call_raw(
2477        &mut self,
2478        service: EntityId<'_>,
2479        request_cdr: &[u8],
2480        response_buf: &mut [u8],
2481    ) -> NodeResult<usize> {
2482        self.clients
2483            .call_raw(service.as_str(), request_cdr, response_buf)
2484    }
2485
2486    /// Whether a server for the service client `service` is CURRENTLY
2487    /// discoverable (M-F.4 — tick-only). Mirrors
2488    /// `rclcpp::ClientBase::service_is_ready`; see
2489    /// [`ClientDispatch::service_is_ready`] for the three answers. There is no
2490    /// blocking `wait_for_service` here because a tick must not block the
2491    /// executor that drives it — check once per tick and return, which is the
2492    /// rclcpp `while (!wait_for_service(1s)) { "waiting again..." }` idiom
2493    /// with the timer as the cadence (phase-428 W13).
2494    #[doc(hidden)]
2495    pub fn service_is_ready(&self, service: EntityId<'_>) -> NodeResult<bool> {
2496        self.clients.service_is_ready(service.as_str())
2497    }
2498
2499    /// [`Self::service_is_ready`] through the entity synthesized from `name`.
2500    pub fn service_is_ready_for_name(&self, name: &str) -> NodeResult<bool> {
2501        self.service_is_ready(EntityId::new(name))
2502    }
2503
2504    /// Whether a server for the action client `action` is CURRENTLY
2505    /// discoverable (tick-only) — `rclcpp_action::Client::action_server_is_ready`.
2506    /// Gate a goal on `Ok(true)` and treat `Err` ("the backend cannot say") as
2507    /// "send anyway": a goal sent to a server nobody has declared yet is lost,
2508    /// because the send is not retried (issue 1918). Check once per tick, the
2509    /// way [`Self::service_is_ready`] documents.
2510    #[doc(hidden)]
2511    pub fn action_server_is_ready(&self, action: EntityId<'_>) -> NodeResult<bool> {
2512        self.clients.action_server_is_ready(action.as_str())
2513    }
2514
2515    /// [`Self::action_server_is_ready`] through the entity synthesized from `name`.
2516    pub fn action_server_is_ready_for_name(&self, name: &str) -> NodeResult<bool> {
2517        self.action_server_is_ready(EntityId::new(name))
2518    }
2519
2520    /// Issue a raw service-client request through the entity synthesized
2521    /// from `name`.
2522    pub fn call_raw_for_name(
2523        &mut self,
2524        name: &str,
2525        request_cdr: &[u8],
2526        response_buf: &mut [u8],
2527    ) -> NodeResult<usize> {
2528        self.call_raw(EntityId::new(name), request_cdr, response_buf)
2529    }
2530
2531    /// Issue a typed service-client request and decode the reply
2532    /// (M-F.4 — tick-only). `REQ_N` / `RESP_N` stack-size the request /
2533    /// response CDR buffers; size them via
2534    /// `<<Req as RosMessage>::SerializedSize as nros::SerializedSize>::SIZE`.
2535    #[doc(hidden)]
2536    pub fn call<Req: RosMessage, Resp: RosMessage, const REQ_N: usize, const RESP_N: usize>(
2537        &mut self,
2538        service: EntityId<'_>,
2539        request: &Req,
2540    ) -> NodeResult<Resp> {
2541        let mut req_buf = [0u8; REQ_N];
2542        let mut writer =
2543            crate::CdrWriter::new_with_header(&mut req_buf).map_err(|_| NodeDeclError::Runtime)?;
2544        request
2545            .serialize(&mut writer)
2546            .map_err(|_| NodeDeclError::Runtime)?;
2547        let req_len = writer.position();
2548
2549        let mut resp_buf = [0u8; RESP_N];
2550        let resp_len =
2551            self.clients
2552                .call_raw(service.as_str(), &req_buf[..req_len], &mut resp_buf)?;
2553
2554        let mut reader = crate::CdrReader::new_with_header(&resp_buf[..resp_len])
2555            .map_err(|_| NodeDeclError::Runtime)?;
2556        Resp::deserialize(&mut reader).map_err(|_| NodeDeclError::Runtime)
2557    }
2558
2559    /// Issue a typed service-client request through the entity synthesized
2560    /// from `name`.
2561    pub fn call_for_name<
2562        Req: RosMessage,
2563        Resp: RosMessage,
2564        const REQ_N: usize,
2565        const RESP_N: usize,
2566    >(
2567        &mut self,
2568        name: &str,
2569        request: &Req,
2570    ) -> NodeResult<Resp> {
2571        self.call::<Req, Resp, REQ_N, RESP_N>(EntityId::new(name), request)
2572    }
2573
2574    /// Send a raw-CDR action-client goal and return the assigned
2575    /// [`GoalId`] (M-F.4 — tick-only). Result + feedback streams arrive
2576    /// via callback dispatch; this method only kicks off the request.
2577    #[doc(hidden)]
2578    pub fn send_goal_raw(&mut self, action: EntityId<'_>, goal_cdr: &[u8]) -> NodeResult<GoalId> {
2579        self.clients.send_goal_raw(action.as_str(), goal_cdr)
2580    }
2581
2582    /// Send a raw-CDR action-client goal through the entity synthesized
2583    /// from `name`.
2584    pub fn send_goal_raw_for_name(&mut self, name: &str, goal_cdr: &[u8]) -> NodeResult<GoalId> {
2585        self.send_goal_raw(EntityId::new(name), goal_cdr)
2586    }
2587
2588    /// Send a typed action-client goal and return the assigned
2589    /// [`GoalId`] (M-F.4 — tick-only). `N` stack-sizes the goal CDR
2590    /// buffer.
2591    #[doc(hidden)]
2592    pub fn send_goal<G: RosMessage, const N: usize>(
2593        &mut self,
2594        action: EntityId<'_>,
2595        goal: &G,
2596    ) -> NodeResult<GoalId> {
2597        // RFC-0069 / issue 0418 — NO inner encapsulation header, same rule as
2598        // `publish_feedback` / `complete_goal`. ROS 2's `<Action>_SendGoal_Request`
2599        // is ONE CDR message: `[header][goal_id][goal fields]`. `send_goal_raw`
2600        // frames `[header][goal_id]`, so this payload is the fields alone.
2601        //
2602        // Issue 0448: this path used `new_with_header` and shipped a SECOND
2603        // encapsulation (`header|uuid|header|fields`) — 4 bytes over the ROS 2
2604        // layout. Fast-DDS sizes its reader history from the type and drops the
2605        // sample outright ("Change payload size of '28' bytes is larger than the
2606        // history payload size of '27'"), so the goal never reached the server
2607        // and the client saw a zeroed default result. nros-c / nros-cpp already
2608        // stripped the header; only this Rust path was missed.
2609        //
2610        // Confirmed independently while root-causing issue 0461: the extra
2611        // header also made every SERVER decode the wrong offset over zenoh
2612        // (C/C++ read the inner header as the first field, so an order of 10
2613        // arrived as 256). Same defect, a second symptom.
2614        let mut buf = [0u8; N];
2615        let mut writer = crate::CdrWriter::new(&mut buf);
2616        goal.serialize(&mut writer)
2617            .map_err(|_| NodeDeclError::Runtime)?;
2618        let len = writer.position();
2619        self.clients.send_goal_raw(action.as_str(), &buf[..len])
2620    }
2621
2622    /// Send a typed action-client goal through the entity synthesized
2623    /// from `name`.
2624    pub fn send_goal_for_name<G: RosMessage, const N: usize>(
2625        &mut self,
2626        name: &str,
2627        goal: &G,
2628    ) -> NodeResult<GoalId> {
2629        self.send_goal::<G, N>(EntityId::new(name), goal)
2630    }
2631}
2632
2633#[cfg(feature = "rmw-cffi")]
2634pub trait ExecutableNode: Component {
2635    /// Per-instance mutable state shared across the component's callbacks.
2636    type State;
2637
2638    /// Build the initial state (called once by the generated runtime).
2639    fn init() -> Self::State;
2640
2641    /// Run the body for `callback`. `ctx` exposes the triggering payload + the
2642    /// immediate publish path. Bodies match on the source callback name declared
2643    /// by `create_*_for_callback_name` and related helpers.
2644    fn on_callback(state: &mut Self::State, callback: Callback<'_>, ctx: &mut CallbackCtx<'_>);
2645
2646    /// Per-spin execution hook (W.5.6), run *between* callback dispatch by the
2647    /// generated runtime — where the executor is free, so this is the only place
2648    /// a component can complete action goals / publish feedback (via `ctx`) or do
2649    /// periodic work. Default: no-op (timer/sub/service-only components).
2650    fn tick(_state: &mut Self::State, _ctx: &mut TickCtx<'_>) {}
2651}
2652
2653/// Emit a no-op [`ExecutableNode`] impl for a declarative-only component
2654/// (W.5.1). The generated runtime calls `on_callback` unconditionally, so a
2655/// component instantiated into a generated binary must impl `ExecutableNode`;
2656/// components without callback bodies use this to satisfy that contract:
2657///
2658/// ```ignore
2659/// pub struct Node;
2660/// impl nros::Component for Node { /* register(...) */ }
2661/// nros::declarative_component!(Node);
2662/// ```
2663#[macro_export]
2664macro_rules! declarative_component {
2665    ($ty:ty) => {
2666        impl $crate::ExecutableNode for $ty {
2667            type State = ();
2668            fn init() -> Self::State {}
2669            fn on_callback(
2670                _state: &mut Self::State,
2671                _callback: $crate::Callback<'_>,
2672                _ctx: &mut $crate::CallbackCtx<'_>,
2673            ) {
2674            }
2675        }
2676    };
2677}
2678
2679/// Run component registration against a metadata recorder, on `executor`.
2680///
2681/// phase-483 W3: registration creates real nodes, so it needs an executor even
2682/// on the probe's road — open one on the `metadata` backend
2683/// (`ExecutorConfig::new("").rmw("metadata")`). The recorder receives every
2684/// declaration exactly as it did when it was the whole runtime.
2685#[cfg(feature = "rmw-cffi")]
2686pub fn register_node<C: Component>(
2687    recorder: &mut dyn NodeRuntime,
2688    executor: &mut crate::Executor<'static>,
2689) -> NodeResult<()> {
2690    let mut sink = RecordSink { recorder };
2691    let mut frame = ComponentFrame::new(&mut sink);
2692    let mut context = NodeContext::new(C::NAME, executor, &mut frame);
2693    C::register(&mut context)
2694}
2695
2696/// Run `f` against a [`NodeContext`] whose declarations go to `recorder`,
2697/// on `executor` (opened on the `metadata` backend). The tests of the
2698/// declarative surface use it; the probe uses [`record_node_metadata`].
2699#[cfg(feature = "rmw-cffi")]
2700#[doc(hidden)]
2701pub fn __record_with_context<T>(
2702    component_name: &'static str,
2703    recorder: &mut dyn NodeRuntime,
2704    executor: &mut crate::Executor<'static>,
2705    f: impl FnOnce(&mut NodeContext<'_>) -> T,
2706) -> T {
2707    let mut sink = RecordSink { recorder };
2708    let mut frame = ComponentFrame::new(&mut sink);
2709    let mut context = NodeContext::new(component_name, executor, &mut frame);
2710    f(&mut context)
2711}
2712
2713/// Phase 212.M.5.a.4 internal — `Box`-erase a freshly built component
2714/// `State` to the type-erased `*mut ()` ABI the BSP path uses. Called
2715/// only from the `nros::node!()` macro emit; not public API.
2716///
2717/// The returned pointer is a leaked `Box`; the BSP runtime keeps it
2718/// alive for the firmware lifetime (embedded slots never deallocate).
2719#[cfg(all(feature = "alloc", feature = "rmw-cffi"))]
2720#[doc(hidden)]
2721pub fn __private_node_state_into_raw<C: ExecutableNode>(state: C::State) -> *mut () {
2722    extern crate alloc;
2723    alloc::boxed::Box::into_raw(alloc::boxed::Box::new(state)) as *mut ()
2724}
2725
2726/// Run component registration against an in-memory metadata recorder, on an
2727/// executor the caller opened on the `metadata` backend.
2728#[cfg(feature = "rmw-cffi")]
2729pub fn record_node_metadata<C: Component>(
2730    recorder: &mut dyn NodeRuntime,
2731    executor: &mut crate::Executor<'static>,
2732) -> NodeResult<()> {
2733    register_node::<C>(recorder, executor)
2734}
2735
2736#[cfg(test)]
2737mod tests {
2738    use super::*;
2739    use crate::{CdrReader, CdrWriter, DeserError, SerError};
2740
2741    #[derive(Debug, Clone, Copy, Default)]
2742    struct TestMsg;
2743
2744    impl crate::Serialize for TestMsg {
2745        fn serialize(&self, _writer: &mut CdrWriter) -> Result<(), SerError> {
2746            Ok(())
2747        }
2748    }
2749
2750    impl crate::Deserialize for TestMsg {
2751        fn deserialize(_reader: &mut CdrReader) -> Result<Self, DeserError> {
2752            Ok(Self)
2753        }
2754    }
2755
2756    impl RosMessage for TestMsg {
2757        const TYPE_NAME: &'static str = "test_msgs::msg::dds_::Test_";
2758        const TYPE_HASH: &'static str = "test_hash";
2759    }
2760
2761    // Phase 380 W4 — `MessageForRmw` now also requires a field schema, because
2762    // that is where a subscription's size bound comes from. `TestMsg` is an
2763    // empty struct, so its schema is the empty slice and its bound is just the
2764    // encapsulation header — which is exactly what the build assertion should
2765    // see for it.
2766    impl nros_serdes::schema::Message for TestMsg {
2767        const TYPE_NAME: &'static str = "test_msgs/msg/Test";
2768        const FIELDS: &'static [nros_serdes::schema::Field] = &[];
2769    }
2770
2771    #[cfg(feature = "rmw-cffi")]
2772    struct TalkerComponent;
2773
2774    #[cfg(feature = "rmw-cffi")]
2775    impl Component for TalkerComponent {
2776        const NAME: &'static str = "talker_component";
2777
2778        fn register(context: &mut NodeContext<'_>) -> NodeResult<()> {
2779            let mut node =
2780                context.create_node_with_id(NodeId::new("node"), NodeOptions::new("talker"))?;
2781            let _publisher =
2782                node.declare_publisher::<TestMsg>(EntityId::new("pub_chatter"), "chatter")?;
2783            let _subscription = node.declare_subscription::<TestMsg>(
2784                EntityId::new("sub_cmd"),
2785                CallbackId::new("on_cmd"),
2786                "~/cmd",
2787            )?;
2788            let _timer = node.declare_timer(
2789                EntityId::new("timer_tick"),
2790                CallbackId::new("on_tick"),
2791                TimerDuration::from_millis(10),
2792            )?;
2793            let _parameter =
2794                node.declare_parameter(EntityId::new("param_gain"), "gain", ParameterType::Double)?;
2795            node.callback(CallbackId::new("on_tick"))
2796                .publishes(EntityId::new("pub_chatter"))?
2797                .writes(EntityId::new("param_gain"))?;
2798            Ok(())
2799        }
2800    }
2801
2802    #[test]
2803    fn component_missing_export_error_message_is_clear() {
2804        assert_eq!(
2805            NodeDeclError::MissingExport.message(),
2806            "package has no exported nros component"
2807        );
2808    }
2809
2810    // W.5.1 — an executable component callback runs its body: mutates state +
2811    // publishes immediately through the resolver (the substrate the generator
2812    // will wire). `TalkerComponent` already impls `Node` (declarative);
2813    // here it also impls `ExecutableNode`.
2814    #[cfg(feature = "rmw-cffi")]
2815    impl ExecutableNode for TalkerComponent {
2816        type State = u32;
2817
2818        fn init() -> u32 {
2819            0
2820        }
2821
2822        fn on_callback(state: &mut u32, callback: Callback<'_>, ctx: &mut CallbackCtx<'_>) {
2823            if callback.as_str() == "on_tick" {
2824                *state += 1;
2825                // Publish through the declared publisher entity.
2826                let _ = ctx.publish::<TestMsg, 64>(EntityId::new("pub_chatter"), &TestMsg);
2827            }
2828        }
2829    }
2830
2831    #[cfg(feature = "rmw-cffi")]
2832    #[test]
2833    fn executable_component_callback_publishes_and_mutates_state() {
2834        use core::cell::RefCell;
2835
2836        struct RecordingResolver {
2837            last: RefCell<Option<(MetadataString, usize)>>,
2838        }
2839        impl PublisherResolver for RecordingResolver {
2840            fn publish_raw(&self, entity_id: &str, data: &[u8]) -> NodeResult<()> {
2841                *self.last.borrow_mut() = Some((copy_str(entity_id)?, data.len()));
2842                Ok(())
2843            }
2844        }
2845
2846        let resolver = RecordingResolver {
2847            last: RefCell::new(None),
2848        };
2849        let mut state = TalkerComponent::init();
2850        let mut ctx = CallbackCtx::new(&[], &resolver);
2851
2852        // An unrelated callback id does nothing.
2853        TalkerComponent::on_callback(
2854            &mut state,
2855            Callback::__from_id(CallbackId::new("other")),
2856            &mut ctx,
2857        );
2858        assert_eq!(state, 0);
2859        assert!(resolver.last.borrow().is_none());
2860
2861        // The bound callback bumps state + publishes through "pub_chatter".
2862        TalkerComponent::on_callback(
2863            &mut state,
2864            Callback::__from_id(CallbackId::new("on_tick")),
2865            &mut ctx,
2866        );
2867        assert_eq!(state, 1);
2868        let last = resolver.last.borrow();
2869        let (entity, len) = last.as_ref().expect("a publish was recorded");
2870        assert_eq!(entity.as_str(), "pub_chatter");
2871        // Empty TestMsg ⇒ just the 4-byte CDR header.
2872        assert_eq!(*len, 4);
2873    }
2874
2875    // W.5.3 — a service-style body writes its reply through the CallbackCtx
2876    // reply sink; the trampoline reads `*written` back. A timer/sub ctx (no
2877    // sink) rejects a reply.
2878    #[test]
2879    fn callback_ctx_reply_sink_roundtrips() {
2880        struct NoopResolver;
2881        impl PublisherResolver for NoopResolver {
2882            fn publish_raw(&self, _entity_id: &str, _data: &[u8]) -> NodeResult<()> {
2883                Ok(())
2884            }
2885        }
2886        let resolver = NoopResolver;
2887        let mut reply_buf = [0u8; 64];
2888        let mut written = 0usize;
2889        {
2890            let mut ctx = CallbackCtx::with_reply(&[], &resolver, &mut reply_buf, &mut written);
2891            ctx.reply::<TestMsg, 64>(&TestMsg).unwrap();
2892        }
2893        // Empty TestMsg ⇒ just the 4-byte CDR header.
2894        assert_eq!(written, 4);
2895
2896        // A reply-less ctx (timer / subscription) rejects a reply.
2897        let mut ctx2 = CallbackCtx::new(&[], &resolver);
2898        assert!(ctx2.reply_raw(&[1, 2, 3]).is_err());
2899    }
2900
2901    // Phase 264 W4c — a callback reads the live parameter value through
2902    // `CallbackCtx::parameter::<T>` once the dispatch site threads the store in.
2903    #[cfg(feature = "param-store")]
2904    #[test]
2905    fn callback_ctx_reads_param() {
2906        struct NoopResolver;
2907        impl PublisherResolver for NoopResolver {
2908            fn publish_raw(&self, _entity_id: &str, _data: &[u8]) -> NodeResult<()> {
2909                Ok(())
2910            }
2911        }
2912        let resolver = NoopResolver;
2913
2914        // No store threaded ⇒ every read is None (param-services off / not registered).
2915        let ctx_none = CallbackCtx::new(&[], &resolver);
2916        assert_eq!(ctx_none.parameter::<i64>("speed"), None);
2917
2918        // Seed a store with the typed value a `ros2 param set speed 7` would land on.
2919        // phase-382 W2' — the slots are the caller's; a local `ParameterStorage`
2920        // is the shape a test wants, sized for what it actually declares rather
2921        // than the build-time `MAX_PARAMETERS` default.
2922        // phase-426 W1/W3 — every store call names the node it is about.
2923        // `talker` is the executor's first node, `listener` its second.
2924        let talker = nros_params::NodeKey::PRIMARY;
2925        let listener = nros_params::NodeKey::new(1);
2926        let mut storage = crate::ParameterStorage::<4>::new();
2927        let mut server = crate::ParameterServer::new_in(storage.as_table());
2928        assert!(server.declare(talker, "speed", crate::ParameterValue::Integer(7)));
2929        assert!(server.declare(listener, "speed", crate::ParameterValue::Integer(99)));
2930
2931        let mut ctx = CallbackCtx::new(&[], &resolver);
2932        ctx.set_param_server(Some(&server), talker);
2933        assert_eq!(ctx.parameter::<i64>("speed"), Some(7));
2934        // Wrong type ⇒ None, not a panic.
2935        assert_eq!(ctx.parameter::<bool>("speed"), None);
2936        // Undeclared ⇒ None.
2937        assert_eq!(ctx.parameter::<i64>("missing"), None);
2938
2939        // phase-426 W3 — the SIBLING's identically-named parameter is a
2940        // different parameter, and the context reads the node it was given.
2941        // Before W3 this ctx was pinned to PRIMARY, so a listener component
2942        // read the talker's 7.
2943        let mut sibling = CallbackCtx::new(&[], &resolver);
2944        sibling.set_param_server(Some(&server), listener);
2945        assert_eq!(sibling.parameter::<i64>("speed"), Some(99));
2946    }
2947
2948    // Phase 250 Wave 2 — the declarative `.safety()` surface: a normal ctx has
2949    // no integrity status; one built with `new_with_integrity` exposes it, read
2950    // alongside the message in the same callback (Shape A).
2951    #[cfg(feature = "safety-e2e")]
2952    #[test]
2953    fn callback_ctx_integrity_surface() {
2954        struct NoopResolver;
2955        impl PublisherResolver for NoopResolver {
2956            fn publish_raw(&self, _entity_id: &str, _data: &[u8]) -> NodeResult<()> {
2957                Ok(())
2958            }
2959        }
2960        let resolver = NoopResolver;
2961
2962        // Non-safety dispatch (timer / plain sub) → None.
2963        let ctx = CallbackCtx::new(&[], &resolver);
2964        assert!(ctx.integrity().is_none());
2965
2966        // Safety dispatch → the status rides alongside the payload.
2967        let status = crate::IntegrityStatus {
2968            gap: 2,
2969            duplicate: false,
2970            crc_valid: Some(true),
2971        };
2972        let ctx = CallbackCtx::new_with_integrity(&[], &resolver, &status);
2973        let got = ctx.integrity().expect("safety ctx carries status");
2974        assert_eq!(got.gap, 2);
2975        assert!(!got.duplicate);
2976        assert_eq!(got.crc_valid, Some(true));
2977    }
2978
2979    // W.5.3 — an action goal / cancel body sets its accept/reject decision
2980    // through the CallbackCtx decision sink; the trampoline returns `*out`. A
2981    // wrong-kind setter (or a sink-less ctx) errors.
2982    #[test]
2983    fn callback_ctx_decision_sink() {
2984        struct NoopResolver;
2985        impl PublisherResolver for NoopResolver {
2986            fn publish_raw(&self, _entity_id: &str, _data: &[u8]) -> NodeResult<()> {
2987                Ok(())
2988            }
2989        }
2990        let resolver = NoopResolver;
2991
2992        let mut gr = GoalResponse::Reject;
2993        {
2994            let mut ctx = CallbackCtx::with_goal_decision(&[], &resolver, &mut gr);
2995            ctx.set_goal_response(GoalResponse::AcceptAndExecute)
2996                .unwrap();
2997            // Wrong-kind setter on a goal ctx errors.
2998            assert!(ctx.set_cancel_response(CancelResponse::Accept).is_err());
2999        }
3000        assert!(matches!(gr, GoalResponse::AcceptAndExecute));
3001
3002        let mut cr = CancelResponse::Reject;
3003        {
3004            let mut ctx = CallbackCtx::with_cancel_decision(&[], &resolver, &mut cr);
3005            ctx.set_cancel_response(CancelResponse::Accept).unwrap();
3006        }
3007        assert!(matches!(cr, CancelResponse::Accept));
3008
3009        // A timer/sub ctx (no decision sink) rejects both.
3010        let mut ctx3 = CallbackCtx::new(&[], &resolver);
3011        assert!(ctx3.set_goal_response(GoalResponse::Reject).is_err());
3012        assert!(ctx3.set_cancel_response(CancelResponse::Accept).is_err());
3013    }
3014
3015    // W.5.6 — the tick hook publishes (immediate) + drives executor-backed action
3016    // ops (complete goal / publish feedback) through the ActionExecutor seam.
3017    #[test]
3018    fn tick_ctx_publish_and_action_ops() {
3019        use core::cell::Cell;
3020        struct RecPub {
3021            published: Cell<bool>,
3022        }
3023        impl PublisherResolver for RecPub {
3024            fn publish_raw(&self, _entity_id: &str, _data: &[u8]) -> NodeResult<()> {
3025                self.published.set(true);
3026                Ok(())
3027            }
3028        }
3029        struct RecAct {
3030            completed: bool,
3031            fed: bool,
3032            visited: usize,
3033        }
3034        impl ActionExecutor for RecAct {
3035            fn complete_goal_raw(
3036                &mut self,
3037                _action_entity: &str,
3038                _goal_id: &GoalId,
3039                _status: GoalStatus,
3040                _result: &[u8],
3041            ) -> NodeResult<()> {
3042                self.completed = true;
3043                Ok(())
3044            }
3045            fn publish_feedback_raw(
3046                &mut self,
3047                _action_entity: &str,
3048                _goal_id: &GoalId,
3049                _feedback: &[u8],
3050            ) -> NodeResult<()> {
3051                self.fed = true;
3052                Ok(())
3053            }
3054            fn for_each_active_goal(
3055                &self,
3056                _action_entity: &str,
3057                visit: &mut dyn FnMut(&GoalId, GoalStatus),
3058            ) {
3059                // One pretend-active goal, so the tick body has something to drive.
3060                visit(&GoalId::zero(), GoalStatus::Executing);
3061            }
3062        }
3063
3064        struct RecClients;
3065        impl ClientDispatch for RecClients {
3066            fn call_raw(
3067                &mut self,
3068                _service: &str,
3069                _req: &[u8],
3070                _resp: &mut [u8],
3071            ) -> NodeResult<usize> {
3072                Err(NodeDeclError::Runtime)
3073            }
3074            fn send_goal_raw(&mut self, _action: &str, _goal: &[u8]) -> NodeResult<GoalId> {
3075                Err(NodeDeclError::Runtime)
3076            }
3077        }
3078
3079        let pubs = RecPub {
3080            published: Cell::new(false),
3081        };
3082        let mut acts = RecAct {
3083            completed: false,
3084            fed: false,
3085            visited: 0,
3086        };
3087        let mut clients = RecClients;
3088        let goal = GoalId::zero();
3089        let mut seen = 0usize;
3090        {
3091            let mut ctx = TickCtx::new(&pubs, &mut acts, &mut clients);
3092            ctx.publish::<TestMsg, 64>(EntityId::new("pub_x"), &TestMsg)
3093                .unwrap();
3094            // Discover the active goal the way a real tick body does, then act on it.
3095            ctx.for_each_active_goal(EntityId::new("act"), &mut |_id, _status| seen += 1);
3096            ctx.publish_feedback::<TestMsg, 64>(EntityId::new("act"), &goal, &TestMsg)
3097                .unwrap();
3098            ctx.complete_goal::<TestMsg, 64>(
3099                EntityId::new("act"),
3100                &goal,
3101                GoalStatus::Succeeded,
3102                &TestMsg,
3103            )
3104            .unwrap();
3105        }
3106        acts.visited = seen;
3107        assert!(pubs.published.get());
3108        assert!(acts.completed);
3109        assert!(acts.fed);
3110        assert_eq!(acts.visited, 1);
3111    }
3112
3113    /// Phase 216.A.3 — `Node::DISPATCH` defaults to
3114    /// `DispatchStrategy::Inline` so every pre-216 `impl Node`
3115    /// keeps compiling unchanged.
3116    #[cfg(feature = "rmw-cffi")]
3117    #[test]
3118    fn node_dispatch_default_is_inline() {
3119        struct Dummy;
3120        impl Component for Dummy {
3121            const NAME: &'static str = "dummy";
3122            fn register(_: &mut NodeContext<'_>) -> NodeResult<()> {
3123                Ok(())
3124            }
3125        }
3126        assert_eq!(Dummy::DISPATCH, crate::DispatchStrategy::Inline);
3127    }
3128
3129    // Phase 216.A.5 — `nros::node!()` emits the
3130    // `__nros_node_<pkg>_dispatch_strategy()` ABI export. We invoke the
3131    // macro on a dummy Node + ExecutableNode pair in a private sub-module
3132    // here so the macro expansion lives inside the `nros` crate itself;
3133    // the emitted `#[unsafe(no_mangle)] extern "C"` symbol is global, so
3134    // the test below re-declares + calls it. If the macro stopped
3135    // emitting the symbol (or renamed it) this would link-fail.
3136    //
3137    // `<pkg>` resolves to `CARGO_PKG_NAME` after
3138    // `sanitize_pkg_name_for_symbol`. The `nros` crate's pkg name is
3139    // literal `nros`, so the expected symbol is
3140    // `__nros_node_nros_dispatch_strategy`.
3141    // Phase 216 final wave — the macro emit now references
3142    // `::nros::Executor` (rmw-cffi-gated) in addition to the existing
3143    // alloc-gated `__private_node_state_into_raw`. Gate the test on
3144    // both features so the macro invocation only attempts to expand
3145    // when every referenced symbol is present.
3146    #[cfg(all(feature = "alloc", feature = "rmw-cffi", feature = "macros"))]
3147    mod dispatch_probe_macro_test {
3148        // `extern crate self as nros;` at the crate root (in `lib.rs`,
3149        // `cfg(test)`-gated) lets the `::nros::*` paths the macro emits
3150        // resolve in-crate.
3151        use super::*;
3152
3153        pub struct DispatchProbe;
3154
3155        impl Component for DispatchProbe {
3156            const NAME: &'static str = "dispatch_probe";
3157            // Default `DISPATCH = Inline` ⇒ discriminant 0.
3158            fn register(_: &mut NodeContext<'_>) -> NodeResult<()> {
3159                Ok(())
3160            }
3161        }
3162
3163        impl ExecutableNode for DispatchProbe {
3164            type State = ();
3165            fn init() -> Self::State {}
3166            fn on_callback(
3167                _state: &mut Self::State,
3168                _callback: Callback<'_>,
3169                _ctx: &mut CallbackCtx<'_>,
3170            ) {
3171            }
3172        }
3173
3174        // Emits both the per-pkg `register` wrapper AND the new
3175        // `__nros_node_nros_dispatch_strategy` ABI symbol.
3176        nros_macros::node!(DispatchProbe);
3177    }
3178
3179    // Also `macros`: this asserts the ABI symbol the `node!` invocation above
3180    // emits, so without the macro there is nothing to assert and the extern
3181    // would not resolve.
3182    #[cfg(all(feature = "alloc", feature = "rmw-cffi", feature = "macros"))]
3183    #[test]
3184    fn node_macro_emits_dispatch_strategy_symbol() {
3185        // Re-declare the ABI export the macro just emitted. If the macro
3186        // elides the symbol (or renames it) this fails to link — exactly
3187        // the regression the test is meant to catch.
3188        unsafe extern "C" {
3189            fn __nros_node_nros_dispatch_strategy() -> u8;
3190        }
3191        let strategy = unsafe { __nros_node_nros_dispatch_strategy() };
3192        // The probe Node uses the default `DISPATCH = Inline`
3193        // (discriminant 0) — confirms the macro is splicing
3194        // `<Type as Component>::DISPATCH as u8`, not a hard-coded zero.
3195        assert_eq!(strategy, crate::DispatchStrategy::Inline as u8);
3196        assert_eq!(strategy, 0);
3197    }
3198
3199    // The `nros::node!()` macro also emits
3200    // `__nros_node_<pkg>_on_callback`, the extern "C" trampoline the
3201    // RTIC / Embassy dispatch tasks call after dequeuing a
3202    // `SignaledCallback<'static>` (see `nros-platform::SignaledCallback`).
3203    // The expansion lives in the same `dispatch_probe_macro_test`
3204    // sub-module as the dispatch-strategy probe, so a single
3205    // `nros_macros::node!(DispatchProbe);` invocation covers both
3206    // symbols. Symbol name resolves to
3207    // `__nros_node_nros_on_callback` (CARGO_PKG_NAME = "nros").
3208    //
3209    // The test only confirms the symbol is linkable — actually
3210    // invoking the trampoline would need a live State + CallbackCtx
3211    // pointer pair, which is the dispatch-task author's contract
3212    // (documented in the macro emit). A link-only probe is enough to
3213    // catch the macro silently eliding the export — the exact
3214    // regression class this test is for.
3215    #[cfg(all(feature = "alloc", feature = "rmw-cffi"))]
3216    #[test]
3217    fn node_macro_emits_on_callback_symbol() {
3218        unsafe extern "C" {
3219            fn __nros_node_nros_on_callback(
3220                state: *mut core::ffi::c_void,
3221                cb_id_ptr: *const u8,
3222                cb_id_len: usize,
3223                ctx: *mut core::ffi::c_void,
3224            );
3225        }
3226        // Take the address of the symbol and feed it through
3227        // `core::hint::black_box` — forces the linker to resolve the
3228        // symbol and prevents the optimiser from folding the unused
3229        // reference away. If the macro stopped emitting the export
3230        // this line fails at link time, which is the exact regression
3231        // class this test catches. (`fn`-pointer values are never
3232        // null per Rust's type system, so a direct null check would
3233        // be a tautology — `-D useless-ptr-null-checks` would reject
3234        // it.)
3235        let fn_ptr: unsafe extern "C" fn(
3236            *mut core::ffi::c_void,
3237            *const u8,
3238            usize,
3239            *mut core::ffi::c_void,
3240        ) = __nros_node_nros_on_callback;
3241        core::hint::black_box(fn_ptr);
3242    }
3243}