nros_node/cyclonedds_register.rs
1//! Per-type descriptor registration — generic seam (Phase 248 C2).
2//!
3//! Some RMW backends (Cyclone DDS) resolve topic-type descriptors via a
4//! runtime registry instead of a static-init table. Each `nros-node`
5//! typed creator (`create_publisher`, `create_subscription`,
6//! `create_client`, `create_service`, `create_action_*`) routes through
7//! [`register_type::<M>`] *before* asking the cffi vtable to create the
8//! entity so the descriptor exists when the backend's `dds_create_topic`
9//! (or equivalent) runs.
10//!
11//! # No named-backend dependency (issue #60, Tier 1)
12//!
13//! Previously this module depended directly on `nros-rmw-cyclonedds` and
14//! called `nros_rmw_cyclonedds::register::<M>()`, baking a concrete-RMW
15//! crate into the platform/RMW-agnostic core executor. It now forwards
16//! the message's flattened schema through the generic
17//! [`nros_rmw::register_type_descriptor`] seam. The Cyclone backend
18//! installs its registrar from its own crate
19//! (`nros-rmw-cyclonedds::install_descriptor_registrar`, driven by the
20//! `-sys` shim's `RMW_INIT_ENTRIES` self-registration); zenoh / xrce
21//! install nothing, so the seam is a no-op there.
22//!
23//! # cfg gating (auto-detected, not feature-gated)
24//!
25//! This module's schema-passing body compiles to a no-op unless
26//! `cfg(rmw_cyclonedds_present)` is on. The cfg is emitted by
27//! `nros-node/build.rs` from the private internal `__cyclonedds-link`
28//! marker feature (no dep edge), which the umbrella `nros/rmw-cyclonedds`
29//! activates alongside its own `dep:nros-rmw-cyclonedds-sys`. Callers
30//! depend on `nros = { features = ["rmw-cyclonedds"] }`; the hook lights
31//! up automatically — no user-facing feature flag on `nros-node`. Each
32//! typed creator calls [`register_type::<M>`] unconditionally; the body
33//! is empty when the cfg is off so zenoh/xrce paths pay nothing. With the
34//! cfg on, the caller pays one mutex acquisition + one lookup per creator
35//! invocation (idempotent; the backend caches the descriptor on first
36//! hit).
37//!
38//! # Trait bound — [`MessageForRmw`]
39//!
40//! A descriptor-needing backend needs [`nros_serdes::schema::Message`]
41//! for the static field schema, but `nros-node`'s typed creators
42//! historically only constrain `M: nros_core::RosMessage`. Adding
43//! `Message` as a super-bound on `RosMessage` breaks every existing
44//! codegen-emitted msg crate (they impl `RosMessage` but not yet
45//! `Message`). Adding it as a per-method bound on every typed creator
46//! touches 30+ sites.
47//!
48//! Compromise: introduce a helper trait [`MessageForRmw`] that is **the
49//! bound the typed creators use** in place of bare `M: RosMessage`. It is
50//! a blanket impl over `RosMessage` whose extra requirement is `Message`
51//! when `cfg(rmw_cyclonedds_present)` is on, and just `RosMessage` when
52//! off.
53//!
54//! Net effect: a msg crate that impls `RosMessage` works as-is for zenoh
55//! + xrce builds; for cyclonedds builds it must additionally impl
56//! `Message`. The codegen template (`nros-cli` — separate repo) emits
57//! both impls for every generated msg crate.
58//!
59//! # Error mapping
60//!
61//! A registrar failure flattens onto [`crate::NodeError::Transport`] with
62//! [`nros_rmw::TransportError::PublisherCreationFailed`]. The choice not
63//! to add a dedicated `NodeError` variant is deliberate: the C/C++ FFI
64//! shim widens to a single `nros_ret_t`, and the failure mode
65//! (out-of-capacity registry, descriptor build error, etc.) is a "topic
66//! could not be created" from the caller's perspective.
67
68use nros_core::RosMessage;
69
70/// Bound used in place of bare `RosMessage` on typed creators.
71///
72/// Equivalent to `RosMessage` without `cfg(rmw_cyclonedds_present)`;
73/// equal to `RosMessage + nros_serdes::schema::Message` with it.
74///
75/// See module-level docs for the rationale.
76#[cfg(rmw_cyclonedds_present)]
77pub trait MessageForRmw: RosMessage + nros_serdes::schema::Message {}
78
79#[cfg(rmw_cyclonedds_present)]
80impl<T> MessageForRmw for T where T: RosMessage + nros_serdes::schema::Message {}
81
82#[cfg(not(rmw_cyclonedds_present))]
83pub trait MessageForRmw: RosMessage {}
84
85#[cfg(not(rmw_cyclonedds_present))]
86impl<T> MessageForRmw for T where T: RosMessage {}
87
88// ============================================================================
89// register_type::<M>() — the K.7.6.b hook
90// ============================================================================
91
92/// Register `M`'s topic-type descriptor with whichever RMW backend
93/// installed the generic descriptor seam (`nros_rmw::register_type_descriptor`).
94///
95/// No-op when `cfg(rmw_cyclonedds_present)` is off (zenoh / xrce builds
96/// never compile the schema-passing body). With the cfg on, flattens
97/// `M`'s static schema (`TYPE_NAME` + `FIELDS`) and forwards it to the
98/// installed [`nros_rmw::TypeDescriptorRegistrar`] — Cyclone DDS installs
99/// one from its own crate (`nros-rmw-cyclonedds::install_descriptor_registrar`),
100/// so the core executor no longer needs a named dependency on the Cyclone
101/// shim. The first call for a given type builds + caches the descriptor;
102/// subsequent calls are O(1) lookups inside the backend.
103///
104/// Returns `Ok(())` on success (including the "no descriptor-needing
105/// backend installed" no-op), or
106/// `NodeError::Transport(TransportError::PublisherCreationFailed)` on a
107/// backend-side build/registry failure.
108#[allow(unused_variables)] // M unused without the cfg
109#[inline]
110pub fn register_type<M: MessageForRmw>() -> Result<(), crate::NodeError> {
111 #[cfg(rmw_cyclonedds_present)]
112 {
113 // SAFETY-OF-INPUT: `M: Message` is enforced by the `MessageForRmw`
114 // bound under `rmw_cyclonedds_present`, so `TYPE_NAME` / `FIELDS`
115 // are available and the registrar receives the schema it expects.
116 nros_rmw::register_type_descriptor(
117 <M as nros_serdes::schema::Message>::TYPE_NAME,
118 <M as nros_serdes::schema::Message>::FIELDS,
119 )
120 .map_err(|err| {
121 #[cfg(feature = "log")]
122 log::error!(
123 "nros_rmw::register_type_descriptor::<{}>() failed: {:?}",
124 <M as nros_serdes::schema::Message>::TYPE_NAME,
125 err
126 );
127 let _ = err; // silence unused without log
128 crate::NodeError::Transport(err)
129 })?;
130 }
131 Ok(())
132}