Skip to main content

nros_rmw_cffi/
rust_adapter.rs

1//! Phase 115.L.0 — Generic Rust-trait → C-vtable adapter.
2//!
3//! `RustBackendAdapter<R>` converts any `R: nros_rmw::Rmw` whose
4//! associated types implement the matching `Session` / `Publisher` /
5//! `Subscription` / `ServiceTrait` / `ClientTrait` traits
6//! into a `static NrosRmwVtable`. Each per-backend cffi crate then
7//! collapses to ~10 LOC:
8//!
9//! ```ignore
10//! #[unsafe(no_mangle)]
11//! pub extern "C" fn nros_rmw_zenoh_register() -> nros_rmw_cffi::NrosRmwRet {
12//!     nros_rmw_cffi::RustBackendAdapter::<nros_rmw_zenoh::ZenohRmw>::register()
13//! }
14//! ```
15//!
16//! # Storage discipline
17//!
18//! - Session: `Box::into_raw(Box::new(session))` is stashed in
19//!   `NrosRmwSession::backend_data`. `close` reclaims via
20//!   `Box::from_raw` (drops the box, runs `Drop`, frees the alloc).
21//! - Publisher / Subscription / Service / Client: same
22//!   pattern with their respective handle types.
23//!
24//! # 'static
25//!
26//! Every handle must be `'static` (the `Box` outlives the call). We
27//! deliberately do **not** require `Send`: the C runtime hands the
28//! `backend_data` pointer back to the same caller that minted it,
29//! and the executor's single-thread-per-session invariant matches
30//! the Rust trait surface. Zenoh-pico's `ZenohSession`, for
31//! instance, holds an unmovable `*const Context` which the
32//! upstream library does not mark `Send`.
33//!
34//! # Bounds
35//!
36//! All error types must be `TransportError` (or `Into<TransportError>`).
37//! This matches every in-tree backend today.
38
39extern crate alloc;
40
41use alloc::boxed::Box;
42use core::{
43    ffi::{c_char, c_void},
44    marker::PhantomData,
45};
46
47use nros_rmw::{
48    ClientTrait, Publisher, QoSProfile, Rmw, RmwConfig, ServiceTrait, Session, SessionMode,
49    Subscription, TopicInfo, TransportError,
50};
51
52use crate::{
53    EMPTY_VTABLE, MAX_SESSION_PROPERTIES, NROS_RMW_RET_INVALID_ARGUMENT, NROS_RMW_RET_OK,
54    NROS_RMW_RET_UNSUPPORTED, NrosRmwClient, NrosRmwEventCallback, NrosRmwEventKind, NrosRmwNode,
55    NrosRmwPublisher, NrosRmwQos, NrosRmwRet, NrosRmwService, NrosRmwSession,
56    NrosRmwSessionOptions, NrosRmwSubscription, NrosRmwVtable, event_kind_from_c, ret_from_error,
57    rmw_publisher_options_t, rmw_subscription_options_t,
58};
59// phase-381 W3 — the endpoint-info slots marshal the generated ABI types
60// directly. Imported from `generated` rather than re-exported through the crate
61// root: these are bindgen output and the root deliberately re-exports only the
62// hand-maintained surface.
63use crate::generated::{
64    NROS_RMW_DURABILITY_UNKNOWN, NROS_RMW_HISTORY_UNKNOWN, NROS_RMW_RELIABILITY_UNKNOWN,
65    rmw_endpoint_type_t, rmw_gid_t, rmw_liveliness_kind_t, rmw_qos_profile_t,
66    rmw_topic_endpoint_info_t,
67};
68
69#[cfg(all(target_os = "none", not(feature = "std")))]
70mod static_subscriber_storage {
71    use core::{cell::UnsafeCell, mem, ptr};
72
73    use portable_atomic::{AtomicBool, Ordering};
74
75    // Issue 0269 — the pool size is build-time configurable via
76    // `NROS_RMW_SUBSCRIBER_SLOTS` (build.rs, default 8). The old
77    // hardcoded 4 silently capped every embedded session at four
78    // subscriptions: the fifth `create_subscription` hit the exhausted
79    // pool, returned BAD_ALLOC, and the executor surfaced it as an
80    // opaque `SubscriberCreationFailed` (the autoware_sentinel comp-all
81    // wall — every std target Boxes instead and never sees a limit).
82    const SLOT_COUNT: usize = crate::parse_env_usize(
83        env!("NROS_RMW_SUBSCRIBER_SLOTS"),
84        "NROS_RMW_SUBSCRIBER_SLOTS must be a decimal integer",
85    );
86    const SLOT_SIZE: usize = 1024;
87    const SLOT_ALIGN: usize = 16;
88
89    #[repr(align(16))]
90    struct Slot {
91        bytes: UnsafeCell<[u8; SLOT_SIZE]>,
92    }
93
94    // Phase 192.5 — `#[repr(align(16))]` can't take the `SLOT_ALIGN` const, so
95    // assert they stay in lockstep (the insert() guard compares against SLOT_ALIGN).
96    const _: () = assert!(mem::align_of::<Slot>() == SLOT_ALIGN);
97
98    unsafe impl Sync for Slot {}
99
100    impl Slot {
101        const fn new() -> Self {
102            Self {
103                bytes: UnsafeCell::new([0; SLOT_SIZE]),
104            }
105        }
106    }
107
108    // issue 0739 — declare the arithmetic so the pool inventory can price it.
109    // 0271 measured this pool at 8,192 bytes on an image that had never heard of
110    // the knob; `SLOT_SIZE` is the literal above, so the figure is derivable.
111    // nros-pool: SLOTS = NROS_RMW_SUBSCRIBER_SLOTS * 1024
112    static USED: [AtomicBool; SLOT_COUNT] = [const { AtomicBool::new(false) }; SLOT_COUNT];
113    static SLOTS: [Slot; SLOT_COUNT] = [const { Slot::new() }; SLOT_COUNT];
114
115    pub unsafe fn insert<T>(value: T) -> Option<*mut T> {
116        if mem::size_of::<T>() > SLOT_SIZE || mem::align_of::<T>() > SLOT_ALIGN {
117            return None;
118        }
119
120        for index in 0..SLOT_COUNT {
121            if USED[index]
122                .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
123                .is_err()
124            {
125                continue;
126            }
127
128            let ptr = SLOTS[index].bytes.get().cast::<T>();
129            unsafe { ptr.write(value) };
130            return Some(ptr);
131        }
132
133        None
134    }
135
136    pub unsafe fn take<T>(ptr: *mut T) -> bool {
137        if ptr.is_null() {
138            return false;
139        }
140
141        for index in 0..SLOT_COUNT {
142            let slot_ptr = SLOTS[index].bytes.get().cast::<T>();
143            if ptr != slot_ptr {
144                continue;
145            }
146
147            unsafe { ptr::drop_in_place(ptr) };
148            USED[index].store(false, Ordering::Release);
149            return true;
150        }
151
152        false
153    }
154}
155
156// ============================================================================
157// Trait alias bundle
158// ============================================================================
159//
160// `RustBackend` ties together every constraint the adapter needs. The
161// alternative — long `where` clauses on every fn — is unreadable. This
162// trait is sealed: backends don't impl it directly; the blanket impl
163// below picks it up automatically for any `R: Rmw` whose associated
164// types line up.
165
166/// Bundle of trait bounds an `Rmw` backend must satisfy to be exposed
167/// through [`RustBackendAdapter`]. Implemented automatically for any
168/// `R: Rmw` whose handle types use `TransportError` and are
169/// `'static`.
170pub trait RustBackend: Sized {
171    type Session: Session<
172            Error = TransportError,
173            PublisherHandle = Self::Publisher,
174            SubscriptionHandle = Self::Subscription,
175            ServiceHandle = Self::Service,
176            ClientHandle = Self::Client,
177        > + 'static;
178    type Publisher: Publisher<Error = TransportError> + 'static;
179    type Subscription: Subscription<Error = TransportError> + 'static;
180    type Service: ServiceTrait<Error = TransportError> + 'static;
181    type Client: ClientTrait<Error = TransportError> + 'static;
182
183    /// Construct a fresh factory instance. Called inside the `open`
184    /// trampoline. Equivalent to `R::default()` for backends that
185    /// `derive(Default)`; the indirection keeps the door open for
186    /// future per-backend registration knobs.
187    fn factory() -> Self;
188
189    /// Move the factory into a session per the `Rmw::open` contract.
190    fn open(self, config: &RmwConfig) -> Result<Self::Session, TransportError>;
191}
192
193impl<R> RustBackend for R
194where
195    R: Rmw<Error = TransportError> + Default + Sized,
196    R::Session: Session<Error = TransportError> + 'static,
197    <R::Session as Session>::PublisherHandle: Publisher<Error = TransportError> + 'static,
198    <R::Session as Session>::SubscriptionHandle: Subscription<Error = TransportError> + 'static,
199    <R::Session as Session>::ServiceHandle: ServiceTrait<Error = TransportError> + 'static,
200    <R::Session as Session>::ClientHandle: ClientTrait<Error = TransportError> + 'static,
201{
202    type Session = R::Session;
203    type Publisher = <R::Session as Session>::PublisherHandle;
204    type Subscription = <R::Session as Session>::SubscriptionHandle;
205    type Service = <R::Session as Session>::ServiceHandle;
206    type Client = <R::Session as Session>::ClientHandle;
207
208    fn factory() -> Self {
209        R::default()
210    }
211
212    fn open(self, config: &RmwConfig) -> Result<Self::Session, TransportError> {
213        Rmw::open(self, config)
214    }
215}
216
217// ============================================================================
218// Helpers: C-string <-> &str
219// ============================================================================
220
221/// Read a null-terminated C string into a Rust `&str`. Returns the
222/// empty string if `ptr` is null or contains invalid UTF-8.
223///
224/// # Safety
225///
226/// `ptr`, if non-null, must point to a valid null-terminated byte
227/// sequence that outlives the returned borrow.
228unsafe fn cstr_to_str<'a>(ptr: *const core::ffi::c_char) -> &'a str {
229    if ptr.is_null() {
230        return "";
231    }
232    let ptr = ptr.cast::<u8>();
233    let mut len = 0usize;
234    // Bound the scan so a missing terminator can't read off the end of
235    // a small caller buffer. 1 KiB matches the typical NAME_BUF_LEN
236    // (256) plus headroom for type-name / hash strings.
237    while len < 4096 && unsafe { *ptr.add(len) } != 0 {
238        len += 1;
239    }
240    let slice = unsafe { core::slice::from_raw_parts(ptr, len) };
241    core::str::from_utf8(slice).unwrap_or("")
242}
243
244/// Borrow a NUL-terminated C string as `&str`, distinguishing "absent" and
245/// "not text" from "empty" — which [`cstr_to_str`] deliberately does not.
246///
247/// `cstr_to_str` collapses NULL and invalid UTF-8 into `""` because its
248/// callers (node names, topic names) treat an unusable name as an absent one.
249/// A configuration property cannot: an empty key is not a key, and a value the
250/// backend cannot read must be reported, not passed on as `""`.
251unsafe fn cstr_to_str_strict<'a>(ptr: *const core::ffi::c_char) -> Option<&'a str> {
252    if ptr.is_null() {
253        return None;
254    }
255    let bytes = ptr.cast::<u8>();
256    let mut len = 0usize;
257    while len < 4096 && unsafe { *bytes.add(len) } != 0 {
258        len += 1;
259    }
260    if len == 0 {
261        return None;
262    }
263    let slice = unsafe { core::slice::from_raw_parts(bytes, len) };
264    core::str::from_utf8(slice).ok()
265}
266
267/// Copy `options->properties` into `out`, returning how many entries were
268/// written, or the `NrosRmwRet` to fail the session with.
269///
270/// # Safety
271/// `options`, when non-NULL, must point at a valid `rmw_session_options_t`
272/// whose `properties` array holds `property_count` valid entries.
273unsafe fn collect_session_properties<'a>(
274    options: *const NrosRmwSessionOptions,
275    out: &mut [(&'a str, &'a str); MAX_SESSION_PROPERTIES],
276) -> Result<usize, NrosRmwRet> {
277    if options.is_null() {
278        return Ok(0);
279    }
280    let opts = unsafe { &*options };
281    let count = opts.property_count;
282    if count == 0 {
283        return Ok(0);
284    }
285    if opts.properties.is_null() || count > MAX_SESSION_PROPERTIES {
286        return Err(NROS_RMW_RET_INVALID_ARGUMENT);
287    }
288    for (i, slot) in out.iter_mut().enumerate().take(count) {
289        let entry = unsafe { &*opts.properties.add(i) };
290        let (Some(key), Some(value)) = (unsafe { cstr_to_str_strict(entry.key) }, unsafe {
291            cstr_to_str_strict(entry.value)
292        }) else {
293            return Err(NROS_RMW_RET_INVALID_ARGUMENT);
294        };
295        *slot = (key, value);
296    }
297    Ok(count)
298}
299
300/// Convert the cffi QoS view back into a `nros_rmw::QoSProfile`. The
301/// adapter trampolines call this when forwarding `create_publisher` /
302/// `create_subscription` into the Rust trait.
303fn qos_from_cffi(q: &NrosRmwQos) -> QoSProfile {
304    use crate::generated;
305    use nros_rmw::{
306        QoSDurabilityPolicy, QoSHistoryPolicy, QoSLivelinessPolicy, QoSReliabilityPolicy,
307    };
308    // Phase 376 W5/B2 — these were `== 0` tests against a dense 0/1 encoding.
309    // Under upstream's numbering 0 is SYSTEM_DEFAULT for every policy, so each
310    // test now has three cases, not two. Written as a match on the named
311    // constants so an unrecognised value is visible rather than folded into
312    // one arm.
313    //
314    // issue 0829 — SYSTEM_DEFAULT now RAISES to the sentinel variant instead
315    // of being folded to the ROS default here. This function is the inbound
316    // edge of a Rust backend, and resolving an absence is the BACKEND's job:
317    // folding at the edge made every Rust backend answer the sentinel
318    // identically, which is exactly what upstream does not do. UNKNOWN keeps
319    // its old fold — it is a read-back artefact ("the backend could not
320    // determine this"), not a request, so it has no sentinel meaning to carry.
321    QoSProfile {
322        reliability: match q.reliability as i32 {
323            generated::NROS_RMW_RELIABILITY_SYSTEM_DEFAULT => QoSReliabilityPolicy::SystemDefault,
324            generated::NROS_RMW_RELIABILITY_BEST_EFFORT => QoSReliabilityPolicy::BestEffort,
325            generated::NROS_RMW_RELIABILITY_RELIABLE => QoSReliabilityPolicy::Reliable,
326            _ => QoSReliabilityPolicy::Reliable,
327        },
328        durability: match q.durability as i32 {
329            generated::NROS_RMW_DURABILITY_SYSTEM_DEFAULT => QoSDurabilityPolicy::SystemDefault,
330            generated::NROS_RMW_DURABILITY_TRANSIENT_LOCAL => QoSDurabilityPolicy::TransientLocal,
331            _ => QoSDurabilityPolicy::Volatile,
332        },
333        history: match q.history as i32 {
334            generated::NROS_RMW_HISTORY_SYSTEM_DEFAULT => QoSHistoryPolicy::SystemDefault,
335            generated::NROS_RMW_HISTORY_KEEP_ALL => QoSHistoryPolicy::KeepAll,
336            _ => QoSHistoryPolicy::KeepLast,
337        },
338        // The depth sentinel is a VALUE (0), so it needs no raising — it
339        // already reads as `DEPTH_SYSTEM_DEFAULT` on the far side.
340        depth: q.depth as u32,
341        // phase-301 (issue 0240): the express hint left the QoS struct; the
342        // create trampolines thread it via `TopicInfo` from the options param.
343        tx_express: false,
344        deadline_ms: q.deadline_ms,
345        lifespan_ms: q.lifespan_ms,
346        liveliness_kind: match q.liveliness_kind as u32 {
347            generated::rmw_liveliness_kind_t::NROS_RMW_LIVELINESS_AUTOMATIC => {
348                QoSLivelinessPolicy::Automatic
349            }
350            generated::rmw_liveliness_kind_t::NROS_RMW_LIVELINESS_MANUAL_BY_NODE => {
351                QoSLivelinessPolicy::ManualByNode
352            }
353            generated::rmw_liveliness_kind_t::NROS_RMW_LIVELINESS_MANUAL_BY_TOPIC => {
354                QoSLivelinessPolicy::ManualByTopic
355            }
356            _ => QoSLivelinessPolicy::None,
357        },
358        liveliness_lease_ms: q.liveliness_lease_ms,
359        avoid_ros_namespace_conventions: q.avoid_ros_namespace_conventions != 0,
360    }
361}
362
363// Phase 376 W5/B1 — `session_node_name` and `session_namespace` are GONE with
364// the `entity_view` fabrication they served. They read the OWNING NODE's
365// identity off a session struct the shim rewrote per call, which is how node
366// identity reached a backend before the node itself did.
367
368/// The node's own identity.
369unsafe fn node_name_of<'a>(node: *const NrosRmwNode) -> Option<&'a str> {
370    let name = unsafe { cstr_to_str((*node).name) };
371    if name.is_empty() { None } else { Some(name) }
372}
373
374unsafe fn node_namespace_of<'a>(node: *const NrosRmwNode) -> &'a str {
375    let namespace = unsafe { cstr_to_str((*node).namespace_) };
376    if namespace.is_empty() { "/" } else { namespace }
377}
378
379// ============================================================================
380// Adapter
381// ============================================================================
382
383/// Longest serialization-format name this seam can hand across the C ABI.
384///
385/// RFC-0088 D2 — the vtable slot carries the format's cross-image identity
386/// STRING, and a Rust backend spells that identity as a `&'static str`
387/// (`Session::SERIALIZATION_FORMAT`), which is not NUL-terminated. The
388/// conversion therefore has to happen somewhere, and it happens HERE, at
389/// compile time, into per-`R` static storage — never at call time into a
390/// buffer whose lifetime the slot's `const char *` contract cannot express.
391///
392/// 32 is generous for a name like `"cdr"`; a longer one is a const-eval error
393/// rather than a truncation, because a truncated format name is a different,
394/// plausible format.
395const FORMAT_NAME_CAP: usize = 32;
396
397/// NUL-terminate a format name at compile time.
398const fn format_name_cstr(name: &str) -> [u8; FORMAT_NAME_CAP] {
399    let bytes = name.as_bytes();
400    assert!(
401        bytes.len() < FORMAT_NAME_CAP,
402        "serialization format name does not fit the vtable slot's buffer"
403    );
404    let mut out = [0u8; FORMAT_NAME_CAP];
405    let mut i = 0;
406    while i < bytes.len() {
407        out[i] = bytes[i];
408        i += 1;
409    }
410    out
411}
412
413/// `get_serialization_format` — RFC-0088 D4.
414///
415/// Takes no arguments, exactly as upstream's `rmw_get_serialization_format()`
416/// does, because the answer is a property of the BACKEND and not of any entity
417/// it created. What differs from upstream is who holds the question: upstream
418/// has one middleware per process and can answer from a global, while
419/// `nros_rmw_cffi_register_named` admits several backends in one image, so the
420/// answer is per-vtable — which is per-session, since a session is opened
421/// against one vtable.
422unsafe extern "C" fn get_serialization_format_trampoline<R: RustBackend>() -> *const c_char {
423    RustBackendAdapter::<R>::SERIALIZATION_FORMAT_CSTR
424        .as_ptr()
425        .cast()
426}
427
428/// Wraps a Rust `Rmw` backend behind the canonical
429/// [`NrosRmwVtable`] C ABI. See module docs.
430pub struct RustBackendAdapter<R>(PhantomData<R>);
431
432impl<R: RustBackend> RustBackendAdapter<R> {
433    /// RFC-0088 D4 — `R`'s serialization format, NUL-terminated, in per-`R`
434    /// static storage so the `get_serialization_format` slot can hand out a
435    /// `const char *` that outlives the call.
436    ///
437    /// `&<const expr>` in a const initialiser is promoted to `'static`, which
438    /// is what makes the slot's lifetime contract true rather than merely
439    /// hoped for.
440    const SERIALIZATION_FORMAT_CSTR: &'static [u8; FORMAT_NAME_CAP] =
441        &format_name_cstr(<R::Session as Session>::SERIALIZATION_FORMAT);
442
443    /// Monomorphised vtable for backend `R`. The `const` is promoted
444    /// to per-type static storage, so `&Self::VTABLE` has `'static`
445    /// lifetime — safe to hand to `nros_rmw_cffi_register`.
446    pub const VTABLE: NrosRmwVtable = NrosRmwVtable {
447        create_session: Some(create_session_trampoline::<R>),
448        destroy_session: Some(destroy_session_trampoline::<R>),
449        drive_io: Some(drive_io_trampoline::<R>),
450        create_publisher: Some(create_publisher_trampoline::<R>),
451        destroy_publisher: Some(destroy_publisher_trampoline::<R>),
452        publish: Some(publish_trampoline::<R>),
453        create_subscription: Some(create_subscription_trampoline::<R>),
454        destroy_subscription: Some(destroy_subscription_trampoline::<R>),
455        take: Some(take_trampoline::<R>),
456        has_data: Some(has_data_trampoline::<R>),
457        create_service: Some(create_service_trampoline::<R>),
458        destroy_service: Some(destroy_service_trampoline::<R>),
459        take_request: Some(take_request_trampoline::<R>),
460        has_request: Some(has_request_trampoline::<R>),
461        send_response: Some(send_response_trampoline::<R>),
462        create_client: Some(create_client_trampoline::<R>),
463        destroy_client: Some(destroy_client_trampoline::<R>),
464        // Phase-301 (issue 0240) — `send_request_raw` + `take_response_raw`
465        // is the one request/reply path; the blocking `call_raw` slot is gone.
466        send_request: Some(send_request_trampoline::<R>),
467        take_response: Some(take_response_trampoline::<R>),
468        subscription_event_init: Some(subscription_event_init_trampoline::<R>),
469        publisher_event_init: Some(publisher_event_init_trampoline::<R>),
470        publisher_assert_liveliness: Some(publisher_assert_liveliness_trampoline::<R>),
471        next_deadline_ms: Some(next_deadline_ms_trampoline::<R>),
472        set_wake_callback: Some(set_wake_callback_trampoline::<R>),
473        // Phase 124.A — zero-copy slots default to NULL on the
474        // generic adapter; per-backend opt-in via dedicated trampolines
475        // (see `nros-rmw-zenoh` for the first implementation in 124.A.4).
476        // Runtime falls back to the arena path when these are NULL.
477        service_server_is_available: Some(service_server_is_available_trampoline::<R>),
478        take_sequence: Some(take_sequence_trampoline::<R>),
479        publish_streamed: Some(publish_streamed_trampoline::<R>),
480        ping_session: Some(ping_session_trampoline::<R>),
481        subscription_supports_in_place: Some(subscription_supports_in_place_trampoline::<R>),
482        process_raw_in_place: Some(process_raw_in_place_trampoline::<R>),
483        // phase-381 W3 — graph enumeration. The trait default is
484        // `Unsupported`, so a backend with no graph (XRCE) answers that rather
485        // than an empty one: "cannot tell you" and "nothing is there" are
486        // different answers and W6 keeps them distinguishable.
487        get_node_names: Some(get_node_names_trampoline::<R>),
488        count_publishers: Some(count_publishers_trampoline::<R>),
489        count_subscribers: Some(count_subscribers_trampoline::<R>),
490        get_topic_names_and_types: Some(get_topic_names_and_types_trampoline::<R>),
491        get_service_names_and_types: Some(get_service_names_and_types_trampoline::<R>),
492        // phase-381 W3 — the last six. The trait defaults are `Unsupported`, so
493        // a backend with no graph still answers "cannot tell you" rather than
494        // an empty graph.
495        get_publisher_names_and_types_by_node: Some(
496            get_publisher_names_and_types_by_node_trampoline::<R>,
497        ),
498        get_subscriber_names_and_types_by_node: Some(
499            get_subscriber_names_and_types_by_node_trampoline::<R>,
500        ),
501        get_service_names_and_types_by_node: Some(
502            get_service_names_and_types_by_node_trampoline::<R>,
503        ),
504        get_client_names_and_types_by_node: Some(
505            get_client_names_and_types_by_node_trampoline::<R>,
506        ),
507        get_publishers_info_by_topic: Some(get_publishers_info_by_topic_trampoline::<R>),
508        get_subscriptions_info_by_topic: Some(get_subscriptions_info_by_topic_trampoline::<R>),
509        // RFC-0088 D4 / phase-421 W2 — every Rust backend answers with its own
510        // `Session::SERIALIZATION_FORMAT`, so a bridge image can ask each
511        // session rather than trusting one image-wide constant. The trait
512        // default is `"cdr"`; a backend that speaks something else overrides
513        // it and this slot reports the override with no work here.
514        get_serialization_format: Some(get_serialization_format_trampoline::<R>),
515        ..EMPTY_VTABLE
516    };
517
518    /// Install the per-`R` vtable into the cffi registry under the
519    /// implicit name `"default"`. Idempotent — re-registering the
520    /// same vtable is a no-op from the runtime's perspective.
521    ///
522    /// Most backends should use [`register_named`](Self::register_named)
523    /// instead so they show up in the registry under their canonical
524    /// name (`"zenoh"`, `"dds"`, `"xrce"`, …). Phase 128.B.5 routes
525    /// the implicit-name path through `_register_named` too, so the
526    /// legacy unnamed C shim no longer participates in registration.
527    pub fn register() -> NrosRmwRet {
528        // SAFETY: `&Self::VTABLE` is a reference to a const-promoted
529        // static; address stable for the program's lifetime.
530        unsafe { crate::nros_rmw_cffi_register_named(c"default".as_ptr(), &Self::VTABLE) }
531    }
532
533    /// Phase 104.B.2 — install the per-`R` vtable under a stable
534    /// name. Multiple backends can coexist via this entry point.
535    ///
536    /// # Safety
537    /// `name` must be a valid NUL-terminated UTF-8 string.
538    pub unsafe fn register_named(name: *const core::ffi::c_char) -> NrosRmwRet {
539        // SAFETY: `&Self::VTABLE` is a reference to a const-promoted
540        // static; address stable for the program's lifetime.
541        unsafe { crate::nros_rmw_cffi_register_named(name, &Self::VTABLE) }
542    }
543}
544
545// ============================================================================
546// Trampolines — session lifecycle
547// ============================================================================
548
549unsafe extern "C" fn create_session_trampoline<R: RustBackend>(
550    locator: *const core::ffi::c_char,
551    mode: u8,
552    domain_id: u32,
553    node_name: *const core::ffi::c_char,
554    options: *const NrosRmwSessionOptions,
555    out: *mut NrosRmwSession,
556) -> NrosRmwRet {
557    if out.is_null() {
558        return NROS_RMW_RET_INVALID_ARGUMENT;
559    }
560    // phase-206 W3 — marshal the caller's session properties into the slice
561    // `RmwConfig` takes. This used to read `let _ = options;` with
562    // `properties: &[]` hard-coded, which made every backend-specific
563    // transport setting unreachable from C and C++: the Rust trait had carried
564    // `properties` since it existed, and only a hosted Rust caller building an
565    // `RmwConfig` by hand could fill it.
566    //
567    // Refused rather than truncated (each is INVALID_ARGUMENT):
568    //   - `property_count > 0` with a NULL `properties` pointer,
569    //   - more than `RMW_SESSION_MAX_PROPERTIES` entries,
570    //   - a NULL, empty, or non-UTF-8 key or value.
571    // Dropping any of these silently is the failure this work item exists to
572    // remove; a caller that cannot see its configuration was ignored debugs
573    // the transport instead of the typo.
574    let mut prop_buf: [(&str, &str); MAX_SESSION_PROPERTIES] = [("", ""); MAX_SESSION_PROPERTIES];
575    let prop_count = match unsafe { collect_session_properties(options, &mut prop_buf) } {
576        Ok(n) => n,
577        Err(ret) => return ret,
578    };
579    let cfg = RmwConfig {
580        locator: unsafe { cstr_to_str(locator) },
581        mode: if mode == 0 {
582            SessionMode::Client
583        } else {
584            SessionMode::Peer
585        },
586        domain_id,
587        node_name: unsafe { cstr_to_str(node_name) },
588        namespace: "",
589        properties: &prop_buf[..prop_count],
590    };
591    let factory = R::factory();
592    match factory.open(&cfg) {
593        Ok(session) => {
594            let boxed = Box::into_raw(Box::new(session));
595            unsafe {
596                (*out).backend_data = boxed as *mut c_void;
597            }
598            NROS_RMW_RET_OK
599        }
600        Err(e) => ret_from_error(&e),
601    }
602}
603
604unsafe extern "C" fn destroy_session_trampoline<R: RustBackend>(
605    session: *mut NrosRmwSession,
606) -> NrosRmwRet {
607    let Some(boxed) = (unsafe { take_box::<R::Session>(session_data_mut(session)) }) else {
608        return NROS_RMW_RET_INVALID_ARGUMENT;
609    };
610    let mut s = boxed;
611    let ret = match Session::close(&mut *s) {
612        Ok(()) => NROS_RMW_RET_OK,
613        Err(e) => ret_from_error(&e),
614    };
615    drop(s);
616    ret
617}
618
619unsafe extern "C" fn drive_io_trampoline<R: RustBackend>(
620    session: *mut NrosRmwSession,
621    timeout_ms: i32,
622) -> NrosRmwRet {
623    let Some(s) = (unsafe { session_mut::<R::Session>(session) }) else {
624        return NROS_RMW_RET_INVALID_ARGUMENT;
625    };
626    match Session::drive_io(s, timeout_ms) {
627        Ok(()) => NROS_RMW_RET_OK,
628        Err(e) => ret_from_error(&e),
629    }
630}
631
632// ============================================================================
633// Trampolines — publisher
634// ============================================================================
635
636unsafe extern "C" fn create_publisher_trampoline<R: RustBackend>(
637    node: *const NrosRmwNode,
638    // phase-406 W1 — the identity arrives as ONE argument, in upstream's
639    // position. Unpacked immediately below so the body is unchanged.
640    type_support: *const crate::generated::rmw_message_type_support_t,
641    topic_name: *const core::ffi::c_char,
642    domain_id: u32,
643    qos: *const NrosRmwQos,
644    options: *const rmw_publisher_options_t,
645    out: *mut NrosRmwPublisher,
646) -> NrosRmwRet {
647    // phase-406 W1 — unpack once, so the body below is untouched. A NULL
648    // `type_support` is INVALID_ARGUMENT rather than a silent empty type: the
649    // identity is what the backend keys its entity on, and an entity created
650    // with no type is a subscription that matches nothing and reports nothing.
651    let Some(ts) = (unsafe { type_support.as_ref() }) else {
652        return NROS_RMW_RET_INVALID_ARGUMENT;
653    };
654    let (type_name, type_hash) = (ts.type_name, ts.type_hash);
655    if out.is_null() || qos.is_null() {
656        return NROS_RMW_RET_INVALID_ARGUMENT;
657    }
658    // Phase 376 W5/B1 — the node carries both halves now: its own identity, and
659    // the route to its session (upstream's `context`). Reading the name off the
660    // session was the `entity_view` fabrication, and it is gone.
661    if node.is_null() {
662        return NROS_RMW_RET_INVALID_ARGUMENT;
663    }
664    let session = unsafe { (*node).session };
665    let Some(s) = (unsafe { session_mut::<R::Session>(session) }) else {
666        return NROS_RMW_RET_INVALID_ARGUMENT;
667    };
668    let node_name = unsafe { node_name_of(node) };
669    let namespace = unsafe { node_namespace_of(node) };
670    let topic = TopicInfo {
671        name: unsafe { cstr_to_str(topic_name) },
672        type_name: unsafe { cstr_to_str(type_name) },
673        type_hash: unsafe { cstr_to_str(type_hash) },
674        domain_id,
675        node_name,
676        namespace,
677        // Publisher side — the receive-buffer hint is subscription-only.
678        rx_buffer_hint: 0,
679        // phase-301 (issue 0240) — express hint from the NULLable options
680        // struct (NULL = default, not express).
681        tx_express: unsafe { options.as_ref() }.is_some_and(|o| o.tx_express != 0),
682    };
683    let qos_settings = qos_from_cffi(unsafe { &*qos });
684    match Session::create_publisher(s, &topic, qos_settings) {
685        Ok(pub_handle) => {
686            let boxed = Box::into_raw(Box::new(pub_handle));
687            unsafe {
688                (*out).backend_data = boxed as *mut c_void;
689            }
690            NROS_RMW_RET_OK
691        }
692        Err(e) => ret_from_error(&e),
693    }
694}
695
696unsafe extern "C" fn destroy_publisher_trampoline<R: RustBackend>(
697    publisher: *mut NrosRmwPublisher,
698) -> NrosRmwRet {
699    let _ = unsafe { take_box::<R::Publisher>(publisher_data_mut(publisher)) };
700
701    NROS_RMW_RET_OK
702}
703
704unsafe extern "C" fn publish_trampoline<R: RustBackend>(
705    publisher: *const NrosRmwPublisher,
706    // phase-406 W2 — by VALUE; unpacked so the body is unchanged.
707    payload: crate::generated::rmw_byte_span_t,
708) -> NrosRmwRet {
709    let (data, len) = (payload.data, payload.len);
710    let Some(p) = (unsafe { publisher_ref::<R::Publisher>(publisher) }) else {
711        return NROS_RMW_RET_INVALID_ARGUMENT;
712    };
713    if data.is_null() && len != 0 {
714        return NROS_RMW_RET_INVALID_ARGUMENT;
715    }
716    let slice = unsafe { core::slice::from_raw_parts(data, len) };
717    match Publisher::publish_raw(p, slice) {
718        Ok(()) => NROS_RMW_RET_OK,
719        Err(e) => ret_from_error(&e),
720    }
721}
722
723// ============================================================================
724// Trampolines — subscriber
725// ============================================================================
726
727unsafe extern "C" fn create_subscription_trampoline<R: RustBackend>(
728    node: *const NrosRmwNode,
729    // phase-406 W1 — the identity arrives as ONE argument, in upstream's
730    // position. Unpacked immediately below so the body is unchanged.
731    type_support: *const crate::generated::rmw_message_type_support_t,
732    topic_name: *const core::ffi::c_char,
733    domain_id: u32,
734    qos: *const NrosRmwQos,
735    options: *const rmw_subscription_options_t,
736    out: *mut NrosRmwSubscription,
737) -> NrosRmwRet {
738    // phase-406 W1 — unpack once, so the body below is untouched. A NULL
739    // `type_support` is INVALID_ARGUMENT rather than a silent empty type: the
740    // identity is what the backend keys its entity on, and an entity created
741    // with no type is a subscription that matches nothing and reports nothing.
742    let Some(ts) = (unsafe { type_support.as_ref() }) else {
743        return NROS_RMW_RET_INVALID_ARGUMENT;
744    };
745    let (type_name, type_hash) = (ts.type_name, ts.type_hash);
746    if out.is_null() || qos.is_null() {
747        return NROS_RMW_RET_INVALID_ARGUMENT;
748    }
749    // Phase 376 W5/B1 — the node carries both halves now: its own identity, and
750    // the route to its session (upstream's `context`). Reading the name off the
751    // session was the `entity_view` fabrication, and it is gone.
752    if node.is_null() {
753        return NROS_RMW_RET_INVALID_ARGUMENT;
754    }
755    let session = unsafe { (*node).session };
756    let Some(s) = (unsafe { session_mut::<R::Session>(session) }) else {
757        return NROS_RMW_RET_INVALID_ARGUMENT;
758    };
759    let node_name = unsafe { node_name_of(node) };
760    let namespace = unsafe { node_namespace_of(node) };
761    let topic = TopicInfo {
762        name: unsafe { cstr_to_str(topic_name) },
763        type_name: unsafe { cstr_to_str(type_name) },
764        type_hash: unsafe { cstr_to_str(type_hash) },
765        domain_id,
766        node_name,
767        namespace,
768        // Phase 231 (RFC-0038) / phase-301 (issue 0240) — receive-buffer size
769        // hint from the NULLable options struct (NULL = unset), so the backend
770        // can size-class its receive storage.
771        rx_buffer_hint: unsafe { options.as_ref() }.map_or(0, |o| o.rx_buffer_hint as usize),
772        // Subscription side — express is a publisher-only hint.
773        tx_express: false,
774    };
775    let qos_settings = qos_from_cffi(unsafe { &*qos });
776    match Session::create_subscription(s, &topic, qos_settings) {
777        Ok(sub_handle) => {
778            #[cfg(all(target_os = "none", not(feature = "std")))]
779            {
780                let Some(ptr) = (unsafe { static_subscriber_storage::insert(sub_handle) }) else {
781                    return crate::NROS_RMW_RET_BAD_ALLOC;
782                };
783                unsafe {
784                    (*out).backend_data = ptr as *mut c_void;
785                }
786                NROS_RMW_RET_OK
787            }
788            #[cfg(not(all(target_os = "none", not(feature = "std"))))]
789            {
790                let boxed = Box::into_raw(Box::new(sub_handle));
791                unsafe {
792                    (*out).backend_data = boxed as *mut c_void;
793                }
794                NROS_RMW_RET_OK
795            }
796        }
797        Err(e) => ret_from_error(&e),
798    }
799}
800
801unsafe extern "C" fn destroy_subscription_trampoline<R: RustBackend>(
802    subscriber: *mut NrosRmwSubscription,
803) -> NrosRmwRet {
804    let slot = unsafe { subscription_data_mut(subscriber) };
805    #[cfg(all(target_os = "none", not(feature = "std")))]
806    {
807        if unsafe {
808            static_subscriber_storage::take::<R::Subscription>(*slot as *mut R::Subscription)
809        } {
810            *slot = core::ptr::null_mut();
811            return NROS_RMW_RET_OK;
812        }
813    }
814    let _ = unsafe { take_box::<R::Subscription>(slot) };
815
816    NROS_RMW_RET_OK
817}
818
819unsafe extern "C" fn take_trampoline<R: RustBackend>(
820    subscriber: *const NrosRmwSubscription,
821    // phase-406 W2 — by POINTER; `capacity` in, `len` out.
822    out: *mut crate::generated::rmw_mut_byte_span_t,
823    taken: *mut bool,
824) -> NrosRmwRet {
825    let Some(span) = (unsafe { out.as_mut() }) else {
826        return NROS_RMW_RET_INVALID_ARGUMENT;
827    };
828    let (buf, buf_len) = (span.data, span.capacity);
829    let out_len: *mut usize = &mut span.len;
830    // Phase 376 W3.b/W3.d step A — upstream's `rmw_take` shape: status in the
831    // return, `taken` and the byte count in out-parameters. `Ok(None)` is no
832    // longer the NO_DATA sentinel.
833    if out_len.is_null() || taken.is_null() {
834        return NROS_RMW_RET_INVALID_ARGUMENT;
835    }
836    let Some(s) = (unsafe { subscription_mut::<R::Subscription>(subscriber) }) else {
837        return NROS_RMW_RET_INVALID_ARGUMENT;
838    };
839    if buf.is_null() && buf_len != 0 {
840        return NROS_RMW_RET_INVALID_ARGUMENT;
841    }
842    let slice = unsafe { core::slice::from_raw_parts_mut(buf, buf_len) };
843    let key = unsafe { (*subscriber).backend_data as usize };
844
845    // SAFETY (all four writes below): both pointers checked non-null above.
846    #[cfg(feature = "safety-e2e")]
847    if crate::take_cffi_integrity_request(key) {
848        return match Subscription::take_validated(s, slice) {
849            Ok(Some((n, status))) => {
850                crate::store_cffi_integrity_status(key, status);
851                unsafe {
852                    *out_len = n;
853                    *taken = true;
854                }
855                NROS_RMW_RET_OK
856            }
857            Ok(None) => {
858                unsafe { *taken = false };
859                NROS_RMW_RET_OK
860            }
861            Err(e) => ret_from_error(&e),
862        };
863    }
864
865    match Subscription::take_serialized_with_info(s, slice) {
866        Ok(Some((n, info))) => {
867            crate::store_cffi_message_info(key, info);
868            unsafe {
869                *out_len = n;
870                *taken = true;
871            }
872            NROS_RMW_RET_OK
873        }
874        Ok(None) => {
875            unsafe { *taken = false };
876            NROS_RMW_RET_OK
877        }
878        Err(e) => ret_from_error(&e),
879    }
880}
881
882unsafe extern "C" fn has_data_trampoline<R: RustBackend>(
883    subscription: *mut NrosRmwSubscription,
884    out_has_data: *mut bool,
885) -> NrosRmwRet {
886    // Phase 376 W3.d step A — the flag moves to an out-parameter so the return
887    // carries only a status. Written on OK only.
888    if out_has_data.is_null() {
889        return NROS_RMW_RET_INVALID_ARGUMENT;
890    }
891    // `subscription_ref`, not `_mut`: the probe is logically read-only and the
892    // header says a backend must not mutate state here.
893    let Some(s) = (unsafe { subscription_ref::<R::Subscription>(subscription) }) else {
894        return NROS_RMW_RET_INVALID_ARGUMENT;
895    };
896    // SAFETY: checked non-null above.
897    unsafe { *out_has_data = Subscription::has_data(s) };
898    NROS_RMW_RET_OK
899}
900
901// Phase 231 (RFC-0038) — in-place subscription take across the C ABI.
902
903unsafe extern "C" fn subscription_supports_in_place_trampoline<R: RustBackend>(
904    subscriber: *mut NrosRmwSubscription,
905    out_supports: *mut bool,
906) -> NrosRmwRet {
907    // Phase 376 W3.d step A — capability out, status returned.
908    if out_supports.is_null() {
909        return NROS_RMW_RET_INVALID_ARGUMENT;
910    }
911    let Some(s) = (unsafe { subscription_ref::<R::Subscription>(subscriber) }) else {
912        return NROS_RMW_RET_INVALID_ARGUMENT;
913    };
914    // SAFETY: checked non-null above.
915    unsafe { *out_supports = Subscription::supports_process_in_place(s) };
916    NROS_RMW_RET_OK
917}
918
919unsafe extern "C" fn process_raw_in_place_trampoline<R: RustBackend>(
920    subscriber: *mut NrosRmwSubscription,
921    ctx: *mut core::ffi::c_void,
922    cb: Option<
923        unsafe extern "C" fn(
924            ctx: *mut core::ffi::c_void,
925            message: crate::generated::rmw_byte_span_t,
926        ),
927    >,
928    out_processed: *mut bool,
929) -> NrosRmwRet {
930    // Phase 376 W3.d step A — "did it process one" is the out-parameter, and
931    // `Ok(false)` is no longer reported as the NO_DATA sentinel.
932    if out_processed.is_null() {
933        return NROS_RMW_RET_INVALID_ARGUMENT;
934    }
935    let Some(s) = (unsafe { subscription_mut::<R::Subscription>(subscriber) }) else {
936        return NROS_RMW_RET_INVALID_ARGUMENT;
937    };
938    let Some(cb) = cb else {
939        return NROS_RMW_RET_INVALID_ARGUMENT;
940    };
941    match Subscription::process_raw_in_place(s, |raw| unsafe {
942        cb(
943            ctx,
944            crate::generated::rmw_byte_span_t {
945                data: raw.as_ptr(),
946                len: raw.len(),
947            },
948        )
949    }) {
950        Ok(processed) => {
951            // SAFETY: checked non-null above.
952            unsafe { *out_processed = processed };
953            NROS_RMW_RET_OK
954        }
955        Err(e) => ret_from_error(&e),
956    }
957}
958
959// ============================================================================
960// Trampolines — service server
961// ============================================================================
962
963unsafe extern "C" fn create_service_trampoline<R: RustBackend>(
964    node: *const NrosRmwNode,
965    // phase-406 W1 — the identity arrives as ONE argument, in upstream's
966    // position. Unpacked immediately below so the body is unchanged.
967    type_support: *const crate::generated::rmw_service_type_support_t,
968    service_name: *const core::ffi::c_char,
969    domain_id: u32,
970    qos: *const NrosRmwQos,
971    out: *mut NrosRmwService,
972) -> NrosRmwRet {
973    // phase-406 W1 — unpack once, so the body below is untouched. A NULL
974    // `type_support` is INVALID_ARGUMENT rather than a silent empty type: the
975    // identity is what the backend keys its entity on, and an entity created
976    // with no type is a subscription that matches nothing and reports nothing.
977    let Some(ts) = (unsafe { type_support.as_ref() }) else {
978        return NROS_RMW_RET_INVALID_ARGUMENT;
979    };
980    let (type_name, type_hash) = (ts.type_name, ts.type_hash);
981    if out.is_null() || qos.is_null() {
982        return NROS_RMW_RET_INVALID_ARGUMENT;
983    }
984    // Phase 376 W5/B1 — the node carries both halves now: its own identity, and
985    // the route to its session (upstream's `context`). Reading the name off the
986    // session was the `entity_view` fabrication, and it is gone.
987    if node.is_null() {
988        return NROS_RMW_RET_INVALID_ARGUMENT;
989    }
990    let session = unsafe { (*node).session };
991    let Some(s) = (unsafe { session_mut::<R::Session>(session) }) else {
992        return NROS_RMW_RET_INVALID_ARGUMENT;
993    };
994    let node_name = unsafe { node_name_of(node) };
995    let namespace = unsafe { node_namespace_of(node) };
996    let info = nros_rmw::ServiceInfo {
997        name: unsafe { cstr_to_str(service_name) },
998        type_name: unsafe { cstr_to_str(type_name) },
999        type_hash: unsafe { cstr_to_str(type_hash) },
1000        domain_id,
1001        node_name,
1002        namespace,
1003    };
1004    let qos_settings = qos_from_cffi(unsafe { &*qos });
1005    match Session::create_service(s, &info, qos_settings) {
1006        Ok(server) => {
1007            let boxed = Box::into_raw(Box::new(server));
1008            unsafe {
1009                (*out).backend_data = boxed as *mut c_void;
1010            }
1011            NROS_RMW_RET_OK
1012        }
1013        Err(e) => ret_from_error(&e),
1014    }
1015}
1016
1017unsafe extern "C" fn destroy_service_trampoline<R: RustBackend>(
1018    server: *mut NrosRmwService,
1019) -> NrosRmwRet {
1020    let _ = unsafe { take_box::<R::Service>(service_data_mut(server)) };
1021
1022    NROS_RMW_RET_OK
1023}
1024
1025unsafe extern "C" fn take_request_trampoline<R: RustBackend>(
1026    server: *const NrosRmwService,
1027    request: *mut crate::generated::rmw_mut_byte_span_t,
1028    seq_out: *mut i64,
1029    taken: *mut bool,
1030) -> NrosRmwRet {
1031    // Phase 376 W3.b/W3.d step A — upstream `rmw_take_request`'s shape;
1032    // phase-406 W2 — the byte range is one span, and `written` lands in it.
1033    if request.is_null() || taken.is_null() {
1034        return NROS_RMW_RET_INVALID_ARGUMENT;
1035    }
1036    let Some(s) = (unsafe { service_mut::<R::Service>(server) }) else {
1037        return NROS_RMW_RET_INVALID_ARGUMENT;
1038    };
1039    let (buf, buf_len) = unsafe { ((*request).data, (*request).capacity) };
1040    let out_len = unsafe { &raw mut (*request).len };
1041    if (buf.is_null() && buf_len != 0) || seq_out.is_null() {
1042        return NROS_RMW_RET_INVALID_ARGUMENT;
1043    }
1044    let slice = unsafe { core::slice::from_raw_parts_mut(buf, buf_len) };
1045    let buf_start = slice.as_ptr() as usize;
1046    match ServiceTrait::take_request(s, slice) {
1047        Ok(Some(req)) => {
1048            // The handle's `data` slice borrows from `buf`. Use its
1049            // offset within the caller's buffer to compute the payload
1050            // length; some backends prepend an envelope header so the
1051            // payload doesn't start at offset 0.
1052            let offset = (req.data.as_ptr() as usize).saturating_sub(buf_start);
1053            let len = req.data.len();
1054            unsafe {
1055                *seq_out = req.sequence_number;
1056            }
1057            // Move the payload to the start of the buffer so the C
1058            // caller sees a `(buf, len)` pair starting at offset 0,
1059            // matching the cyclonedds backend's contract.
1060            if offset != 0 {
1061                let total = offset + len;
1062                // SAFETY: `slice[..total]` is initialised by the
1063                // backend; copy_within respects overlapping ranges.
1064                slice.copy_within(offset..total, 0);
1065            }
1066            // SAFETY: both checked non-null above.
1067            unsafe {
1068                *out_len = len;
1069                *taken = true;
1070            }
1071            NROS_RMW_RET_OK
1072        }
1073        Ok(None) => {
1074            // SAFETY: checked non-null above.
1075            unsafe { *taken = false };
1076            NROS_RMW_RET_OK
1077        }
1078        Err(e) => ret_from_error(&e),
1079    }
1080}
1081
1082unsafe extern "C" fn has_request_trampoline<R: RustBackend>(
1083    server: *mut NrosRmwService,
1084    out_has_request: *mut bool,
1085) -> NrosRmwRet {
1086    // Phase 376 W3.d step A — see `has_data_trampoline`.
1087    if out_has_request.is_null() {
1088        return NROS_RMW_RET_INVALID_ARGUMENT;
1089    }
1090    let Some(s) = (unsafe { service_ref::<R::Service>(server) }) else {
1091        return NROS_RMW_RET_INVALID_ARGUMENT;
1092    };
1093    // SAFETY: checked non-null above.
1094    unsafe { *out_has_request = ServiceTrait::has_request(s) };
1095    NROS_RMW_RET_OK
1096}
1097
1098unsafe extern "C" fn send_response_trampoline<R: RustBackend>(
1099    server: *const NrosRmwService,
1100    seq: i64,
1101    response: crate::generated::rmw_byte_span_t,
1102) -> NrosRmwRet {
1103    let Some(s) = (unsafe { service_mut::<R::Service>(server) }) else {
1104        return NROS_RMW_RET_INVALID_ARGUMENT;
1105    };
1106    let (data, len) = (response.data, response.len);
1107    if data.is_null() && len != 0 {
1108        return NROS_RMW_RET_INVALID_ARGUMENT;
1109    }
1110    let slice = unsafe { core::slice::from_raw_parts(data, len) };
1111    match ServiceTrait::send_response(s, seq, slice) {
1112        Ok(()) => NROS_RMW_RET_OK,
1113        Err(e) => ret_from_error(&e),
1114    }
1115}
1116
1117// ============================================================================
1118// Trampolines — service client
1119// ============================================================================
1120
1121unsafe extern "C" fn create_client_trampoline<R: RustBackend>(
1122    node: *const NrosRmwNode,
1123    // phase-406 W1 — the identity arrives as ONE argument, in upstream's
1124    // position. Unpacked immediately below so the body is unchanged.
1125    type_support: *const crate::generated::rmw_service_type_support_t,
1126    service_name: *const core::ffi::c_char,
1127    domain_id: u32,
1128    qos: *const NrosRmwQos,
1129    out: *mut NrosRmwClient,
1130) -> NrosRmwRet {
1131    // phase-406 W1 — unpack once, so the body below is untouched. A NULL
1132    // `type_support` is INVALID_ARGUMENT rather than a silent empty type: the
1133    // identity is what the backend keys its entity on, and an entity created
1134    // with no type is a subscription that matches nothing and reports nothing.
1135    let Some(ts) = (unsafe { type_support.as_ref() }) else {
1136        return NROS_RMW_RET_INVALID_ARGUMENT;
1137    };
1138    let (type_name, type_hash) = (ts.type_name, ts.type_hash);
1139    if out.is_null() || qos.is_null() {
1140        return NROS_RMW_RET_INVALID_ARGUMENT;
1141    }
1142    // Phase 376 W5/B1 — the node carries both halves now: its own identity, and
1143    // the route to its session (upstream's `context`). Reading the name off the
1144    // session was the `entity_view` fabrication, and it is gone.
1145    if node.is_null() {
1146        return NROS_RMW_RET_INVALID_ARGUMENT;
1147    }
1148    let session = unsafe { (*node).session };
1149    let Some(s) = (unsafe { session_mut::<R::Session>(session) }) else {
1150        return NROS_RMW_RET_INVALID_ARGUMENT;
1151    };
1152    let node_name = unsafe { node_name_of(node) };
1153    let namespace = unsafe { node_namespace_of(node) };
1154    let info = nros_rmw::ServiceInfo {
1155        name: unsafe { cstr_to_str(service_name) },
1156        type_name: unsafe { cstr_to_str(type_name) },
1157        type_hash: unsafe { cstr_to_str(type_hash) },
1158        domain_id,
1159        node_name,
1160        namespace,
1161    };
1162    let qos_settings = qos_from_cffi(unsafe { &*qos });
1163    match Session::create_client(s, &info, qos_settings) {
1164        Ok(client) => {
1165            let boxed = Box::into_raw(Box::new(client));
1166            unsafe {
1167                (*out).backend_data = boxed as *mut c_void;
1168            }
1169            NROS_RMW_RET_OK
1170        }
1171        Err(e) => ret_from_error(&e),
1172    }
1173}
1174
1175unsafe extern "C" fn destroy_client_trampoline<R: RustBackend>(
1176    client: *mut NrosRmwClient,
1177) -> NrosRmwRet {
1178    let _ = unsafe { take_box::<R::Client>(client_data_mut(client)) };
1179
1180    NROS_RMW_RET_OK
1181}
1182
1183// Non-blocking send/recv trampolines — the one request/reply path
1184// (phase-301: the blocking `call_raw` slot is deleted).
1185unsafe extern "C" fn send_request_trampoline<R: RustBackend>(
1186    client: *const NrosRmwClient,
1187    request: crate::generated::rmw_byte_span_t,
1188    sequence_id: *mut i64,
1189) -> NrosRmwRet {
1190    let Some(c) = (unsafe { client_mut::<R::Client>(client) }) else {
1191        return NROS_RMW_RET_INVALID_ARGUMENT;
1192    };
1193    let (request, req_len) = (request.data, request.len);
1194    if request.is_null() && req_len != 0 {
1195        return NROS_RMW_RET_INVALID_ARGUMENT;
1196    }
1197    let req = unsafe { core::slice::from_raw_parts(request, req_len) };
1198    match ClientTrait::send_request_raw(c, req) {
1199        Ok(seq) => {
1200            // Issue 0778 — hand the id back. NULL is tolerated for a caller
1201            // that genuinely has one call in flight and does not want it.
1202            if !sequence_id.is_null() {
1203                // SAFETY: checked non-null.
1204                unsafe { *sequence_id = seq };
1205            }
1206            NROS_RMW_RET_OK
1207        }
1208        Err(e) => ret_from_error(&e),
1209    }
1210}
1211
1212unsafe extern "C" fn take_response_trampoline<R: RustBackend>(
1213    client: *const NrosRmwClient,
1214    reply: *mut crate::generated::rmw_mut_byte_span_t,
1215    seq_out: *mut i64,
1216    taken: *mut bool,
1217) -> NrosRmwRet {
1218    // Phase 376 W3.b/W3.d step A — upstream `rmw_take_response`'s shape;
1219    // phase-406 W2 — one span in place of the buf/len/out_len triple.
1220    if reply.is_null() || taken.is_null() {
1221        return NROS_RMW_RET_INVALID_ARGUMENT;
1222    }
1223    let Some(c) = (unsafe { client_mut::<R::Client>(client) }) else {
1224        return NROS_RMW_RET_INVALID_ARGUMENT;
1225    };
1226    let (reply_buf, reply_buf_len) = unsafe { ((*reply).data, (*reply).capacity) };
1227    let out_len = unsafe { &raw mut (*reply).len };
1228    if reply_buf.is_null() && reply_buf_len != 0 {
1229        return NROS_RMW_RET_INVALID_ARGUMENT;
1230    }
1231    let reply = unsafe { core::slice::from_raw_parts_mut(reply_buf, reply_buf_len) };
1232    match ClientTrait::take_response_raw(c, reply) {
1233        Ok(Some((n, seq))) => {
1234            // SAFETY: both checked non-null above.
1235            unsafe {
1236                *out_len = n;
1237                *taken = true;
1238                if !seq_out.is_null() {
1239                    *seq_out = seq;
1240                }
1241            }
1242            NROS_RMW_RET_OK
1243        }
1244        Ok(None) => {
1245            // SAFETY: checked non-null above.
1246            unsafe { *taken = false };
1247            NROS_RMW_RET_OK
1248        }
1249        Err(e) => ret_from_error(&e),
1250    }
1251}
1252
1253// ============================================================================
1254// Event-callback bridge (Phase 115.L.0.events)
1255// ============================================================================
1256//
1257// Events / liveliness / deadline are wired through the vtable (see the
1258// `register_subscription_event` / `register_publisher_event` /
1259// `assert_publisher_liveliness` / `next_deadline_ms` trampolines registered
1260// above) — the stale "TODO: wire through" header was removed (Phase 192.9).
1261//
1262// `NrosRmwEventCallback` (cffi shape) and `nros_rmw::EventCallback`
1263// (trait shape) have *layout-identical* arguments:
1264//
1265//   * NrosRmwEventKind  ↔ EventKind     — both `#[repr(u8)]`, same variant ids.
1266//   * *const NrosRmwEventPayload ↔ *const c_void  — fn-ptr-width pointer; the
1267//     trait callback dereferences as `*const LivelinessChangedStatus` /
1268//     `*const CountStatus`, the cffi callback dereferences as the matching
1269//     union member. The field-by-field layout matches today (see the
1270//     `_event_payload_layout_match` const-asserts below).
1271//   * *mut c_void       ↔ *mut c_void   — identical.
1272//
1273// Therefore the cffi callback can be transmuted into a trait callback
1274// pointer; the receiving Rust trait code calls it via the trait
1275// signature, but the bytes on the wire are the cffi struct.
1276
1277const _: () = {
1278    use core::mem::{align_of, size_of};
1279    assert!(
1280        size_of::<crate::NrosRmwLivelinessChangedStatus>()
1281            == size_of::<nros_rmw::LivelinessChangedStatus>()
1282    );
1283    assert!(
1284        align_of::<crate::NrosRmwLivelinessChangedStatus>()
1285            == align_of::<nros_rmw::LivelinessChangedStatus>()
1286    );
1287    assert!(size_of::<crate::NrosRmwCountStatus>() == size_of::<nros_rmw::CountStatus>());
1288    assert!(align_of::<crate::NrosRmwCountStatus>() == align_of::<nros_rmw::CountStatus>());
1289    // EventKind tags must round-trip 0..=4 between the generated C
1290    // discriminants and the `#[repr(u8)]` trait enum.
1291    use crate::rmw_event_type_t as ck;
1292    assert!(
1293        ck::NROS_RMW_EVENT_LIVELINESS_CHANGED == nros_rmw::EventKind::LivelinessChanged as ck::Type
1294    );
1295    assert!(
1296        ck::NROS_RMW_EVENT_REQUESTED_DEADLINE_MISSED
1297            == nros_rmw::EventKind::RequestedDeadlineMissed as ck::Type
1298    );
1299    assert!(ck::NROS_RMW_EVENT_MESSAGE_LOST == nros_rmw::EventKind::MessageLost as ck::Type);
1300    assert!(ck::NROS_RMW_EVENT_LIVELINESS_LOST == nros_rmw::EventKind::LivelinessLost as ck::Type);
1301    assert!(
1302        ck::NROS_RMW_EVENT_OFFERED_DEADLINE_MISSED
1303            == nros_rmw::EventKind::OfferedDeadlineMissed as ck::Type
1304    );
1305};
1306
1307unsafe extern "C" fn subscription_event_init_trampoline<R: RustBackend>(
1308    subscriber: *const NrosRmwSubscription,
1309    kind: NrosRmwEventKind,
1310    deadline_ms: u32,
1311    cb: NrosRmwEventCallback,
1312    user_context: *mut c_void,
1313) -> NrosRmwRet {
1314    let Some(s) = (unsafe { subscription_mut::<R::Subscription>(subscriber) }) else {
1315        return NROS_RMW_RET_INVALID_ARGUMENT;
1316    };
1317    // Explicit conversion, not a transmute: `NrosRmwEventKind` is
1318    // C-uint-sized (#238) while trait `EventKind` is `#[repr(u8)]` —
1319    // reinterpreting bytes would be UB.
1320    let trait_kind: nros_rmw::EventKind = event_kind_from_c(kind);
1321    // The generated callback type is nullable; a NULL callback can't be
1322    // forwarded into the trait's non-null callback surface.
1323    let Some(cb) = cb else {
1324        return NROS_RMW_RET_INVALID_ARGUMENT;
1325    };
1326    // SAFETY: see module-level note. `cb`'s parameter types
1327    // `(NrosRmwEventKind, *const NrosRmwEventPayload, *mut c_void)` are
1328    // ABI-compatible with the trait's `(EventKind, *const c_void, *mut c_void)`.
1329    let trait_cb: nros_rmw::EventCallback = unsafe { core::mem::transmute(cb) };
1330    let res = unsafe {
1331        Subscription::register_event_callback(s, trait_kind, deadline_ms, trait_cb, user_context)
1332    };
1333    match res {
1334        Ok(()) => NROS_RMW_RET_OK,
1335        Err(e) => {
1336            // Backend's `Unsupported` mapping for events is its
1337            // `serialization_error()` by trait-doc default; the cffi
1338            // contract for "this event kind unsupported" is its own
1339            // ret code, so map any error here to UNSUPPORTED rather
1340            // than e.g. SERIALIZATION_ERROR — the caller's mental
1341            // model is "vtable said no events," not "marshalling
1342            // failed."
1343            let _ = e;
1344            NROS_RMW_RET_UNSUPPORTED
1345        }
1346    }
1347}
1348
1349unsafe extern "C" fn publisher_event_init_trampoline<R: RustBackend>(
1350    publisher: *const NrosRmwPublisher,
1351    kind: NrosRmwEventKind,
1352    deadline_ms: u32,
1353    cb: NrosRmwEventCallback,
1354    user_context: *mut c_void,
1355) -> NrosRmwRet {
1356    // Publisher::register_event_callback takes `&mut self`. Need a
1357    // mut-ptr to the boxed handle.
1358    if publisher.is_null() {
1359        return NROS_RMW_RET_INVALID_ARGUMENT;
1360    }
1361    let p_ptr = unsafe { (*publisher).backend_data } as *mut R::Publisher;
1362    if p_ptr.is_null() {
1363        return NROS_RMW_RET_INVALID_ARGUMENT;
1364    }
1365    let p = unsafe { &mut *p_ptr };
1366    // Explicit conversion, not a transmute (see subscriber trampoline / #238).
1367    let trait_kind: nros_rmw::EventKind = event_kind_from_c(kind);
1368    let Some(cb) = cb else {
1369        return NROS_RMW_RET_INVALID_ARGUMENT;
1370    };
1371    let trait_cb: nros_rmw::EventCallback = unsafe { core::mem::transmute(cb) };
1372    let res = unsafe {
1373        Publisher::register_event_callback(p, trait_kind, deadline_ms, trait_cb, user_context)
1374    };
1375    match res {
1376        Ok(()) => NROS_RMW_RET_OK,
1377        Err(_) => NROS_RMW_RET_UNSUPPORTED,
1378    }
1379}
1380
1381unsafe extern "C" fn publisher_assert_liveliness_trampoline<R: RustBackend>(
1382    publisher: *const NrosRmwPublisher,
1383) -> NrosRmwRet {
1384    let Some(p) = (unsafe { publisher_ref::<R::Publisher>(publisher) }) else {
1385        return NROS_RMW_RET_INVALID_ARGUMENT;
1386    };
1387    match Publisher::assert_liveliness(p) {
1388        Ok(()) => NROS_RMW_RET_OK,
1389        Err(e) => ret_from_error(&e),
1390    }
1391}
1392
1393unsafe extern "C" fn next_deadline_ms_trampoline<R: RustBackend>(
1394    session: *const NrosRmwSession,
1395    out_ms: *mut u32,
1396    has_deadline: *mut bool,
1397) -> NrosRmwRet {
1398    // Phase 376 W3.d step A — value and presence out, status returned.
1399    if out_ms.is_null() || has_deadline.is_null() {
1400        return NROS_RMW_RET_INVALID_ARGUMENT;
1401    }
1402    // A NULL session was previously reported as `-1`, indistinguishable from a
1403    // link with nothing scheduled. It is an invalid argument.
1404    let Some(s) = (unsafe { session_ref::<R::Session>(session) }) else {
1405        return NROS_RMW_RET_INVALID_ARGUMENT;
1406    };
1407    match Session::next_deadline_ms(s) {
1408        Some(ms) => {
1409            // SAFETY: both checked non-null above.
1410            unsafe {
1411                *out_ms = ms;
1412                *has_deadline = true;
1413            }
1414        }
1415        None => {
1416            // SAFETY: checked non-null above.
1417            unsafe { *has_deadline = false };
1418        }
1419    }
1420    NROS_RMW_RET_OK
1421}
1422
1423unsafe extern "C" fn set_wake_callback_trampoline<R: RustBackend>(
1424    session: *mut NrosRmwSession,
1425    cb: Option<unsafe extern "C" fn(ctx: *mut core::ffi::c_void)>,
1426    ctx: *mut core::ffi::c_void,
1427) -> NrosRmwRet {
1428    let Some(s) = (unsafe { session_mut::<R::Session>(session) }) else {
1429        return NROS_RMW_RET_INVALID_ARGUMENT;
1430    };
1431    // Phase 124.B.1 — delegate to the Rust backend. Default trait
1432    // body ignores; concrete backends opt in.
1433    // SAFETY: this trampoline forwards the C ABI's callback/context
1434    // lifetime contract to the Rust backend.
1435    unsafe { Session::set_wake_callback(s, cb, ctx) };
1436    NROS_RMW_RET_OK
1437}
1438
1439unsafe extern "C" fn service_server_is_available_trampoline<R: RustBackend>(
1440    client: *const NrosRmwClient,
1441    out_available: *mut bool,
1442) -> NrosRmwRet {
1443    // Phase 124.C.1 — delegate to the Rust backend's
1444    // `ClientTrait::server_available` impl. Default trait
1445    // body returns `Err(TransportError::Unsupported)`; concrete
1446    // backends opt in by overriding.
1447    //
1448    // Phase 376 W3.d step A — status in the return, answer in the
1449    // out-parameter. `*out_available` is written ONLY on OK, so an
1450    // error leaves whatever the caller initialised it to; that is
1451    // upstream's contract and it is also what keeps a caller who
1452    // ignores the status from reading a stale `true` as fresh.
1453    if out_available.is_null() {
1454        return NROS_RMW_RET_INVALID_ARGUMENT;
1455    }
1456    let Some(c) = (unsafe { client_mut::<R::Client>(client) }) else {
1457        return NROS_RMW_RET_INVALID_ARGUMENT;
1458    };
1459    match ClientTrait::service_is_ready(c) {
1460        Ok(available) => {
1461            // SAFETY: checked non-null above; the caller owns a `bool`.
1462            unsafe { *out_available = available };
1463            NROS_RMW_RET_OK
1464        }
1465        Err(e) => ret_from_error(&e),
1466    }
1467}
1468
1469unsafe extern "C" fn ping_session_trampoline<R: RustBackend>(
1470    session: *mut NrosRmwSession,
1471    timeout_ms: i32,
1472) -> NrosRmwRet {
1473    // Phase 124.F.1 — delegate to the Rust backend's
1474    // `Session::ping_session` impl. Default trait body returns
1475    // `Err(TransportError::Unsupported)`; concrete backends opt in
1476    // by overriding.
1477    let Some(s) = (unsafe { session_mut::<R::Session>(session) }) else {
1478        return NROS_RMW_RET_INVALID_ARGUMENT;
1479    };
1480    match Session::ping_session(s, timeout_ms) {
1481        Ok(()) => NROS_RMW_RET_OK,
1482        Err(e) => ret_from_error(&e),
1483    }
1484}
1485
1486/// phase-381 W3 — bridge the C visitor to the Rust `Session::get_node_names`.
1487///
1488/// The C side hands a `rmw_node_visit_fn` plus an opaque `ctx`; the Rust trait
1489/// takes a closure. This wraps the former in the latter, and the strings it
1490/// passes are NUL-terminated on the stack because the C contract borrows them
1491/// for the call only — no allocation on either side of the seam.
1492///
1493/// A name that does not fit the stack buffer is SKIPPED rather than truncated:
1494/// a truncated node name is a different, plausible node.
1495unsafe extern "C" fn get_node_names_trampoline<R: RustBackend>(
1496    session: *const NrosRmwSession,
1497    // phase-406 W2 — the pair as one argument. The NODE visitor: its callback
1498    // takes an enclave, which the names-and-types one does not.
1499    visitor: crate::generated::rmw_node_visitor_t,
1500) -> NrosRmwRet {
1501    let (visit, ctx) = (visitor.visit, visitor.ctx);
1502    let Some(visit) = visit else {
1503        return NROS_RMW_RET_INVALID_ARGUMENT;
1504    };
1505    let Some(s) = (unsafe { session_mut::<R::Session>(session.cast_mut()) }) else {
1506        return NROS_RMW_RET_INVALID_ARGUMENT;
1507    };
1508
1509    /// Longest node name / namespace this seam will pass through.
1510    const NAME_MAX: usize = 256;
1511
1512    let mut cb = |name: &str, namespace: &str, enclave: Option<&str>| -> bool {
1513        let mut name_buf = [0u8; NAME_MAX];
1514        let mut ns_buf = [0u8; NAME_MAX];
1515        let mut enc_buf = [0u8; NAME_MAX];
1516        // `< NAME_MAX`, not `<= `: the buffer must hold the bytes AND the NUL
1517        // the C side reads to. Spelled as the strict compare clippy wants
1518        // (`int_plus_one`) rather than the `len + 1 <= cap` that says it.
1519        if name.len() >= NAME_MAX || namespace.len() >= NAME_MAX {
1520            return true; // skip this one, keep enumerating
1521        }
1522        name_buf[..name.len()].copy_from_slice(name.as_bytes());
1523        ns_buf[..namespace.len()].copy_from_slice(namespace.as_bytes());
1524        let enc_ptr = match enclave {
1525            Some(e) if e.len() < NAME_MAX => {
1526                enc_buf[..e.len()].copy_from_slice(e.as_bytes());
1527                enc_buf.as_ptr() as *const c_char
1528            }
1529            // NULL is the contract's "this backend does not track one", which
1530            // is what lets one slot answer both `rmw_get_node_names` and
1531            // `rmw_get_node_names_with_enclaves`.
1532            _ => core::ptr::null(),
1533        };
1534        unsafe {
1535            visit(
1536                ctx,
1537                name_buf.as_ptr() as *const c_char,
1538                ns_buf.as_ptr() as *const c_char,
1539                enc_ptr,
1540            )
1541        }
1542    };
1543
1544    match Session::get_node_names(s, &mut cb) {
1545        Ok(()) => NROS_RMW_RET_OK,
1546        Err(e) => ret_from_error(&e),
1547    }
1548}
1549
1550/// phase-381 W3 — `count_publishers` / `count_subscribers` share one body.
1551macro_rules! count_trampoline {
1552    ($name:ident, $method:ident) => {
1553        unsafe extern "C" fn $name<R: RustBackend>(
1554            session: *const NrosRmwSession,
1555            topic_name: *const c_char,
1556            count: *mut usize,
1557        ) -> NrosRmwRet {
1558            if topic_name.is_null() || count.is_null() {
1559                return NROS_RMW_RET_INVALID_ARGUMENT;
1560            }
1561            let Some(s) = (unsafe { session_mut::<R::Session>(session.cast_mut()) }) else {
1562                return NROS_RMW_RET_INVALID_ARGUMENT;
1563            };
1564            let Ok(topic) = (unsafe { core::ffi::CStr::from_ptr(topic_name) }).to_str() else {
1565                return NROS_RMW_RET_INVALID_ARGUMENT;
1566            };
1567            match Session::$method(s, topic) {
1568                Ok(n) => {
1569                    unsafe { *count = n };
1570                    NROS_RMW_RET_OK
1571                }
1572                Err(e) => ret_from_error(&e),
1573            }
1574        }
1575    };
1576}
1577
1578count_trampoline!(count_publishers_trampoline, count_publishers);
1579count_trampoline!(count_subscribers_trampoline, count_subscribers);
1580
1581/// phase-381 W3 — `get_topic_names_and_types` / `get_service_names_and_types`.
1582///
1583/// The C visitor takes `(name, types[], types_count)`. Rust hands over
1584/// `&[&str]`, so each call builds a bounded array of NUL-terminated pointers on
1585/// the stack — the strings are borrowed for the call only, which is the
1586/// contract on both sides.
1587///
1588/// A name or type that does not fit is SKIPPED, never truncated: a truncated
1589/// type name is a different, plausible type.
1590macro_rules! names_and_types_trampoline {
1591    // The topic form carries `no_demangle`; the service form does not — that
1592    // asymmetry is upstream's, mirrored rather than smoothed over.
1593    ($name:ident, $method:ident, demangle_flag) => {
1594        unsafe extern "C" fn $name<R: RustBackend>(
1595            session: *const NrosRmwSession,
1596            no_demangle: bool,
1597            // phase-406 W2 — the pair as one argument.
1598            visitor: crate::generated::rmw_names_and_types_visitor_t,
1599        ) -> NrosRmwRet {
1600            // `no_demangle` asks for the WIRE spelling. This backend reports ROS
1601            // names, so honouring it would mean re-mangling what was just
1602            // demangled; refused rather than silently ignored, because ignoring
1603            // it answers a different question than the caller asked.
1604            if no_demangle {
1605                return NROS_RMW_RET_UNSUPPORTED;
1606            }
1607            $crate::names_and_types_body!(R, $method, session, visitor)
1608        }
1609    };
1610    ($name:ident, $method:ident) => {
1611        unsafe extern "C" fn $name<R: RustBackend>(
1612            session: *const NrosRmwSession,
1613            // phase-406 W2 — the pair as one argument.
1614            visitor: crate::generated::rmw_names_and_types_visitor_t,
1615        ) -> NrosRmwRet {
1616            $crate::names_and_types_body!(R, $method, session, visitor)
1617        }
1618    };
1619}
1620
1621/// The shared body, so the two arms above differ only by the parameter list.
1622#[macro_export]
1623#[doc(hidden)]
1624macro_rules! names_and_types_body {
1625    ($R:ident, $method:ident, $session:ident, $visitor:ident) => {{
1626        // phase-406 W2 — unpack HERE: a `let` in the caller's expansion is not
1627        // visible in this one (macro hygiene), so the struct crosses instead.
1628        let (visit_fn, ctx) = ($visitor.visit, $visitor.ctx);
1629        let Some(visit) = visit_fn else {
1630            return NROS_RMW_RET_INVALID_ARGUMENT;
1631        };
1632        let Some(s) = (unsafe { session_mut::<$R::Session>($session.cast_mut()) }) else {
1633            return NROS_RMW_RET_INVALID_ARGUMENT;
1634        };
1635
1636        const NAME_MAX: usize = 256;
1637        const TYPES_MAX: usize = 8;
1638
1639        let mut cb = |name: &str, types: &[&str]| -> bool {
1640            let mut name_buf = [0u8; NAME_MAX];
1641            if name.len() >= NAME_MAX {
1642                return true; // skip, keep enumerating
1643            }
1644            name_buf[..name.len()].copy_from_slice(name.as_bytes());
1645
1646            let mut type_bufs = [[0u8; NAME_MAX]; TYPES_MAX];
1647            let mut ptrs: [*const c_char; TYPES_MAX] = [core::ptr::null(); TYPES_MAX];
1648            let mut n = 0usize;
1649            for t in types.iter().take(TYPES_MAX) {
1650                if t.len() >= NAME_MAX {
1651                    continue;
1652                }
1653                type_bufs[n][..t.len()].copy_from_slice(t.as_bytes());
1654                ptrs[n] = type_bufs[n].as_ptr() as *const c_char;
1655                n += 1;
1656            }
1657            unsafe { visit(ctx, name_buf.as_ptr() as *const c_char, ptrs.as_ptr(), n) }
1658        };
1659
1660        match Session::$method(s, &mut cb) {
1661            Ok(()) => NROS_RMW_RET_OK,
1662            Err(e) => ret_from_error(&e),
1663        }
1664    }};
1665}
1666
1667names_and_types_trampoline!(
1668    get_topic_names_and_types_trampoline,
1669    get_topic_names_and_types,
1670    demangle_flag
1671);
1672names_and_types_trampoline!(
1673    get_service_names_and_types_trampoline,
1674    get_service_names_and_types
1675);
1676
1677/// phase-381 W3 — the four `*_by_node` slots, one body.
1678///
1679/// Longhand callers, shared implementation: they differ only by which
1680/// `GraphEntityKind` they pass, and four copies of this marshalling is four
1681/// chances for one of them to drift.
1682///
1683/// # Safety
1684/// Same contract as the slots that call it.
1685unsafe fn by_node_impl<R: RustBackend>(
1686    session: *const NrosRmwSession,
1687    node_name: *const c_char,
1688    node_namespace: *const c_char,
1689    no_demangle: bool,
1690    // phase-406 W2 — the pair as one argument.
1691    visitor: crate::generated::rmw_names_and_types_visitor_t,
1692    kind: nros_rmw::GraphEntityKind,
1693) -> NrosRmwRet {
1694    let (visit, ctx) = (visitor.visit, visitor.ctx);
1695    // `no_demangle` asks for the WIRE spelling; we report ROS names, so
1696    // honouring it would mean re-mangling what was just demangled. Refused
1697    // rather than ignored — ignoring answers a different question than asked.
1698    if no_demangle {
1699        return NROS_RMW_RET_UNSUPPORTED;
1700    }
1701    if node_name.is_null() || node_namespace.is_null() {
1702        return NROS_RMW_RET_INVALID_ARGUMENT;
1703    }
1704    let Some(visit) = visit else {
1705        return NROS_RMW_RET_INVALID_ARGUMENT;
1706    };
1707    let Some(s) = (unsafe { session_mut::<R::Session>(session.cast_mut()) }) else {
1708        return NROS_RMW_RET_INVALID_ARGUMENT;
1709    };
1710    let (Ok(name), Ok(ns)) = (
1711        unsafe { core::ffi::CStr::from_ptr(node_name) }.to_str(),
1712        unsafe { core::ffi::CStr::from_ptr(node_namespace) }.to_str(),
1713    ) else {
1714        return NROS_RMW_RET_INVALID_ARGUMENT;
1715    };
1716
1717    const NAME_MAX: usize = 256;
1718    const TYPES_MAX: usize = 8;
1719    let mut cb = |n: &str, types: &[&str]| -> bool {
1720        let mut name_buf = [0u8; NAME_MAX];
1721        if n.len() >= NAME_MAX {
1722            return true;
1723        }
1724        name_buf[..n.len()].copy_from_slice(n.as_bytes());
1725        let mut type_bufs = [[0u8; NAME_MAX]; TYPES_MAX];
1726        let mut ptrs: [*const c_char; TYPES_MAX] = [core::ptr::null(); TYPES_MAX];
1727        let mut count = 0usize;
1728        for t in types.iter().take(TYPES_MAX) {
1729            if t.len() >= NAME_MAX {
1730                continue;
1731            }
1732            type_bufs[count][..t.len()].copy_from_slice(t.as_bytes());
1733            ptrs[count] = type_bufs[count].as_ptr() as *const c_char;
1734            count += 1;
1735        }
1736        unsafe {
1737            visit(
1738                ctx,
1739                name_buf.as_ptr() as *const c_char,
1740                ptrs.as_ptr(),
1741                count,
1742            )
1743        }
1744    };
1745    match Session::get_names_and_types_by_node(s, kind, name, ns, &mut cb) {
1746        Ok(()) => NROS_RMW_RET_OK,
1747        Err(e) => ret_from_error(&e),
1748    }
1749}
1750
1751macro_rules! by_node_trampoline {
1752    // Upstream gives `no_demangle` to the publisher and subscriber forms and
1753    // NOT to the service and client ones. Mirrored rather than smoothed over —
1754    // RFC-0054 makes these headers the ABI SSoT, so an asymmetry upstream has
1755    // is one we have.
1756    ($name:ident, $kind:ident, no_demangle) => {
1757        unsafe extern "C" fn $name<R: RustBackend>(
1758            session: *const NrosRmwSession,
1759            node_name: *const c_char,
1760            node_namespace: *const c_char,
1761            no_demangle: bool,
1762            // phase-406 W2 — the pair as one argument.
1763            visitor: crate::generated::rmw_names_and_types_visitor_t,
1764        ) -> NrosRmwRet {
1765            unsafe {
1766                by_node_impl::<R>(
1767                    session,
1768                    node_name,
1769                    node_namespace,
1770                    no_demangle,
1771                    visitor,
1772                    nros_rmw::GraphEntityKind::$kind,
1773                )
1774            }
1775        }
1776    };
1777    // The service and client forms, which upstream gives no `no_demangle`.
1778    ($name:ident, $kind:ident) => {
1779        unsafe extern "C" fn $name<R: RustBackend>(
1780            session: *const NrosRmwSession,
1781            node_name: *const c_char,
1782            node_namespace: *const c_char,
1783            // phase-406 W2 — the pair as one argument.
1784            visitor: crate::generated::rmw_names_and_types_visitor_t,
1785        ) -> NrosRmwRet {
1786            unsafe {
1787                by_node_impl::<R>(
1788                    session,
1789                    node_name,
1790                    node_namespace,
1791                    false,
1792                    visitor,
1793                    nros_rmw::GraphEntityKind::$kind,
1794                )
1795            }
1796        }
1797    };
1798}
1799
1800by_node_trampoline!(
1801    get_publisher_names_and_types_by_node_trampoline,
1802    Publisher,
1803    no_demangle
1804);
1805by_node_trampoline!(
1806    get_subscriber_names_and_types_by_node_trampoline,
1807    Subscriber,
1808    no_demangle
1809);
1810by_node_trampoline!(get_service_names_and_types_by_node_trampoline, Service);
1811by_node_trampoline!(get_client_names_and_types_by_node_trampoline, Client);
1812
1813/// phase-381 W3 — the two `*_info_by_topic` slots, one body.
1814///
1815/// # Safety
1816/// Same contract as the slots that call it.
1817unsafe fn endpoint_info_impl<R: RustBackend>(
1818    session: *const NrosRmwSession,
1819    topic_name: *const c_char,
1820    no_mangle: bool,
1821    // phase-406 W2 — the pair as one argument.
1822    visitor: crate::generated::rmw_topic_endpoint_info_visitor_t,
1823    publishers: bool,
1824) -> NrosRmwRet {
1825    let (visit, ctx) = (visitor.visit, visitor.ctx);
1826    if no_mangle {
1827        return NROS_RMW_RET_UNSUPPORTED;
1828    }
1829    if topic_name.is_null() {
1830        return NROS_RMW_RET_INVALID_ARGUMENT;
1831    }
1832    let Some(visit) = visit else {
1833        return NROS_RMW_RET_INVALID_ARGUMENT;
1834    };
1835    let Some(s) = (unsafe { session_mut::<R::Session>(session.cast_mut()) }) else {
1836        return NROS_RMW_RET_INVALID_ARGUMENT;
1837    };
1838    let Ok(topic) = (unsafe { core::ffi::CStr::from_ptr(topic_name) }).to_str() else {
1839        return NROS_RMW_RET_INVALID_ARGUMENT;
1840    };
1841
1842    const NAME_MAX: usize = 256;
1843    let mut cb = |info: &nros_rmw::GraphEndpointInfo<'_>| -> bool {
1844        let mut name_buf = [0u8; NAME_MAX];
1845        let mut ns_buf = [0u8; NAME_MAX];
1846        let mut ty_buf = [0u8; NAME_MAX];
1847        if info.node_name.len() >= NAME_MAX
1848            || info.node_namespace.len() >= NAME_MAX
1849            || info.topic_type.len() >= NAME_MAX
1850        {
1851            return true; // skip, never truncate
1852        }
1853        name_buf[..info.node_name.len()].copy_from_slice(info.node_name.as_bytes());
1854        ns_buf[..info.node_namespace.len()].copy_from_slice(info.node_namespace.as_bytes());
1855        ty_buf[..info.topic_type.len()].copy_from_slice(info.topic_type.as_bytes());
1856        // The struct has no "qos known" flag, by design: the ABI expresses an
1857        // unreadable policy with the `*_UNKNOWN` sentinels and the contract on
1858        // `publisher_get_actual_qos` spells it out — write UNKNOWN for what you
1859        // cannot determine and return OK; `UNSUPPORTED` means no read-back AT
1860        // ALL, not "I know some of it".
1861        //
1862        // zenoh's liveliness token DOES carry a QoS chunk, but decoding it is
1863        // deferred rather than guessed: it is the DECLARING side's own profile
1864        // and this seam promises a GRANTED one, so shipping it now would be the
1865        // plausible wrong answer this slot exists to avoid. `GraphEndpointInfo::qos`
1866        // is `None` from every backend today, so this is always the UNKNOWN arm.
1867        let mut qos: rmw_qos_profile_t = unsafe { core::mem::zeroed() };
1868        qos.reliability = NROS_RMW_RELIABILITY_UNKNOWN as u8;
1869        qos.durability = NROS_RMW_DURABILITY_UNKNOWN as u8;
1870        qos.history = NROS_RMW_HISTORY_UNKNOWN as u8;
1871        qos.liveliness_kind = rmw_liveliness_kind_t::NROS_RMW_LIVELINESS_UNKNOWN as u8;
1872        let c_info = rmw_topic_endpoint_info_t {
1873            node_name: name_buf.as_ptr() as *const c_char,
1874            node_namespace: ns_buf.as_ptr() as *const c_char,
1875            topic_type: ty_buf.as_ptr() as *const c_char,
1876            endpoint_type: if info.is_publisher {
1877                rmw_endpoint_type_t::RMW_ENDPOINT_PUBLISHER
1878            } else {
1879                rmw_endpoint_type_t::RMW_ENDPOINT_SUBSCRIPTION
1880            },
1881            endpoint_gid: rmw_gid_t {
1882                implementation_identifier: core::ptr::null(),
1883                data: info.endpoint_gid,
1884            },
1885            qos_profile: qos,
1886        };
1887        unsafe { visit(ctx, &c_info as *const rmw_topic_endpoint_info_t) }
1888    };
1889    match Session::get_endpoint_info_by_topic(s, publishers, topic, &mut cb) {
1890        Ok(()) => NROS_RMW_RET_OK,
1891        Err(e) => ret_from_error(&e),
1892    }
1893}
1894
1895macro_rules! endpoint_info_trampoline {
1896    ($name:ident, $publishers:expr) => {
1897        unsafe extern "C" fn $name<R: RustBackend>(
1898            session: *const NrosRmwSession,
1899            topic_name: *const c_char,
1900            no_mangle: bool,
1901            // phase-406 W2 — the pair as one argument.
1902            visitor: crate::generated::rmw_topic_endpoint_info_visitor_t,
1903        ) -> NrosRmwRet {
1904            unsafe { endpoint_info_impl::<R>(session, topic_name, no_mangle, visitor, $publishers) }
1905        }
1906    };
1907}
1908
1909endpoint_info_trampoline!(get_publishers_info_by_topic_trampoline, true);
1910endpoint_info_trampoline!(get_subscriptions_info_by_topic_trampoline, false);
1911
1912unsafe extern "C" fn publish_streamed_trampoline<R: RustBackend>(
1913    publisher: *mut NrosRmwPublisher,
1914    size_cb: Option<unsafe extern "C" fn(out_total_len: *mut usize, user_ctx: *mut c_void)>,
1915    chunk_cb: Option<
1916        unsafe extern "C" fn(
1917            out_buf: *mut u8,
1918            cap: usize,
1919            out_written: *mut usize,
1920            user_ctx: *mut c_void,
1921        ),
1922    >,
1923    user_ctx: *mut c_void,
1924) -> NrosRmwRet {
1925    // Phase 124.E.1 — delegate to the Rust backend's
1926    // `Publisher::publish_streamed` impl. Default trait body fires
1927    // the staging-buffer fallback (124.E.2); concrete backends opt
1928    // in by overriding for true streamed publish into the network
1929    // buffer.
1930    let Some(p) = (unsafe { publisher_ref::<R::Publisher>(publisher) }) else {
1931        return NROS_RMW_RET_INVALID_ARGUMENT;
1932    };
1933    // Generated ABI marks the callbacks nullable; the trait surface is not.
1934    let (Some(size_cb), Some(chunk_cb)) = (size_cb, chunk_cb) else {
1935        return NROS_RMW_RET_INVALID_ARGUMENT;
1936    };
1937    // SAFETY: this trampoline is entered from the C ABI with the same
1938    // callback/user_ctx lifetime contract required by `Publisher::publish_streamed`.
1939    match unsafe { Publisher::publish_streamed(p, size_cb, chunk_cb, user_ctx) } {
1940        Ok(()) => NROS_RMW_RET_OK,
1941        Err(e) => ret_from_error(&e),
1942    }
1943}
1944
1945unsafe extern "C" fn take_sequence_trampoline<R: RustBackend>(
1946    subscriber: *const NrosRmwSubscription,
1947    buf: *mut u8,
1948    per_msg_cap: usize,
1949    max_msgs: usize,
1950    out_lens: *mut usize,
1951    taken: *mut usize,
1952) -> NrosRmwRet {
1953    // Phase 376 W3.b/W3.d step A — the COUNT is an out-parameter, matching
1954    // upstream's `size_t *taken`.
1955    if taken.is_null() {
1956        return NROS_RMW_RET_INVALID_ARGUMENT;
1957    }
1958    // Phase 124.D.1 — delegate to the Rust backend's
1959    // `Subscription::take_sequence` impl. Default trait body
1960    // loop-drives `take_serialized`; concrete backends opt in by
1961    // overriding for a native batch take.
1962    let Some(s) = (unsafe { subscription_mut::<R::Subscription>(subscriber) }) else {
1963        return NROS_RMW_RET_INVALID_ARGUMENT;
1964    };
1965    if buf.is_null() || out_lens.is_null() || per_msg_cap == 0 {
1966        return NROS_RMW_RET_INVALID_ARGUMENT;
1967    }
1968    // SAFETY: caller pinky-promised a contiguous block of
1969    // `max_msgs * per_msg_cap` bytes at `buf` and at least
1970    // `max_msgs` `usize` slots at `out_lens`. The buffer is
1971    // exclusively borrowed for the duration of this call.
1972    let buf_slice =
1973        unsafe { core::slice::from_raw_parts_mut(buf, max_msgs.saturating_mul(per_msg_cap)) };
1974    let lens_slice = unsafe { core::slice::from_raw_parts_mut(out_lens, max_msgs) };
1975    match Subscription::take_sequence(s, buf_slice, per_msg_cap, max_msgs, lens_slice) {
1976        Ok(count) => {
1977            // SAFETY: checked non-null above.
1978            unsafe { *taken = count };
1979            NROS_RMW_RET_OK
1980        }
1981        Err(e) => ret_from_error(&e),
1982    }
1983}
1984
1985// ============================================================================
1986// Pointer plumbing — keep one place where each entity's
1987// `backend_data` is dereferenced.
1988// ============================================================================
1989//
1990// Validity contract:
1991//
1992//   - The C-side wrappers (`CffiSession`, `CffiPublisher`, …) hand
1993//     each trampoline an entity-struct pointer whose `backend_data`
1994//     was last written by *this* adapter's create-trampoline. That
1995//     write is `Box::into_raw(Box::new(handle))` of `R::Session` /
1996//     `R::Publisher` / … so the pointer aliases a valid Box<T>.
1997//   - `_mut` variants give `&mut T`; the runtime serialises access
1998//     per the executor's single-thread invariant.
1999//   - `take_box` reclaims ownership; subsequent calls see a null
2000//     `backend_data` and return `INVALID_ARGUMENT`.
2001
2002#[inline]
2003unsafe fn session_data_mut(session: *mut NrosRmwSession) -> &'static mut *mut c_void {
2004    unsafe { &mut (*session).backend_data }
2005}
2006
2007#[inline]
2008unsafe fn publisher_data_mut(publisher: *mut NrosRmwPublisher) -> &'static mut *mut c_void {
2009    unsafe { &mut (*publisher).backend_data }
2010}
2011
2012#[inline]
2013unsafe fn subscription_data_mut(subscriber: *mut NrosRmwSubscription) -> &'static mut *mut c_void {
2014    unsafe { &mut (*subscriber).backend_data }
2015}
2016
2017#[inline]
2018unsafe fn service_data_mut(server: *mut NrosRmwService) -> &'static mut *mut c_void {
2019    unsafe { &mut (*server).backend_data }
2020}
2021
2022#[inline]
2023unsafe fn client_data_mut(client: *mut NrosRmwClient) -> &'static mut *mut c_void {
2024    unsafe { &mut (*client).backend_data }
2025}
2026
2027#[inline]
2028unsafe fn session_mut<'a, T>(session: *mut NrosRmwSession) -> Option<&'a mut T> {
2029    if session.is_null() {
2030        return None;
2031    }
2032    let p = unsafe { (*session).backend_data } as *mut T;
2033    if p.is_null() {
2034        None
2035    } else {
2036        Some(unsafe { &mut *p })
2037    }
2038}
2039
2040#[inline]
2041unsafe fn session_ref<'a, T>(session: *const NrosRmwSession) -> Option<&'a T> {
2042    if session.is_null() {
2043        return None;
2044    }
2045    let p = unsafe { (*session).backend_data } as *const T;
2046    if p.is_null() {
2047        None
2048    } else {
2049        Some(unsafe { &*p })
2050    }
2051}
2052
2053#[inline]
2054unsafe fn publisher_ref<'a, T>(publisher: *const NrosRmwPublisher) -> Option<&'a T> {
2055    if publisher.is_null() {
2056        return None;
2057    }
2058    let p = unsafe { (*publisher).backend_data } as *const T;
2059    if p.is_null() {
2060        None
2061    } else {
2062        Some(unsafe { &*p })
2063    }
2064}
2065
2066#[inline]
2067unsafe fn subscription_mut<'a, T>(subscriber: *const NrosRmwSubscription) -> Option<&'a mut T> {
2068    if subscriber.is_null() {
2069        return None;
2070    }
2071    let p = unsafe { (*subscriber).backend_data } as *mut T;
2072    if p.is_null() {
2073        None
2074    } else {
2075        Some(unsafe { &mut *p })
2076    }
2077}
2078
2079#[inline]
2080unsafe fn subscription_ref<'a, T>(subscriber: *const NrosRmwSubscription) -> Option<&'a T> {
2081    if subscriber.is_null() {
2082        return None;
2083    }
2084    let p = unsafe { (*subscriber).backend_data } as *const T;
2085    if p.is_null() {
2086        None
2087    } else {
2088        Some(unsafe { &*p })
2089    }
2090}
2091
2092#[inline]
2093unsafe fn service_mut<'a, T>(server: *const NrosRmwService) -> Option<&'a mut T> {
2094    if server.is_null() {
2095        return None;
2096    }
2097    let p = unsafe { (*server).backend_data } as *mut T;
2098    if p.is_null() {
2099        None
2100    } else {
2101        Some(unsafe { &mut *p })
2102    }
2103}
2104
2105#[inline]
2106unsafe fn service_ref<'a, T>(server: *const NrosRmwService) -> Option<&'a T> {
2107    if server.is_null() {
2108        return None;
2109    }
2110    let p = unsafe { (*server).backend_data } as *const T;
2111    if p.is_null() {
2112        None
2113    } else {
2114        Some(unsafe { &*p })
2115    }
2116}
2117
2118#[inline]
2119unsafe fn client_mut<'a, T>(client: *const NrosRmwClient) -> Option<&'a mut T> {
2120    if client.is_null() {
2121        return None;
2122    }
2123    let p = unsafe { (*client).backend_data } as *mut T;
2124    if p.is_null() {
2125        None
2126    } else {
2127        Some(unsafe { &mut *p })
2128    }
2129}
2130
2131#[inline]
2132unsafe fn take_box<T>(slot: &mut *mut c_void) -> Option<Box<T>> {
2133    if slot.is_null() {
2134        return None;
2135    }
2136    let ptr = *slot as *mut T;
2137    *slot = core::ptr::null_mut();
2138    if ptr.is_null() {
2139        None
2140    } else {
2141        // SAFETY: this pointer was minted by `Box::into_raw` in the
2142        // corresponding create-trampoline; we are taking ownership
2143        // back and the slot has been cleared so no second take is
2144        // possible.
2145        Some(unsafe { Box::from_raw(ptr) })
2146    }
2147}
2148
2149#[cfg(test)]
2150mod tests {
2151    use super::*;
2152
2153    /// issue 0829 — the inbound edge RAISES a 0 policy back to the sentinel, so
2154    /// a C caller's "unstated" survives to the Rust backend that has to resolve
2155    /// it. Before this, `qos_from_cffi` folded 0 to RELIABLE / VOLATILE /
2156    /// KEEP_LAST here at the edge, which made every Rust backend answer the
2157    /// sentinel identically — the thing upstream deliberately does not do.
2158    ///
2159    /// This is the round trip: profile -> C struct -> profile.
2160    #[test]
2161    fn a_zero_filled_c_profile_reads_back_as_the_sentinel() {
2162        let raised = qos_from_cffi(&crate::NROS_RMW_QOS_PROFILE_SYSTEM_DEFAULT);
2163        assert_eq!(raised, nros_rmw::QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT);
2164        assert!(raised.has_unresolved_system_default());
2165    }
2166
2167    /// UNKNOWN is NOT the sentinel and must keep its old fold: it is a
2168    /// read-back artefact ("the backend could not determine this"), not a
2169    /// request, so there is no absence to carry.
2170    #[test]
2171    fn unknown_is_not_raised_to_the_sentinel() {
2172        let mut q = crate::NROS_RMW_QOS_PROFILE_SYSTEM_DEFAULT;
2173        q.reliability = crate::generated::NROS_RMW_RELIABILITY_UNKNOWN as u8;
2174        let raised = qos_from_cffi(&q);
2175        assert_eq!(
2176            raised.reliability,
2177            nros_rmw::QoSReliabilityPolicy::Reliable,
2178            "UNKNOWN must keep folding to the ROS default, not become a sentinel"
2179        );
2180    }
2181}