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: boxed (fallibly, via `box_handle` — issue 1551) and 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_BAD_ALLOC, NROS_RMW_RET_INVALID_ARGUMENT,
54    NROS_RMW_RET_OK, NROS_RMW_RET_UNSUPPORTED, NrosRmwClient, NrosRmwEventCallback,
55    NrosRmwEventKind, NrosRmwGid, NrosRmwNode, NrosRmwPublisher, NrosRmwQos, NrosRmwRet,
56    NrosRmwService, NrosRmwSession, NrosRmwSessionOptions, NrosRmwSubscription, NrosRmwVtable,
57    entity_wake::{EntityWakeCb, Hosted},
58    event_kind_from_c, ret_from_error, rmw_publisher_options_t, rmw_subscription_options_t,
59};
60// phase-381 W3 — the endpoint-info slots marshal the generated ABI types
61// directly. Imported from `generated` rather than re-exported through the crate
62// root: these are bindgen output and the root deliberately re-exports only the
63// hand-maintained surface.
64use crate::generated::{
65    NROS_RMW_DURABILITY_UNKNOWN, NROS_RMW_HISTORY_UNKNOWN, NROS_RMW_RELIABILITY_UNKNOWN,
66    rmw_endpoint_type_t, rmw_gid_t, rmw_liveliness_kind_t, rmw_qos_profile_t,
67    rmw_topic_endpoint_info_t,
68};
69
70#[cfg(all(target_os = "none", not(feature = "std")))]
71mod static_subscriber_storage {
72    use core::{cell::UnsafeCell, mem, ptr};
73
74    use portable_atomic::{AtomicBool, Ordering};
75
76    // Issue 0269 — the pool size is build-time configurable via
77    // `NROS_RMW_SUBSCRIBER_SLOTS` (build.rs, default 8). The old
78    // hardcoded 4 silently capped every embedded session at four
79    // subscriptions: the fifth `create_subscription` hit the exhausted
80    // pool, returned BAD_ALLOC, and the executor surfaced it as an
81    // opaque `SubscriberCreationFailed` (the autoware_sentinel comp-all
82    // wall — every std target Boxes instead and never sees a limit).
83    const SLOT_COUNT: usize = crate::parse_env_usize(
84        env!("NROS_RMW_SUBSCRIBER_SLOTS"),
85        "NROS_RMW_SUBSCRIBER_SLOTS must be a decimal integer",
86    );
87    // issue 1322 — the slot WIDTH, which is a different quantity from the slot
88    // COUNT above and now has its own knob. It was a bare `1024`, so the only
89    // lever the tree offered against a handle that did not fit was
90    // `NROS_RMW_SUBSCRIBER_SLOTS` — which buys more slots of the same too-small
91    // width, at `SLOT_SIZE` bytes each, on the platform with the least RAM.
92    //
93    // AUTHORED, never derived: RFC-0100 D5 rules this out as a sizing input by
94    // name, because no user fact answers "how big is a backend's private state
95    // struct". So it takes the RFC-0049 ladder (env -> `$DOTCONFIG` ->
96    // `[knobs.rmw] subscriber_slot_bytes` -> builtin 1024) and NOT the sizing
97    // descriptor — the same shape as `NROS_RUNTIME_COMPONENT_SLOT_BYTES`, which
98    // is this quantity one layer up in the component pool.
99    const SLOT_SIZE: usize = crate::parse_env_usize(
100        env!("NROS_RMW_SUBSCRIBER_SLOT_BYTES"),
101        "NROS_RMW_SUBSCRIBER_SLOT_BYTES must be a decimal integer",
102    );
103    const SLOT_ALIGN: usize = 16;
104
105    #[repr(align(16))]
106    struct Slot {
107        bytes: UnsafeCell<[u8; SLOT_SIZE]>,
108    }
109
110    // Phase 192.5 — `#[repr(align(16))]` can't take the `SLOT_ALIGN` const, so
111    // assert they stay in lockstep (the insert() guard compares against SLOT_ALIGN).
112    const _: () = assert!(mem::align_of::<Slot>() == SLOT_ALIGN);
113
114    unsafe impl Sync for Slot {}
115
116    impl Slot {
117        const fn new() -> Self {
118            Self {
119                bytes: UnsafeCell::new([0; SLOT_SIZE]),
120            }
121        }
122    }
123
124    // issue 0739 — declare the arithmetic so the pool inventory can price it.
125    // 0271 measured this pool at 8,192 bytes on an image that had never heard of
126    // the knob. issue 1322 — both factors are knobs now, so the figure stays
127    // derivable and the page names the width instead of hiding it in a literal.
128    // nros-pool: SLOTS = NROS_RMW_SUBSCRIBER_SLOTS * NROS_RMW_SUBSCRIBER_SLOT_BYTES
129    static USED: [AtomicBool; SLOT_COUNT] = [const { AtomicBool::new(false) }; SLOT_COUNT];
130    static SLOTS: [Slot; SLOT_COUNT] = [const { Slot::new() }; SLOT_COUNT];
131
132    /// Park `value` in a free slot. `None` means the pool is EXHAUSTED — and
133    /// since issue 1322, nothing else.
134    ///
135    /// It used to mean two unrelated things, and the caller mapped both to
136    /// `NROS_RMW_RET_BAD_ALLOC`: every slot claimed (raise
137    /// `NROS_RMW_SUBSCRIBER_SLOTS`) *or* `size_of::<T>() > SLOT_SIZE`, which no
138    /// value of that knob can fix. So the one remedy the failure pointed at was
139    /// the wrong lever for half the failures, and the expensive one.
140    ///
141    /// The size cause is gone from this return because it was never a runtime
142    /// fact. `size_of::<T>()` is known at compile time, so a given build either
143    /// fits or never fits; the check is the `const` block below, i.e. a BUILD
144    /// error naming the type instead of an image that links, boots, and then
145    /// refuses to register a subscription.
146    ///
147    /// Be precise about WHEN that error arrives: an inline `const` in a generic
148    /// fn is evaluated at MONOMORPHISATION, which `cargo check` does not reach
149    /// (it emits metadata and never collects instantiations — measured). Any
150    /// `cargo build` of a bare-metal image that creates a subscription does.
151    /// That is weaker than a source gate and strictly stronger than the
152    /// boot-time return it replaces.
153    pub unsafe fn insert<T>(value: T) -> Option<*mut T> {
154        const {
155            // FIT GUARD — issue 1322. RFC-0100 D7's shape: the knob publishes a
156            // WIDTH and the FLOOR lives at the pool, which is the only place
157            // that can see `T`.
158            //
159            // Written as a SUBTRACTION rather than an `assert!` because a const
160            // panic message cannot be FORMATTED. An assertion can say what to do
161            // and never what the numbers are; the underflow diagnostic says both,
162            // and the instantiation note names the backend handle. Measured, at
163            // `NROS_RMW_SUBSCRIBER_SLOT_BYTES=1` on `thumbv7m-none-eabi`:
164            //
165            //     error[E0080]: attempt to compute `1_usize - 112_usize`,
166            //                   which would overflow
167            //     note: ... while instantiating
168            //           `fn insert::<ZenohSubscriber>`
169            //
170            // The BINDING NAMES below are load-bearing, and this is the correction
171            // to a first draft that put the remedy in these comments: rustc echoes
172            // the offending SOURCE LINE, and it COLLAPSES the rest of the const
173            // block to `...` — so a comment here reaches whoever opens the file and
174            // never reaches the error text. The name does both.
175            //
176            // REMEDY: raise `NROS_RMW_SUBSCRIBER_SLOT_BYTES` to at least the second
177            // operand — env, or `[knobs.rmw] subscriber_slot_bytes` on the
178            // platform or board (it has no Kconfig symbol).
179            // `NROS_RMW_SUBSCRIBER_SLOTS` is NOT the lever: it multiplies the same
180            // too-small width, at `SLOT_SIZE` bytes of `.bss` a slot.
181            let _raise_nros_rmw_subscriber_slot_bytes = SLOT_SIZE - mem::size_of::<T>();
182            // The align arm, same reasoning. `SLOT_ALIGN` is what `Slot`'s
183            // `repr(align)` gives every element of `SLOTS`, so an over-aligned
184            // handle would be written to a misaligned address. No knob raises this
185            // one — `repr(align)` cannot take a const — so the name says what a
186            // reader has to do instead.
187            let _over_aligned_for_the_cffi_subscriber_slot = SLOT_ALIGN - mem::align_of::<T>();
188        };
189
190        for index in 0..SLOT_COUNT {
191            if USED[index]
192                .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
193                .is_err()
194            {
195                continue;
196            }
197
198            let ptr = SLOTS[index].bytes.get().cast::<T>();
199            unsafe { ptr.write(value) };
200            return Some(ptr);
201        }
202
203        // issue 1322 — the ONE cause left, said out loud beside the `None` with
204        // the lever that answers it and what raising it costs. Before this the
205        // caller's `BAD_ALLOC` was the whole diagnostic, and it could not
206        // distinguish this from a fit failure. issue 0589 — `nros_log`, never
207        // std stdio.
208        nros_log::log_error!(
209            nros_log::get_logger("nros_rmw_cffi"),
210            "subscription pool exhausted: all {} static slot(s) are in use. \
211             Raise NROS_RMW_SUBSCRIBER_SLOTS (each slot costs \
212             NROS_RMW_SUBSCRIBER_SLOT_BYTES = {} bytes of .bss). A handle that \
213             did not FIT cannot reach this path — that is a build error",
214            SLOT_COUNT,
215            SLOT_SIZE
216        );
217        None
218    }
219
220    pub unsafe fn take<T>(ptr: *mut T) -> bool {
221        if ptr.is_null() {
222            return false;
223        }
224
225        for index in 0..SLOT_COUNT {
226            let slot_ptr = SLOTS[index].bytes.get().cast::<T>();
227            if ptr != slot_ptr {
228                continue;
229            }
230
231            unsafe { ptr::drop_in_place(ptr) };
232            USED[index].store(false, Ordering::Release);
233            return true;
234        }
235
236        false
237    }
238}
239
240// ============================================================================
241// Trait alias bundle
242// ============================================================================
243//
244// `RustBackend` ties together every constraint the adapter needs. The
245// alternative — long `where` clauses on every fn — is unreadable. This
246// trait is sealed: backends don't impl it directly; the blanket impl
247// below picks it up automatically for any `R: Rmw` whose associated
248// types line up.
249
250/// Bundle of trait bounds an `Rmw` backend must satisfy to be exposed
251/// through [`RustBackendAdapter`]. Implemented automatically for any
252/// `R: Rmw` whose handle types use `TransportError` and are
253/// `'static`.
254pub trait RustBackend: Sized {
255    type Session: Session<
256            Error = TransportError,
257            PublisherHandle = Self::Publisher,
258            SubscriptionHandle = Self::Subscription,
259            ServiceHandle = Self::Service,
260            ClientHandle = Self::Client,
261        > + 'static;
262    type Publisher: Publisher<Error = TransportError> + 'static;
263    type Subscription: Subscription<Error = TransportError> + 'static;
264    type Service: ServiceTrait<Error = TransportError> + 'static;
265    type Client: ClientTrait<Error = TransportError> + 'static;
266
267    /// Construct a fresh factory instance. Called inside the `open`
268    /// trampoline. Equivalent to `R::default()` for backends that
269    /// `derive(Default)`; the indirection keeps the door open for
270    /// future per-backend registration knobs.
271    fn factory() -> Self;
272
273    /// Move the factory into a session per the `Rmw::open` contract.
274    fn open(self, config: &RmwConfig) -> Result<Self::Session, TransportError>;
275}
276
277impl<R> RustBackend for R
278where
279    R: Rmw<Error = TransportError> + Default + Sized,
280    R::Session: Session<Error = TransportError> + 'static,
281    <R::Session as Session>::PublisherHandle: Publisher<Error = TransportError> + 'static,
282    <R::Session as Session>::SubscriptionHandle: Subscription<Error = TransportError> + 'static,
283    <R::Session as Session>::ServiceHandle: ServiceTrait<Error = TransportError> + 'static,
284    <R::Session as Session>::ClientHandle: ClientTrait<Error = TransportError> + 'static,
285{
286    type Session = R::Session;
287    type Publisher = <R::Session as Session>::PublisherHandle;
288    type Subscription = <R::Session as Session>::SubscriptionHandle;
289    type Service = <R::Session as Session>::ServiceHandle;
290    type Client = <R::Session as Session>::ClientHandle;
291
292    fn factory() -> Self {
293        R::default()
294    }
295
296    fn open(self, config: &RmwConfig) -> Result<Self::Session, TransportError> {
297        Rmw::open(self, config)
298    }
299}
300
301// ============================================================================
302// Helpers: C-string <-> &str
303// ============================================================================
304
305/// Read a null-terminated C string into a Rust `&str`. Returns the
306/// empty string if `ptr` is null or contains invalid UTF-8.
307///
308/// # Safety
309///
310/// `ptr`, if non-null, must point to a valid null-terminated byte
311/// sequence that outlives the returned borrow.
312unsafe fn cstr_to_str<'a>(ptr: *const core::ffi::c_char) -> &'a str {
313    if ptr.is_null() {
314        return "";
315    }
316    let ptr = ptr.cast::<u8>();
317    let mut len = 0usize;
318    // Bound the scan so a missing terminator can't read off the end of
319    // a small caller buffer. 1 KiB matches the typical NAME_BUF_LEN
320    // (256) plus headroom for type-name / hash strings.
321    while len < 4096 && unsafe { *ptr.add(len) } != 0 {
322        len += 1;
323    }
324    let slice = unsafe { core::slice::from_raw_parts(ptr, len) };
325    core::str::from_utf8(slice).unwrap_or("")
326}
327
328/// Borrow a NUL-terminated C string as `&str`, distinguishing "absent" and
329/// "not text" from "empty" — which [`cstr_to_str`] deliberately does not.
330///
331/// `cstr_to_str` collapses NULL and invalid UTF-8 into `""` because its
332/// callers (node names, topic names) treat an unusable name as an absent one.
333/// A configuration property cannot: an empty key is not a key, and a value the
334/// backend cannot read must be reported, not passed on as `""`.
335unsafe fn cstr_to_str_strict<'a>(ptr: *const core::ffi::c_char) -> Option<&'a str> {
336    if ptr.is_null() {
337        return None;
338    }
339    let bytes = ptr.cast::<u8>();
340    let mut len = 0usize;
341    while len < 4096 && unsafe { *bytes.add(len) } != 0 {
342        len += 1;
343    }
344    if len == 0 {
345        return None;
346    }
347    let slice = unsafe { core::slice::from_raw_parts(bytes, len) };
348    core::str::from_utf8(slice).ok()
349}
350
351/// Copy `options->properties` into `out`, returning how many entries were
352/// written, or the `NrosRmwRet` to fail the session with.
353///
354/// # Safety
355/// `options`, when non-NULL, must point at a valid `rmw_session_options_t`
356/// whose `properties` array holds `property_count` valid entries.
357unsafe fn collect_session_properties<'a>(
358    options: *const NrosRmwSessionOptions,
359    out: &mut [(&'a str, &'a str); MAX_SESSION_PROPERTIES],
360) -> Result<usize, NrosRmwRet> {
361    if options.is_null() {
362        return Ok(0);
363    }
364    let opts = unsafe { &*options };
365    let count = opts.property_count;
366    if count == 0 {
367        return Ok(0);
368    }
369    if opts.properties.is_null() || count > MAX_SESSION_PROPERTIES {
370        return Err(NROS_RMW_RET_INVALID_ARGUMENT);
371    }
372    for (i, slot) in out.iter_mut().enumerate().take(count) {
373        let entry = unsafe { &*opts.properties.add(i) };
374        let (Some(key), Some(value)) = (unsafe { cstr_to_str_strict(entry.key) }, unsafe {
375            cstr_to_str_strict(entry.value)
376        }) else {
377            return Err(NROS_RMW_RET_INVALID_ARGUMENT);
378        };
379        *slot = (key, value);
380    }
381    Ok(count)
382}
383
384/// Convert the cffi QoS view back into a `nros_rmw::QoSProfile`. The
385/// adapter trampolines call this when forwarding `create_publisher` /
386/// `create_subscription` into the Rust trait.
387fn qos_from_cffi(q: &NrosRmwQos) -> QoSProfile {
388    use crate::generated;
389    use nros_rmw::{
390        QoSDurabilityPolicy, QoSHistoryPolicy, QoSLivelinessPolicy, QoSReliabilityPolicy,
391    };
392    // Phase 376 W5/B2 — these were `== 0` tests against a dense 0/1 encoding.
393    // Under upstream's numbering 0 is SYSTEM_DEFAULT for every policy, so each
394    // test now has three cases, not two. Written as a match on the named
395    // constants so an unrecognised value is visible rather than folded into
396    // one arm.
397    //
398    // issue 0829 — SYSTEM_DEFAULT now RAISES to the sentinel variant instead
399    // of being folded to the ROS default here. This function is the inbound
400    // edge of a Rust backend, and resolving an absence is the BACKEND's job:
401    // folding at the edge made every Rust backend answer the sentinel
402    // identically, which is exactly what upstream does not do. UNKNOWN keeps
403    // its old fold — it is a read-back artefact ("the backend could not
404    // determine this"), not a request, so it has no sentinel meaning to carry.
405    QoSProfile {
406        reliability: match q.reliability as i32 {
407            generated::NROS_RMW_RELIABILITY_SYSTEM_DEFAULT => QoSReliabilityPolicy::SystemDefault,
408            generated::NROS_RMW_RELIABILITY_BEST_EFFORT => QoSReliabilityPolicy::BestEffort,
409            generated::NROS_RMW_RELIABILITY_RELIABLE => QoSReliabilityPolicy::Reliable,
410            // issue 1973 — RAISED like the sentinel, never folded: a Rust
411            // backend reached past `CffiSession` (which resolves first) must
412            // still see the request and resolve it against its own graph.
413            generated::NROS_RMW_RELIABILITY_BEST_AVAILABLE => QoSReliabilityPolicy::BestAvailable,
414            _ => QoSReliabilityPolicy::Reliable,
415        },
416        durability: match q.durability as i32 {
417            generated::NROS_RMW_DURABILITY_SYSTEM_DEFAULT => QoSDurabilityPolicy::SystemDefault,
418            generated::NROS_RMW_DURABILITY_TRANSIENT_LOCAL => QoSDurabilityPolicy::TransientLocal,
419            generated::NROS_RMW_DURABILITY_BEST_AVAILABLE => QoSDurabilityPolicy::BestAvailable,
420            _ => QoSDurabilityPolicy::Volatile,
421        },
422        history: match q.history as i32 {
423            generated::NROS_RMW_HISTORY_SYSTEM_DEFAULT => QoSHistoryPolicy::SystemDefault,
424            generated::NROS_RMW_HISTORY_KEEP_ALL => QoSHistoryPolicy::KeepAll,
425            _ => QoSHistoryPolicy::KeepLast,
426        },
427        // The depth sentinel is a VALUE (0), so it needs no raising — it
428        // already reads as `DEPTH_SYSTEM_DEFAULT` on the far side.
429        depth: q.depth as u32,
430        // phase-301 (issue 0240): the express hint left the QoS struct; the
431        // create trampolines thread it via `TopicInfo` from the options param.
432        tx_express: false,
433        deadline_ms: q.deadline_ms,
434        lifespan_ms: q.lifespan_ms,
435        liveliness_kind: match q.liveliness_kind as u32 {
436            generated::rmw_liveliness_kind_t::NROS_RMW_LIVELINESS_AUTOMATIC => {
437                QoSLivelinessPolicy::Automatic
438            }
439            generated::rmw_liveliness_kind_t::NROS_RMW_LIVELINESS_MANUAL_BY_NODE => {
440                QoSLivelinessPolicy::ManualByNode
441            }
442            generated::rmw_liveliness_kind_t::NROS_RMW_LIVELINESS_MANUAL_BY_TOPIC => {
443                QoSLivelinessPolicy::ManualByTopic
444            }
445            generated::rmw_liveliness_kind_t::NROS_RMW_LIVELINESS_BEST_AVAILABLE => {
446                QoSLivelinessPolicy::BestAvailable
447            }
448            _ => QoSLivelinessPolicy::None,
449        },
450        liveliness_lease_ms: q.liveliness_lease_ms,
451        avoid_ros_namespace_conventions: q.avoid_ros_namespace_conventions != 0,
452    }
453}
454
455// Phase 376 W5/B1 — ~~`session_node_name`~~ and ~~`session_namespace`~~ are GONE with
456// the `entity_view` fabrication they served. They read the OWNING NODE's
457// identity off a session struct the shim rewrote per call, which is how node
458// identity reached a backend before the node itself did.
459
460/// The node's own identity.
461unsafe fn node_name_of<'a>(node: *const NrosRmwNode) -> Option<&'a str> {
462    let name = unsafe { cstr_to_str((*node).name) };
463    if name.is_empty() { None } else { Some(name) }
464}
465
466unsafe fn node_namespace_of<'a>(node: *const NrosRmwNode) -> &'a str {
467    let namespace = unsafe { cstr_to_str((*node).namespace_) };
468    if namespace.is_empty() { "/" } else { namespace }
469}
470
471// ============================================================================
472// Adapter
473// ============================================================================
474
475/// Longest serialization-format name this seam can hand across the C ABI.
476///
477/// RFC-0088 D2 — the vtable slot carries the format's cross-image identity
478/// STRING, and a Rust backend spells that identity as a `&'static str`
479/// (`Session::SERIALIZATION_FORMAT`), which is not NUL-terminated. The
480/// conversion therefore has to happen somewhere, and it happens HERE, at
481/// compile time, into per-`R` static storage — never at call time into a
482/// buffer whose lifetime the slot's `const char *` contract cannot express.
483///
484/// 32 is generous for a name like `"cdr"`; a longer one is a const-eval error
485/// rather than a truncation, because a truncated format name is a different,
486/// plausible format.
487const FORMAT_NAME_CAP: usize = 32;
488
489/// NUL-terminate a format name at compile time.
490const fn format_name_cstr(name: &str) -> [u8; FORMAT_NAME_CAP] {
491    let bytes = name.as_bytes();
492    assert!(
493        bytes.len() < FORMAT_NAME_CAP,
494        "backend name (format or identifier) does not fit the vtable slot's buffer"
495    );
496    let mut out = [0u8; FORMAT_NAME_CAP];
497    let mut i = 0;
498    while i < bytes.len() {
499        out[i] = bytes[i];
500        i += 1;
501    }
502    out
503}
504
505/// `get_serialization_format` — RFC-0088 D4.
506///
507/// Takes no arguments, exactly as upstream's `rmw_get_serialization_format()`
508/// does, because the answer is a property of the BACKEND and not of any entity
509/// it created. What differs from upstream is who holds the question: upstream
510/// has one middleware per process and can answer from a global, while
511/// `nros_rmw_cffi_register_named` admits several backends in one image, so the
512/// answer is per-vtable — which is per-session, since a session is opened
513/// against one vtable.
514unsafe extern "C" fn get_serialization_format_trampoline<R: RustBackend>() -> *const c_char {
515    RustBackendAdapter::<R>::SERIALIZATION_FORMAT_CSTR
516        .as_ptr()
517        .cast()
518}
519
520/// `get_implementation_identifier` — the backend's name from its own
521/// `Session::IMPLEMENTATION_IDENTIFIER`, installed only when that const is
522/// `Some` (see `VTABLE`). Static storage, as for the format slot.
523unsafe extern "C" fn get_implementation_identifier_trampoline<R: RustBackend>() -> *const c_char {
524    RustBackendAdapter::<R>::IMPLEMENTATION_IDENTIFIER_CSTR
525        .as_ptr()
526        .cast()
527}
528
529/// issue 1329 — a Rust backend reached through the C ABI answers for ITSELF.
530///
531/// `Session::supported_qos_policies` is already the Rust-side answer, derived
532/// from `nros-qos-honours:` claims in the backend's own sources (phase-428
533/// W9). Without this trampoline a Rust backend registered through the C route
534/// would answer `NONE` — the NULL-slot meaning — and every entity stating a
535/// policy on it would be refused: the mask exists and the seam could not carry
536/// it, which is issue 1329's shape one lane over.
537///
538/// # Safety
539/// Called only by the runtime, with a session view this adapter created.
540unsafe extern "C" fn supported_qos_policies_trampoline<R: RustBackend>(
541    session: *const NrosRmwSession,
542    out_mask: *mut u32,
543) -> NrosRmwRet {
544    if out_mask.is_null() {
545        return NROS_RMW_RET_INVALID_ARGUMENT;
546    }
547    let Some(s) = (unsafe { session_ref::<R::Session>(session) }) else {
548        return NROS_RMW_RET_INVALID_ARGUMENT;
549    };
550    // SAFETY: checked non-null above.
551    unsafe { *out_mask = Session::supported_qos_policies(s).0 };
552    NROS_RMW_RET_OK
553}
554
555/// Wraps a Rust `Rmw` backend behind the canonical
556/// [`NrosRmwVtable`] C ABI. See module docs.
557pub struct RustBackendAdapter<R>(PhantomData<R>);
558
559impl<R: RustBackend> RustBackendAdapter<R> {
560    /// RFC-0088 D4 — `R`'s serialization format, NUL-terminated, in per-`R`
561    /// static storage so the `get_serialization_format` slot can hand out a
562    /// `const char *` that outlives the call.
563    ///
564    /// `&<const expr>` in a const initialiser is promoted to `'static`, which
565    /// is what makes the slot's lifetime contract true rather than merely
566    /// hoped for.
567    const SERIALIZATION_FORMAT_CSTR: &'static [u8; FORMAT_NAME_CAP] =
568        &format_name_cstr(<R::Session as Session>::SERIALIZATION_FORMAT);
569
570    /// `R`'s implementation identifier, NUL-terminated, in per-`R` static
571    /// storage. Empty when `R` states none — the slot is then NOT installed
572    /// (see `VTABLE`), so this buffer is never handed out in that case.
573    const IMPLEMENTATION_IDENTIFIER_CSTR: &'static [u8; FORMAT_NAME_CAP] =
574        &format_name_cstr(match <R::Session as Session>::IMPLEMENTATION_IDENTIFIER {
575            Some(name) => name,
576            None => "",
577        });
578
579    /// Monomorphised vtable for backend `R`. The `const` is promoted
580    /// to per-type static storage, so `&Self::VTABLE` has `'static`
581    /// lifetime — safe to hand to `nros_rmw_cffi_register_named`.
582    pub const VTABLE: NrosRmwVtable = NrosRmwVtable {
583        create_session: Some(create_session_trampoline::<R>),
584        destroy_session: Some(destroy_session_trampoline::<R>),
585        drive_io: Some(drive_io_trampoline::<R>),
586        create_publisher: Some(create_publisher_trampoline::<R>),
587        destroy_publisher: Some(destroy_publisher_trampoline::<R>),
588        publish: Some(publish_trampoline::<R>),
589        create_subscription: Some(create_subscription_trampoline::<R>),
590        destroy_subscription: Some(destroy_subscription_trampoline::<R>),
591        take: Some(take_trampoline::<R>),
592        has_data: Some(has_data_trampoline::<R>),
593        create_service: Some(create_service_trampoline::<R>),
594        destroy_service: Some(destroy_service_trampoline::<R>),
595        take_request: Some(take_request_trampoline::<R>),
596        has_request: Some(has_request_trampoline::<R>),
597        send_response: Some(send_response_trampoline::<R>),
598        create_client: Some(create_client_trampoline::<R>),
599        destroy_client: Some(destroy_client_trampoline::<R>),
600        // Phase-301 (issue 0240) — `send_request_raw` + `take_response_raw`
601        // is the one request/reply path; the blocking `call_raw` slot is gone.
602        send_request: Some(send_request_trampoline::<R>),
603        take_response: Some(take_response_trampoline::<R>),
604        // Issue 2136 — the reply's metadata, from `ClientTrait::response_info`.
605        // Installed for every Rust backend: one that reports nothing hands
606        // back the all-zero struct, which reads as `NOT_REPORTED`.
607        take_response_with_info: Some(take_response_with_info_trampoline::<R>),
608        subscription_event_init: Some(subscription_event_init_trampoline::<R>),
609        publisher_event_init: Some(publisher_event_init_trampoline::<R>),
610        publisher_assert_liveliness: Some(publisher_assert_liveliness_trampoline::<R>),
611        get_gid_for_publisher: Some(get_gid_for_publisher_trampoline::<R>),
612        next_deadline_ms: Some(next_deadline_ms_trampoline::<R>),
613        set_wake_callback: Some(set_wake_callback_trampoline::<R>),
614        // phase-521 W3 (RFC-0113 D4) — the backend's own answer. Installed
615        // for every Rust backend because `set_wake_callback` above is too, so
616        // the NULL-slot inference ("a wake slot means Notify") would claim
617        // Notify for a backend whose trait method ignores the callback.
618        readiness_source: Some(readiness_source_trampoline::<R>),
619        // Phase 124.A — zero-copy slots default to NULL on the
620        // generic adapter; per-backend opt-in via dedicated trampolines
621        // (see `nros-rmw-zenoh` for the first implementation in 124.A.4).
622        // Runtime falls back to the arena path when these are NULL.
623        service_server_is_available: Some(service_server_is_available_trampoline::<R>),
624        take_sequence: Some(take_sequence_trampoline::<R>),
625        publish_streamed: Some(publish_streamed_trampoline::<R>),
626        ping_session: Some(ping_session_trampoline::<R>),
627        subscription_supports_in_place: Some(subscription_supports_in_place_trampoline::<R>),
628        process_raw_in_place: Some(process_raw_in_place_trampoline::<R>),
629        // phase-381 W3 — graph enumeration. The trait default is
630        // `Unsupported`, so a backend with no graph (XRCE) answers that rather
631        // than an empty one: "cannot tell you" and "nothing is there" are
632        // different answers and W6 keeps them distinguishable.
633        get_node_names: Some(get_node_names_trampoline::<R>),
634        count_publishers: Some(count_publishers_trampoline::<R>),
635        count_subscribers: Some(count_subscribers_trampoline::<R>),
636        get_topic_names_and_types: Some(get_topic_names_and_types_trampoline::<R>),
637        get_service_names_and_types: Some(get_service_names_and_types_trampoline::<R>),
638        // phase-381 W3 — the last six. The trait defaults are `Unsupported`, so
639        // a backend with no graph still answers "cannot tell you" rather than
640        // an empty graph.
641        get_publisher_names_and_types_by_node: Some(
642            get_publisher_names_and_types_by_node_trampoline::<R>,
643        ),
644        get_subscriber_names_and_types_by_node: Some(
645            get_subscriber_names_and_types_by_node_trampoline::<R>,
646        ),
647        get_service_names_and_types_by_node: Some(
648            get_service_names_and_types_by_node_trampoline::<R>,
649        ),
650        get_client_names_and_types_by_node: Some(
651            get_client_names_and_types_by_node_trampoline::<R>,
652        ),
653        get_publishers_info_by_topic: Some(get_publishers_info_by_topic_trampoline::<R>),
654        get_subscriptions_info_by_topic: Some(get_subscriptions_info_by_topic_trampoline::<R>),
655        // phase-467 Row 8 — the graph-CHANGE edge, the twelfth graph slot and
656        // the last one that was empty. Unconditional for every
657        // `R: RustBackend`, like the eleven above and for the same reason: the
658        // trait default is `Unsupported`, so a backend with no graph answers
659        // "cannot tell you" rather than accepting a callback nothing will
660        // fire. That default is ALSO the capability probe the runtime makes
661        // before it consumes an executor handle slot, so this row being
662        // unconditional is what keeps the refusal reachable.
663        node_get_graph_guard_condition: Some(node_get_graph_guard_condition_trampoline::<R>),
664        // RFC-0088 D4 / phase-421 W2 — every Rust backend answers with its own
665        // `Session::SERIALIZATION_FORMAT`, so a bridge image can ask each
666        // session rather than trusting one image-wide constant. The trait
667        // default is `"cdr"`; a backend that speaks something else overrides
668        // it and this slot reports the override with no work here.
669        get_serialization_format: Some(get_serialization_format_trampoline::<R>),
670        // The backend's name, when `R` states one. Conditional, unlike the
671        // format slot: the identifier has no honest default, and a NULL slot
672        // makes the runtime answer with the name the vtable was REGISTERED
673        // under, which is a fact rather than a guess (`rmw_vtable.h`).
674        get_implementation_identifier: if <R::Session as Session>::IMPLEMENTATION_IDENTIFIER
675            .is_some()
676        {
677            Some(get_implementation_identifier_trampoline::<R>)
678        } else {
679            None
680        },
681        // issue 1329 — `R`'s own mask, so the C route stops answering a union
682        // on its behalf. Unconditional for every `R: RustBackend`: the trait
683        // default is `QoSPolicyMask::NONE`, which is the same answer a NULL
684        // slot carries, so installing it for a backend that declares nothing
685        // changes nothing.
686        supported_qos_policies: Some(supported_qos_policies_trampoline::<R>),
687        // The granted-QoS read-back. See the trampolines for why these are
688        // unconditional.
689        publisher_get_actual_qos: Some(publisher_get_actual_qos_trampoline::<R>),
690        subscription_get_actual_qos: Some(subscription_get_actual_qos_trampoline::<R>),
691        client_request_publisher_get_actual_qos: Some(
692            client_request_publisher_get_actual_qos_trampoline::<R>,
693        ),
694        client_response_subscription_get_actual_qos: Some(
695            client_response_subscription_get_actual_qos_trampoline::<R>,
696        ),
697        service_request_subscription_get_actual_qos: Some(
698            service_request_subscription_get_actual_qos_trampoline::<R>,
699        ),
700        service_response_publisher_get_actual_qos: Some(
701            service_response_publisher_get_actual_qos_trampoline::<R>,
702        ),
703        // Issue 1940 — every Rust backend's `register_waker`, reached from C.
704        // Unconditional: a backend whose `register_waker` returns `false` (the
705        // trait default) makes the slot answer UNSUPPORTED, the same verdict a
706        // NULL slot carries.
707        subscription_set_on_new_message_callback: Some(
708            subscription_set_on_new_message_callback_trampoline::<R>,
709        ),
710        service_set_on_new_request_callback: Some(
711            service_set_on_new_request_callback_trampoline::<R>,
712        ),
713        client_set_on_new_response_callback: Some(
714            client_set_on_new_response_callback_trampoline::<R>,
715        ),
716        // Issue 1978 — the Jazzy contract's four ADOPT slots. Unconditional
717        // for the same reason as the graph slots above: each trait default
718        // (`Unsupported`, `Unsupported`, `Unsupported`, `false`) is the answer
719        // a NULL slot would carry, so a backend that overrides nothing still
720        // answers honestly.
721        count_clients: Some(count_clients_trampoline::<R>),
722        count_services: Some(count_services_trampoline::<R>),
723        get_gid_for_client: Some(get_gid_for_client_trampoline::<R>),
724        event_type_is_supported: Some(event_type_is_supported_trampoline::<R>),
725        ..EMPTY_VTABLE
726    };
727
728    /// Install the per-`R` vtable into the cffi registry under the
729    /// implicit name `"default"`. Idempotent — re-registering the
730    /// same vtable is a no-op from the runtime's perspective.
731    ///
732    /// Most backends should use [`register_named`](Self::register_named)
733    /// instead so they show up in the registry under their canonical
734    /// name (`"zenoh"`, `"dds"`, `"xrce"`, …). Phase 128.B.5 routes
735    /// the implicit-name path through `_register_named` too, so the
736    /// legacy unnamed C shim no longer participates in registration.
737    pub fn register() -> NrosRmwRet {
738        // SAFETY: `&Self::VTABLE` is a reference to a const-promoted
739        // static; address stable for the program's lifetime.
740        unsafe { crate::nros_rmw_cffi_register_named(c"default".as_ptr(), &Self::VTABLE) }
741    }
742
743    /// Phase 104.B.2 — install the per-`R` vtable under a stable
744    /// name. Multiple backends can coexist via this entry point.
745    ///
746    /// # Safety
747    /// `name` must be a valid NUL-terminated UTF-8 string.
748    pub unsafe fn register_named(name: *const core::ffi::c_char) -> NrosRmwRet {
749        // SAFETY: `&Self::VTABLE` is a reference to a const-promoted
750        // static; address stable for the program's lifetime.
751        unsafe { crate::nros_rmw_cffi_register_named(name, &Self::VTABLE) }
752    }
753}
754
755// ============================================================================
756// Trampolines — session lifecycle
757// ============================================================================
758
759unsafe extern "C" fn create_session_trampoline<R: RustBackend>(
760    locator: *const core::ffi::c_char,
761    mode: u8,
762    domain_id: u32,
763    node_name: *const core::ffi::c_char,
764    options: *const NrosRmwSessionOptions,
765    out: *mut NrosRmwSession,
766) -> NrosRmwRet {
767    if out.is_null() {
768        return NROS_RMW_RET_INVALID_ARGUMENT;
769    }
770    // phase-206 W3 — marshal the caller's session properties into the slice
771    // `RmwConfig` takes. This used to read `let _ = options;` with
772    // `properties: &[]` hard-coded, which made every backend-specific
773    // transport setting unreachable from C and C++: the Rust trait had carried
774    // `properties` since it existed, and only a hosted Rust caller building an
775    // `RmwConfig` by hand could fill it.
776    //
777    // Refused rather than truncated (each is INVALID_ARGUMENT):
778    //   - `property_count > 0` with a NULL `properties` pointer,
779    //   - more than `RMW_SESSION_MAX_PROPERTIES` entries,
780    //   - a NULL, empty, or non-UTF-8 key or value.
781    // Dropping any of these silently is the failure this work item exists to
782    // remove; a caller that cannot see its configuration was ignored debugs
783    // the transport instead of the typo.
784    let mut prop_buf: [(&str, &str); MAX_SESSION_PROPERTIES] = [("", ""); MAX_SESSION_PROPERTIES];
785    let prop_count = match unsafe { collect_session_properties(options, &mut prop_buf) } {
786        Ok(n) => n,
787        Err(ret) => return ret,
788    };
789    let cfg = RmwConfig {
790        locator: unsafe { cstr_to_str(locator) },
791        mode: if mode == 0 {
792            SessionMode::Client
793        } else {
794            SessionMode::Peer
795        },
796        domain_id,
797        node_name: unsafe { cstr_to_str(node_name) },
798        // Issue 1444 — the INBOUND half. `node_name` arrives as its own
799        // argument; the namespace arrives on the session view, which
800        // `rmw_vtable.h` says the runtime hands over "with `node_name` /
801        // `namespace_` already filled". This read `""` unconditionally, so
802        // even once the runtime filled the buffer a Rust backend behind this
803        // adapter — which is every in-tree zenoh build — would still have seen
804        // nothing. Both halves move together or neither is observable.
805        //
806        // SAFETY: `out` is non-null (checked above) and its `namespace_` is
807        // either the runtime's NUL-terminated buffer or NULL; `cstr_to_str`
808        // answers `""` for NULL, which is the root.
809        namespace: unsafe { cstr_to_str((*out).namespace_) },
810        properties: &prop_buf[..prop_count],
811    };
812    let factory = R::factory();
813    match factory.open(&cfg) {
814        Ok(session) => match box_handle("session", session) {
815            Ok(boxed) => {
816                unsafe {
817                    (*out).backend_data = boxed;
818                }
819                NROS_RMW_RET_OK
820            }
821            // An unboxable session must not stay open with nobody holding it:
822            // close then drop, the order `destroy_session_trampoline` uses.
823            Err(mut session) => {
824                let _ = Session::close(&mut session);
825                drop(session);
826                NROS_RMW_RET_BAD_ALLOC
827            }
828        },
829        Err(e) => ret_from_error(&e),
830    }
831}
832
833unsafe extern "C" fn destroy_session_trampoline<R: RustBackend>(
834    session: *mut NrosRmwSession,
835) -> NrosRmwRet {
836    let Some(boxed) = (unsafe { take_box::<R::Session>(session_data_mut(session)) }) else {
837        return NROS_RMW_RET_INVALID_ARGUMENT;
838    };
839    let mut s = boxed;
840    let ret = match Session::close(&mut *s) {
841        Ok(()) => NROS_RMW_RET_OK,
842        Err(e) => ret_from_error(&e),
843    };
844    drop(s);
845    ret
846}
847
848unsafe extern "C" fn drive_io_trampoline<R: RustBackend>(
849    session: *mut NrosRmwSession,
850    timeout_ms: i32,
851) -> NrosRmwRet {
852    let Some(s) = (unsafe { session_mut::<R::Session>(session) }) else {
853        return NROS_RMW_RET_INVALID_ARGUMENT;
854    };
855    match Session::drive_io(s, timeout_ms) {
856        Ok(()) => NROS_RMW_RET_OK,
857        Err(e) => ret_from_error(&e),
858    }
859}
860
861// ============================================================================
862// Trampolines — publisher
863// ============================================================================
864
865unsafe extern "C" fn create_publisher_trampoline<R: RustBackend>(
866    node: *const NrosRmwNode,
867    // phase-406 W1 — the identity arrives as ONE argument, in upstream's
868    // position. Unpacked immediately below so the body is unchanged.
869    type_support: *const crate::generated::rmw_message_type_support_t,
870    topic_name: *const core::ffi::c_char,
871    domain_id: u32,
872    qos: *const NrosRmwQos,
873    options: *const rmw_publisher_options_t,
874    out: *mut NrosRmwPublisher,
875) -> NrosRmwRet {
876    // phase-406 W1 — unpack once, so the body below is untouched. A NULL
877    // `type_support` is INVALID_ARGUMENT rather than a silent empty type: the
878    // identity is what the backend keys its entity on, and an entity created
879    // with no type is a subscription that matches nothing and reports nothing.
880    let Some(ts) = (unsafe { type_support.as_ref() }) else {
881        return NROS_RMW_RET_INVALID_ARGUMENT;
882    };
883    let (type_name, type_hash) = (ts.type_name, ts.type_hash);
884    if out.is_null() || qos.is_null() {
885        return NROS_RMW_RET_INVALID_ARGUMENT;
886    }
887    // Phase 376 W5/B1 — the node carries both halves now: its own identity, and
888    // the route to its session (upstream's `context`). Reading the name off the
889    // session was the `entity_view` fabrication, and it is gone.
890    if node.is_null() {
891        return NROS_RMW_RET_INVALID_ARGUMENT;
892    }
893    let session = unsafe { (*node).session };
894    let Some(s) = (unsafe { session_mut::<R::Session>(session) }) else {
895        return NROS_RMW_RET_INVALID_ARGUMENT;
896    };
897    let node_name = unsafe { node_name_of(node) };
898    let namespace = unsafe { node_namespace_of(node) };
899    let topic = TopicInfo {
900        name: unsafe { cstr_to_str(topic_name) },
901        type_name: unsafe { cstr_to_str(type_name) },
902        type_hash: unsafe { cstr_to_str(type_hash) },
903        domain_id,
904        node_name,
905        namespace,
906        // Publisher side — the receive-buffer hint is subscription-only.
907        rx_buffer_hint: 0,
908        arrival_rate_mhz: 0,
909        // phase-301 (issue 0240) — express hint from the NULLable options
910        // struct (NULL = default, not express).
911        tx_express: unsafe { options.as_ref() }.is_some_and(|o| o.tx_express != 0),
912    };
913    let qos_settings = qos_from_cffi(unsafe { &*qos });
914    match Session::create_publisher(s, &topic, qos_settings) {
915        Ok(pub_handle) => match box_handle("publisher", pub_handle) {
916            Ok(boxed) => {
917                unsafe {
918                    (*out).backend_data = boxed;
919                }
920                NROS_RMW_RET_OK
921            }
922            // Dropping the handle undeclares the entity the backend just made.
923            Err(pub_handle) => {
924                drop(pub_handle);
925                NROS_RMW_RET_BAD_ALLOC
926            }
927        },
928        Err(e) => ret_from_error(&e),
929    }
930}
931
932unsafe extern "C" fn destroy_publisher_trampoline<R: RustBackend>(
933    publisher: *mut NrosRmwPublisher,
934) -> NrosRmwRet {
935    let _ = unsafe { take_box::<R::Publisher>(publisher_data_mut(publisher)) };
936
937    NROS_RMW_RET_OK
938}
939
940unsafe extern "C" fn publish_trampoline<R: RustBackend>(
941    publisher: *const NrosRmwPublisher,
942    // phase-406 W2 — by VALUE; unpacked so the body is unchanged.
943    payload: crate::generated::rmw_byte_span_t,
944) -> NrosRmwRet {
945    let (data, len) = (payload.data, payload.len);
946    let Some(p) = (unsafe { publisher_ref::<R::Publisher>(publisher) }) else {
947        return NROS_RMW_RET_INVALID_ARGUMENT;
948    };
949    if data.is_null() && len != 0 {
950        return NROS_RMW_RET_INVALID_ARGUMENT;
951    }
952    let slice = unsafe { core::slice::from_raw_parts(data, len) };
953    match Publisher::publish_raw(p, slice) {
954        Ok(()) => NROS_RMW_RET_OK,
955        Err(e) => ret_from_error(&e),
956    }
957}
958
959// ============================================================================
960// Trampolines — subscriber
961// ============================================================================
962
963unsafe extern "C" fn create_subscription_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_message_type_support_t,
968    topic_name: *const core::ffi::c_char,
969    domain_id: u32,
970    qos: *const NrosRmwQos,
971    options: *const rmw_subscription_options_t,
972    out: *mut NrosRmwSubscription,
973) -> NrosRmwRet {
974    // phase-406 W1 — unpack once, so the body below is untouched. A NULL
975    // `type_support` is INVALID_ARGUMENT rather than a silent empty type: the
976    // identity is what the backend keys its entity on, and an entity created
977    // with no type is a subscription that matches nothing and reports nothing.
978    let Some(ts) = (unsafe { type_support.as_ref() }) else {
979        return NROS_RMW_RET_INVALID_ARGUMENT;
980    };
981    let (type_name, type_hash) = (ts.type_name, ts.type_hash);
982    if out.is_null() || qos.is_null() {
983        return NROS_RMW_RET_INVALID_ARGUMENT;
984    }
985    // Phase 376 W5/B1 — the node carries both halves now: its own identity, and
986    // the route to its session (upstream's `context`). Reading the name off the
987    // session was the `entity_view` fabrication, and it is gone.
988    if node.is_null() {
989        return NROS_RMW_RET_INVALID_ARGUMENT;
990    }
991    let session = unsafe { (*node).session };
992    let Some(s) = (unsafe { session_mut::<R::Session>(session) }) else {
993        return NROS_RMW_RET_INVALID_ARGUMENT;
994    };
995    let node_name = unsafe { node_name_of(node) };
996    let namespace = unsafe { node_namespace_of(node) };
997    // The NULLable options struct, read once for both of its fields.
998    let opts = unsafe { options.as_ref() };
999    let topic = TopicInfo {
1000        name: unsafe { cstr_to_str(topic_name) },
1001        type_name: unsafe { cstr_to_str(type_name) },
1002        type_hash: unsafe { cstr_to_str(type_hash) },
1003        domain_id,
1004        node_name,
1005        namespace,
1006        // Phase 231 (RFC-0038) / phase-301 (issue 0240) — receive-buffer size
1007        // hint from the NULLable options struct (NULL = unset), so the backend
1008        // can size-class its receive storage.
1009        rx_buffer_hint: opts.map_or(0, |o| o.rx_buffer_hint as usize),
1010        // phase-519 W3 — the declared arrival rate (RFC-0074), same struct.
1011        arrival_rate_mhz: opts.map_or(0, |o| o.arrival_rate_mhz),
1012        // Subscription side — express is a publisher-only hint.
1013        tx_express: false,
1014    };
1015    let qos_settings = qos_from_cffi(unsafe { &*qos });
1016    match Session::create_subscription(s, &topic, qos_settings) {
1017        Ok(sub_handle) => {
1018            // Issue 1940 — the handle travels inside a `Hosted`, which carries
1019            // its C wake registration. `backend_data` still reads as a
1020            // `*const R::Subscription` everywhere else (`repr(C)`, inner first).
1021            let sub_handle = Hosted::new(sub_handle, |t: &R::Subscription, w| {
1022                Subscription::register_waker(t, w)
1023            });
1024            #[cfg(all(target_os = "none", not(feature = "std")))]
1025            {
1026                let Some(ptr) = (unsafe { static_subscriber_storage::insert(sub_handle) }) else {
1027                    return crate::NROS_RMW_RET_BAD_ALLOC;
1028                };
1029                unsafe {
1030                    (*out).backend_data = ptr as *mut c_void;
1031                }
1032                NROS_RMW_RET_OK
1033            }
1034            #[cfg(not(all(target_os = "none", not(feature = "std"))))]
1035            {
1036                match box_handle("subscription", sub_handle) {
1037                    Ok(boxed) => {
1038                        unsafe {
1039                            (*out).backend_data = boxed;
1040                        }
1041                        NROS_RMW_RET_OK
1042                    }
1043                    // Dropping the handle undeclares the entity the backend
1044                    // just made.
1045                    Err(sub_handle) => {
1046                        drop(sub_handle);
1047                        NROS_RMW_RET_BAD_ALLOC
1048                    }
1049                }
1050            }
1051        }
1052        Err(e) => ret_from_error(&e),
1053    }
1054}
1055
1056unsafe extern "C" fn destroy_subscription_trampoline<R: RustBackend>(
1057    subscriber: *mut NrosRmwSubscription,
1058) -> NrosRmwRet {
1059    let slot = unsafe { subscription_data_mut(subscriber) };
1060    #[cfg(all(target_os = "none", not(feature = "std")))]
1061    {
1062        if unsafe {
1063            static_subscriber_storage::take::<Hosted<R::Subscription>>(
1064                *slot as *mut Hosted<R::Subscription>,
1065            )
1066        } {
1067            *slot = core::ptr::null_mut();
1068            return NROS_RMW_RET_OK;
1069        }
1070    }
1071    let _ = unsafe { take_box::<Hosted<R::Subscription>>(slot) };
1072
1073    NROS_RMW_RET_OK
1074}
1075
1076unsafe extern "C" fn take_trampoline<R: RustBackend>(
1077    subscriber: *const NrosRmwSubscription,
1078    // phase-406 W2 — by POINTER; `capacity` in, `len` out.
1079    out: *mut crate::generated::rmw_mut_byte_span_t,
1080    taken: *mut bool,
1081) -> NrosRmwRet {
1082    let Some(span) = (unsafe { out.as_mut() }) else {
1083        return NROS_RMW_RET_INVALID_ARGUMENT;
1084    };
1085    let (buf, buf_len) = (span.data, span.capacity);
1086    let out_len: *mut usize = &mut span.len;
1087    // Phase 376 W3.b/W3.d step A — upstream's `rmw_take` shape: status in the
1088    // return, `taken` and the byte count in out-parameters. `Ok(None)` is no
1089    // longer the NO_DATA sentinel.
1090    if out_len.is_null() || taken.is_null() {
1091        return NROS_RMW_RET_INVALID_ARGUMENT;
1092    }
1093    let Some(s) = (unsafe { subscription_mut::<R::Subscription>(subscriber) }) else {
1094        return NROS_RMW_RET_INVALID_ARGUMENT;
1095    };
1096    if buf.is_null() && buf_len != 0 {
1097        return NROS_RMW_RET_INVALID_ARGUMENT;
1098    }
1099    let slice = unsafe { core::slice::from_raw_parts_mut(buf, buf_len) };
1100    let key = unsafe { (*subscriber).backend_data as usize };
1101
1102    // SAFETY (all four writes below): both pointers checked non-null above.
1103    #[cfg(feature = "safety-e2e")]
1104    if crate::take_cffi_integrity_request(key) {
1105        return match Subscription::take_validated(s, slice) {
1106            Ok(Some((n, status))) => {
1107                crate::store_cffi_integrity_status(key, status);
1108                unsafe {
1109                    *out_len = n;
1110                    *taken = true;
1111                }
1112                NROS_RMW_RET_OK
1113            }
1114            Ok(None) => {
1115                unsafe { *taken = false };
1116                NROS_RMW_RET_OK
1117            }
1118            Err(e) => refused_take_ret(s, &e, out_len),
1119        };
1120    }
1121
1122    match Subscription::take_serialized_with_info(s, slice) {
1123        Ok(Some((n, info))) => {
1124            crate::store_cffi_message_info(key, info);
1125            unsafe {
1126                *out_len = n;
1127                *taken = true;
1128            }
1129            NROS_RMW_RET_OK
1130        }
1131        Ok(None) => {
1132            unsafe { *taken = false };
1133            NROS_RMW_RET_OK
1134        }
1135        Err(e) => refused_take_ret(s, &e, out_len),
1136    }
1137}
1138
1139/// Issue 1612 — a Rust backend's refused take, carried across the C ABI.
1140///
1141/// The status alone says THAT the sample did not fit; `rmw_vtable.h` now
1142/// also asks for `out->len` to say how big it was. The Rust backend answers
1143/// through [`Subscription::refused_sample_len`], so this is where the two
1144/// halves meet. Unknown leaves `len` at the caller's
1145/// `NROS_RMW_TAKE_LEN_UNKNOWN`, which is the answer the header gives for it.
1146fn refused_take_ret<S: Subscription<Error = TransportError>>(
1147    s: &S,
1148    e: &TransportError,
1149    out_len: *mut usize,
1150) -> NrosRmwRet {
1151    refused_ret(e, s.refused_sample_len(), out_len)
1152}
1153
1154/// Issues 1612 / 1632 — the ONE place a Rust backend's refused size crosses
1155/// the C ABI, for all three take slots: on `BufferTooSmall` the span's `len`
1156/// gets the size the backend reported; unknown leaves the caller's
1157/// `NROS_RMW_TAKE_LEN_UNKNOWN`.
1158fn refused_ret(e: &TransportError, needed: Option<usize>, out_len: *mut usize) -> NrosRmwRet {
1159    if matches!(e, TransportError::BufferTooSmall)
1160        && let Some(needed) = needed
1161    {
1162        // SAFETY: every caller checked the span (and so `out_len`) non-null
1163        // before the take.
1164        unsafe { *out_len = needed };
1165    }
1166    ret_from_error(e)
1167}
1168
1169unsafe extern "C" fn has_data_trampoline<R: RustBackend>(
1170    subscription: *mut NrosRmwSubscription,
1171    out_has_data: *mut bool,
1172) -> NrosRmwRet {
1173    // Phase 376 W3.d step A — the flag moves to an out-parameter so the return
1174    // carries only a status. Written on OK only.
1175    if out_has_data.is_null() {
1176        return NROS_RMW_RET_INVALID_ARGUMENT;
1177    }
1178    // `subscription_ref`, not `_mut`: the probe is logically read-only and the
1179    // header says a backend must not mutate state here.
1180    let Some(s) = (unsafe { subscription_ref::<R::Subscription>(subscription) }) else {
1181        return NROS_RMW_RET_INVALID_ARGUMENT;
1182    };
1183    // SAFETY: checked non-null above.
1184    unsafe { *out_has_data = Subscription::has_data(s) };
1185    NROS_RMW_RET_OK
1186}
1187
1188// Phase 231 (RFC-0038) — in-place subscription take across the C ABI.
1189
1190unsafe extern "C" fn subscription_supports_in_place_trampoline<R: RustBackend>(
1191    subscriber: *mut NrosRmwSubscription,
1192    out_supports: *mut bool,
1193) -> NrosRmwRet {
1194    // Phase 376 W3.d step A — capability out, status returned.
1195    if out_supports.is_null() {
1196        return NROS_RMW_RET_INVALID_ARGUMENT;
1197    }
1198    let Some(s) = (unsafe { subscription_ref::<R::Subscription>(subscriber) }) else {
1199        return NROS_RMW_RET_INVALID_ARGUMENT;
1200    };
1201    // SAFETY: checked non-null above.
1202    unsafe { *out_supports = Subscription::supports_process_in_place(s) };
1203    NROS_RMW_RET_OK
1204}
1205
1206unsafe extern "C" fn process_raw_in_place_trampoline<R: RustBackend>(
1207    subscriber: *mut NrosRmwSubscription,
1208    ctx: *mut core::ffi::c_void,
1209    cb: Option<
1210        unsafe extern "C" fn(
1211            ctx: *mut core::ffi::c_void,
1212            message: crate::generated::rmw_byte_span_t,
1213        ),
1214    >,
1215    out_processed: *mut bool,
1216) -> NrosRmwRet {
1217    // Phase 376 W3.d step A — "did it process one" is the out-parameter, and
1218    // `Ok(false)` is no longer reported as the NO_DATA sentinel.
1219    if out_processed.is_null() {
1220        return NROS_RMW_RET_INVALID_ARGUMENT;
1221    }
1222    let Some(s) = (unsafe { subscription_mut::<R::Subscription>(subscriber) }) else {
1223        return NROS_RMW_RET_INVALID_ARGUMENT;
1224    };
1225    let Some(cb) = cb else {
1226        return NROS_RMW_RET_INVALID_ARGUMENT;
1227    };
1228    match Subscription::process_raw_in_place(s, |raw| unsafe {
1229        cb(
1230            ctx,
1231            crate::generated::rmw_byte_span_t {
1232                data: raw.as_ptr(),
1233                len: raw.len(),
1234            },
1235        )
1236    }) {
1237        Ok(processed) => {
1238            // SAFETY: checked non-null above.
1239            unsafe { *out_processed = processed };
1240            NROS_RMW_RET_OK
1241        }
1242        Err(e) => ret_from_error(&e),
1243    }
1244}
1245
1246// ============================================================================
1247// Trampolines — service server
1248// ============================================================================
1249
1250unsafe extern "C" fn create_service_trampoline<R: RustBackend>(
1251    node: *const NrosRmwNode,
1252    // phase-406 W1 — the identity arrives as ONE argument, in upstream's
1253    // position. Unpacked immediately below so the body is unchanged.
1254    type_support: *const crate::generated::rmw_service_type_support_t,
1255    service_name: *const core::ffi::c_char,
1256    domain_id: u32,
1257    qos: *const NrosRmwQos,
1258    out: *mut NrosRmwService,
1259) -> NrosRmwRet {
1260    // phase-406 W1 — unpack once, so the body below is untouched. A NULL
1261    // `type_support` is INVALID_ARGUMENT rather than a silent empty type: the
1262    // identity is what the backend keys its entity on, and an entity created
1263    // with no type is a subscription that matches nothing and reports nothing.
1264    let Some(ts) = (unsafe { type_support.as_ref() }) else {
1265        return NROS_RMW_RET_INVALID_ARGUMENT;
1266    };
1267    let (type_name, type_hash) = (ts.type_name, ts.type_hash);
1268    if out.is_null() || qos.is_null() {
1269        return NROS_RMW_RET_INVALID_ARGUMENT;
1270    }
1271    // Phase 376 W5/B1 — the node carries both halves now: its own identity, and
1272    // the route to its session (upstream's `context`). Reading the name off the
1273    // session was the `entity_view` fabrication, and it is gone.
1274    if node.is_null() {
1275        return NROS_RMW_RET_INVALID_ARGUMENT;
1276    }
1277    let session = unsafe { (*node).session };
1278    let Some(s) = (unsafe { session_mut::<R::Session>(session) }) else {
1279        return NROS_RMW_RET_INVALID_ARGUMENT;
1280    };
1281    let node_name = unsafe { node_name_of(node) };
1282    let namespace = unsafe { node_namespace_of(node) };
1283    let info = nros_rmw::ServiceInfo {
1284        name: unsafe { cstr_to_str(service_name) },
1285        type_name: unsafe { cstr_to_str(type_name) },
1286        type_hash: unsafe { cstr_to_str(type_hash) },
1287        domain_id,
1288        node_name,
1289        namespace,
1290    };
1291    let qos_settings = qos_from_cffi(unsafe { &*qos });
1292    match Session::create_service(s, &info, qos_settings) {
1293        Ok(server) => match box_handle(
1294            "service server",
1295            Hosted::new(server, |t: &R::Service, w| {
1296                ServiceTrait::register_waker(t, w)
1297            }),
1298        ) {
1299            Ok(boxed) => {
1300                unsafe {
1301                    (*out).backend_data = boxed;
1302                }
1303                NROS_RMW_RET_OK
1304            }
1305            // Dropping the handle undeclares the entity the backend just made.
1306            Err(server) => {
1307                drop(server);
1308                NROS_RMW_RET_BAD_ALLOC
1309            }
1310        },
1311        Err(e) => ret_from_error(&e),
1312    }
1313}
1314
1315unsafe extern "C" fn destroy_service_trampoline<R: RustBackend>(
1316    server: *mut NrosRmwService,
1317) -> NrosRmwRet {
1318    let _ = unsafe { take_box::<Hosted<R::Service>>(service_data_mut(server)) };
1319
1320    NROS_RMW_RET_OK
1321}
1322
1323unsafe extern "C" fn take_request_trampoline<R: RustBackend>(
1324    server: *const NrosRmwService,
1325    request: *mut crate::generated::rmw_mut_byte_span_t,
1326    seq_out: *mut i64,
1327    taken: *mut bool,
1328) -> NrosRmwRet {
1329    // Phase 376 W3.b/W3.d step A — upstream `rmw_take_request`'s shape;
1330    // phase-406 W2 — the byte range is one span, and `written` lands in it.
1331    if request.is_null() || taken.is_null() {
1332        return NROS_RMW_RET_INVALID_ARGUMENT;
1333    }
1334    let Some(s) = (unsafe { service_mut::<R::Service>(server) }) else {
1335        return NROS_RMW_RET_INVALID_ARGUMENT;
1336    };
1337    let (buf, buf_len) = unsafe { ((*request).data, (*request).capacity) };
1338    let out_len = unsafe { &raw mut (*request).len };
1339    if (buf.is_null() && buf_len != 0) || seq_out.is_null() {
1340        return NROS_RMW_RET_INVALID_ARGUMENT;
1341    }
1342    let slice = unsafe { core::slice::from_raw_parts_mut(buf, buf_len) };
1343    let buf_start = slice.as_ptr() as usize;
1344    match ServiceTrait::take_request(s, slice) {
1345        Ok(Some(req)) => {
1346            // The handle's `data` slice borrows from `buf`. Use its
1347            // offset within the caller's buffer to compute the payload
1348            // length; some backends prepend an envelope header so the
1349            // payload doesn't start at offset 0.
1350            let offset = (req.data.as_ptr() as usize).saturating_sub(buf_start);
1351            let len = req.data.len();
1352            unsafe {
1353                *seq_out = req.sequence_number;
1354            }
1355            // Issue 1942 — the C slot has nowhere for the per-request
1356            // metadata, so it rides the subscription `MessageInfo` side
1357            // channel (`store_cffi_service_info`).
1358            let key = unsafe { (*server).backend_data as usize };
1359            crate::store_cffi_service_info(key, ServiceTrait::request_info(s));
1360            // Move the payload to the start of the buffer so the C
1361            // caller sees a `(buf, len)` pair starting at offset 0,
1362            // matching the cyclonedds backend's contract.
1363            if offset != 0 {
1364                let total = offset + len;
1365                // SAFETY: `slice[..total]` is initialised by the
1366                // backend; copy_within respects overlapping ranges.
1367                slice.copy_within(offset..total, 0);
1368            }
1369            // SAFETY: both checked non-null above.
1370            unsafe {
1371                *out_len = len;
1372                *taken = true;
1373            }
1374            NROS_RMW_RET_OK
1375        }
1376        Ok(None) => {
1377            // SAFETY: checked non-null above.
1378            unsafe { *taken = false };
1379            NROS_RMW_RET_OK
1380        }
1381        Err(e) => refused_ret(&e, ServiceTrait::refused_request_len(s), out_len),
1382    }
1383}
1384
1385unsafe extern "C" fn has_request_trampoline<R: RustBackend>(
1386    server: *mut NrosRmwService,
1387    out_has_request: *mut bool,
1388) -> NrosRmwRet {
1389    // Phase 376 W3.d step A — see `has_data_trampoline`.
1390    if out_has_request.is_null() {
1391        return NROS_RMW_RET_INVALID_ARGUMENT;
1392    }
1393    let Some(s) = (unsafe { service_ref::<R::Service>(server) }) else {
1394        return NROS_RMW_RET_INVALID_ARGUMENT;
1395    };
1396    // SAFETY: checked non-null above.
1397    unsafe { *out_has_request = ServiceTrait::has_request(s) };
1398    NROS_RMW_RET_OK
1399}
1400
1401unsafe extern "C" fn send_response_trampoline<R: RustBackend>(
1402    server: *const NrosRmwService,
1403    seq: i64,
1404    response: crate::generated::rmw_byte_span_t,
1405) -> NrosRmwRet {
1406    let Some(s) = (unsafe { service_mut::<R::Service>(server) }) else {
1407        return NROS_RMW_RET_INVALID_ARGUMENT;
1408    };
1409    let (data, len) = (response.data, response.len);
1410    if data.is_null() && len != 0 {
1411        return NROS_RMW_RET_INVALID_ARGUMENT;
1412    }
1413    let slice = unsafe { core::slice::from_raw_parts(data, len) };
1414    match ServiceTrait::send_response(s, seq, slice) {
1415        Ok(()) => NROS_RMW_RET_OK,
1416        Err(e) => ret_from_error(&e),
1417    }
1418}
1419
1420// ============================================================================
1421// Trampolines — service client
1422// ============================================================================
1423
1424unsafe extern "C" fn create_client_trampoline<R: RustBackend>(
1425    node: *const NrosRmwNode,
1426    // phase-406 W1 — the identity arrives as ONE argument, in upstream's
1427    // position. Unpacked immediately below so the body is unchanged.
1428    type_support: *const crate::generated::rmw_service_type_support_t,
1429    service_name: *const core::ffi::c_char,
1430    domain_id: u32,
1431    qos: *const NrosRmwQos,
1432    out: *mut NrosRmwClient,
1433) -> NrosRmwRet {
1434    // phase-406 W1 — unpack once, so the body below is untouched. A NULL
1435    // `type_support` is INVALID_ARGUMENT rather than a silent empty type: the
1436    // identity is what the backend keys its entity on, and an entity created
1437    // with no type is a subscription that matches nothing and reports nothing.
1438    let Some(ts) = (unsafe { type_support.as_ref() }) else {
1439        return NROS_RMW_RET_INVALID_ARGUMENT;
1440    };
1441    let (type_name, type_hash) = (ts.type_name, ts.type_hash);
1442    if out.is_null() || qos.is_null() {
1443        return NROS_RMW_RET_INVALID_ARGUMENT;
1444    }
1445    // Phase 376 W5/B1 — the node carries both halves now: its own identity, and
1446    // the route to its session (upstream's `context`). Reading the name off the
1447    // session was the `entity_view` fabrication, and it is gone.
1448    if node.is_null() {
1449        return NROS_RMW_RET_INVALID_ARGUMENT;
1450    }
1451    let session = unsafe { (*node).session };
1452    let Some(s) = (unsafe { session_mut::<R::Session>(session) }) else {
1453        return NROS_RMW_RET_INVALID_ARGUMENT;
1454    };
1455    let node_name = unsafe { node_name_of(node) };
1456    let namespace = unsafe { node_namespace_of(node) };
1457    let info = nros_rmw::ServiceInfo {
1458        name: unsafe { cstr_to_str(service_name) },
1459        type_name: unsafe { cstr_to_str(type_name) },
1460        type_hash: unsafe { cstr_to_str(type_hash) },
1461        domain_id,
1462        node_name,
1463        namespace,
1464    };
1465    let qos_settings = qos_from_cffi(unsafe { &*qos });
1466    match Session::create_client(s, &info, qos_settings) {
1467        Ok(client) => match box_handle(
1468            "service client",
1469            Hosted::new(client, |t: &R::Client, w| ClientTrait::register_waker(t, w)),
1470        ) {
1471            Ok(boxed) => {
1472                unsafe {
1473                    (*out).backend_data = boxed;
1474                }
1475                NROS_RMW_RET_OK
1476            }
1477            // Dropping the handle undeclares the entity the backend just made.
1478            Err(client) => {
1479                drop(client);
1480                NROS_RMW_RET_BAD_ALLOC
1481            }
1482        },
1483        Err(e) => ret_from_error(&e),
1484    }
1485}
1486
1487unsafe extern "C" fn destroy_client_trampoline<R: RustBackend>(
1488    client: *mut NrosRmwClient,
1489) -> NrosRmwRet {
1490    let _ = unsafe { take_box::<Hosted<R::Client>>(client_data_mut(client)) };
1491
1492    NROS_RMW_RET_OK
1493}
1494
1495// Non-blocking send/recv trampolines — the one request/reply path
1496// (phase-301: the blocking `call_raw` slot is deleted).
1497unsafe extern "C" fn send_request_trampoline<R: RustBackend>(
1498    client: *const NrosRmwClient,
1499    request: crate::generated::rmw_byte_span_t,
1500    sequence_id: *mut i64,
1501) -> NrosRmwRet {
1502    let Some(c) = (unsafe { client_mut::<R::Client>(client) }) else {
1503        return NROS_RMW_RET_INVALID_ARGUMENT;
1504    };
1505    let (request, req_len) = (request.data, request.len);
1506    if request.is_null() && req_len != 0 {
1507        return NROS_RMW_RET_INVALID_ARGUMENT;
1508    }
1509    let req = unsafe { core::slice::from_raw_parts(request, req_len) };
1510    match ClientTrait::send_request_raw(c, req) {
1511        Ok(seq) => {
1512            // Issue 0778 — hand the id back. NULL is tolerated for a caller
1513            // that genuinely has one call in flight and does not want it.
1514            if !sequence_id.is_null() {
1515                // SAFETY: checked non-null.
1516                unsafe { *sequence_id = seq };
1517            }
1518            NROS_RMW_RET_OK
1519        }
1520        Err(e) => ret_from_error(&e),
1521    }
1522}
1523
1524unsafe extern "C" fn take_response_trampoline<R: RustBackend>(
1525    client: *const NrosRmwClient,
1526    reply: *mut crate::generated::rmw_mut_byte_span_t,
1527    seq_out: *mut i64,
1528    taken: *mut bool,
1529) -> NrosRmwRet {
1530    unsafe { take_response_impl::<R>(client, reply, seq_out, taken, core::ptr::null_mut()) }
1531}
1532
1533/// Issue 2136 — `take_response` plus the reply's `rmw_service_info_t`, from
1534/// the backend's [`ClientTrait::response_info`] for the reply just taken.
1535unsafe extern "C" fn take_response_with_info_trampoline<R: RustBackend>(
1536    client: *const NrosRmwClient,
1537    reply: *mut crate::generated::rmw_mut_byte_span_t,
1538    seq_out: *mut i64,
1539    taken: *mut bool,
1540    service_info: *mut crate::generated::rmw_service_info_t,
1541) -> NrosRmwRet {
1542    if service_info.is_null() {
1543        return NROS_RMW_RET_INVALID_ARGUMENT;
1544    }
1545    unsafe { take_response_impl::<R>(client, reply, seq_out, taken, service_info) }
1546}
1547
1548/// Issue 2136 — [`nros_rmw::ServiceRequestInfo`] → `rmw_service_info_t`, the
1549/// inverse of `service_info_from_raw`: `None` becomes the ABI's zero.
1550fn service_info_to_raw(
1551    info: &nros_rmw::ServiceRequestInfo,
1552    out: &mut crate::generated::rmw_service_info_t,
1553) {
1554    out.source_timestamp = info.source_timestamp.map_or(0, |t| t.to_nanos());
1555    out.received_timestamp = info.received_timestamp.map_or(0, |t| t.to_nanos());
1556    match info.request_id {
1557        Some(id) => {
1558            for (dst, src) in out
1559                .request_id
1560                .writer_guid
1561                .iter_mut()
1562                .zip(id.writer_guid.iter())
1563            {
1564                *dst = *src as i8;
1565            }
1566            out.request_id.sequence_number = id.sequence_number;
1567        }
1568        None => {
1569            out.request_id.writer_guid = [0; 16];
1570            out.request_id.sequence_number = 0;
1571        }
1572    }
1573}
1574
1575unsafe fn take_response_impl<R: RustBackend>(
1576    client: *const NrosRmwClient,
1577    reply: *mut crate::generated::rmw_mut_byte_span_t,
1578    seq_out: *mut i64,
1579    taken: *mut bool,
1580    service_info: *mut crate::generated::rmw_service_info_t,
1581) -> NrosRmwRet {
1582    // Phase 376 W3.b/W3.d step A — upstream `rmw_take_response`'s shape;
1583    // phase-406 W2 — one span in place of the buf/len/out_len triple.
1584    if reply.is_null() || taken.is_null() {
1585        return NROS_RMW_RET_INVALID_ARGUMENT;
1586    }
1587    let Some(c) = (unsafe { client_mut::<R::Client>(client) }) else {
1588        return NROS_RMW_RET_INVALID_ARGUMENT;
1589    };
1590    let (reply_buf, reply_buf_len) = unsafe { ((*reply).data, (*reply).capacity) };
1591    let out_len = unsafe { &raw mut (*reply).len };
1592    if reply_buf.is_null() && reply_buf_len != 0 {
1593        return NROS_RMW_RET_INVALID_ARGUMENT;
1594    }
1595    let reply = unsafe { core::slice::from_raw_parts_mut(reply_buf, reply_buf_len) };
1596    match ClientTrait::take_response_raw(c, reply) {
1597        Ok(Some((n, seq))) => {
1598            // SAFETY: both checked non-null above.
1599            unsafe {
1600                *out_len = n;
1601                *taken = true;
1602                if !seq_out.is_null() {
1603                    *seq_out = seq;
1604                }
1605            }
1606            if !service_info.is_null() {
1607                // SAFETY: checked non-null; caller-owned for the call.
1608                service_info_to_raw(&ClientTrait::response_info(c), unsafe {
1609                    &mut *service_info
1610                });
1611            }
1612            NROS_RMW_RET_OK
1613        }
1614        Ok(None) => {
1615            // SAFETY: checked non-null above.
1616            unsafe { *taken = false };
1617            NROS_RMW_RET_OK
1618        }
1619        Err(e) => refused_ret(&e, ClientTrait::refused_response_len(c), out_len),
1620    }
1621}
1622
1623// ============================================================================
1624// Event-callback bridge (Phase 115.L.0.events)
1625// ============================================================================
1626//
1627// Events / liveliness / deadline are wired through the vtable (see the
1628// `subscription_event_init` / `publisher_event_init` /
1629// `publisher_assert_liveliness` / `next_deadline_ms` trampolines registered
1630// above) — the stale "TODO: wire through" header was removed (Phase 192.9).
1631//
1632// `NrosRmwEventCallback` (cffi shape) and `nros_rmw::EventCallback`
1633// (trait shape) have *layout-identical* arguments:
1634//
1635//   * NrosRmwEventKind  ↔ EventKind     — both `#[repr(u8)]`, same variant ids.
1636//   * *const NrosRmwEventPayload ↔ *const c_void  — fn-ptr-width pointer; the
1637//     trait callback dereferences as `*const LivelinessChangedStatus` /
1638//     `*const CountStatus`, the cffi callback dereferences as the matching
1639//     union member. The field-by-field layout matches today (see the
1640//     `const _: () = { … }` size/align asserts below).
1641//   * *mut c_void       ↔ *mut c_void   — identical.
1642//
1643// Therefore the cffi callback can be transmuted into a trait callback
1644// pointer; the receiving Rust trait code calls it via the trait
1645// signature, but the bytes on the wire are the cffi struct.
1646
1647const _: () = {
1648    use core::mem::{align_of, size_of};
1649    assert!(
1650        size_of::<crate::NrosRmwLivelinessChangedStatus>()
1651            == size_of::<nros_rmw::LivelinessChangedStatus>()
1652    );
1653    assert!(
1654        align_of::<crate::NrosRmwLivelinessChangedStatus>()
1655            == align_of::<nros_rmw::LivelinessChangedStatus>()
1656    );
1657    assert!(size_of::<crate::NrosRmwCountStatus>() == size_of::<nros_rmw::CountStatus>());
1658    assert!(align_of::<crate::NrosRmwCountStatus>() == align_of::<nros_rmw::CountStatus>());
1659    // Issue 1972 — the matched and incompatible-type payloads.
1660    assert!(
1661        size_of::<crate::generated::rmw_matched_status_t>() == size_of::<nros_rmw::MatchedStatus>()
1662    );
1663    assert!(
1664        align_of::<crate::generated::rmw_matched_status_t>()
1665            == align_of::<nros_rmw::MatchedStatus>()
1666    );
1667    assert!(
1668        size_of::<crate::generated::rmw_incompatible_type_status_t>()
1669            == size_of::<nros_rmw::IncompatibleTypeStatus>()
1670    );
1671    assert!(
1672        align_of::<crate::generated::rmw_incompatible_type_status_t>()
1673            == align_of::<nros_rmw::IncompatibleTypeStatus>()
1674    );
1675    // EventKind tags must round-trip 0..=8 between the generated C
1676    // discriminants and the `#[repr(u8)]` trait enum.
1677    use crate::rmw_event_type_t as ck;
1678    assert!(
1679        ck::NROS_RMW_EVENT_LIVELINESS_CHANGED == nros_rmw::EventKind::LivelinessChanged as ck::Type
1680    );
1681    assert!(
1682        ck::NROS_RMW_EVENT_REQUESTED_DEADLINE_MISSED
1683            == nros_rmw::EventKind::RequestedDeadlineMissed as ck::Type
1684    );
1685    assert!(ck::NROS_RMW_EVENT_MESSAGE_LOST == nros_rmw::EventKind::MessageLost as ck::Type);
1686    assert!(ck::NROS_RMW_EVENT_LIVELINESS_LOST == nros_rmw::EventKind::LivelinessLost as ck::Type);
1687    assert!(
1688        ck::NROS_RMW_EVENT_OFFERED_DEADLINE_MISSED
1689            == nros_rmw::EventKind::OfferedDeadlineMissed as ck::Type
1690    );
1691    assert!(
1692        ck::NROS_RMW_EVENT_PUBLICATION_MATCHED
1693            == nros_rmw::EventKind::PublicationMatched as ck::Type
1694    );
1695    assert!(
1696        ck::NROS_RMW_EVENT_SUBSCRIPTION_MATCHED
1697            == nros_rmw::EventKind::SubscriptionMatched as ck::Type
1698    );
1699    assert!(
1700        ck::NROS_RMW_EVENT_PUBLISHER_INCOMPATIBLE_TYPE
1701            == nros_rmw::EventKind::PublisherIncompatibleType as ck::Type
1702    );
1703    assert!(
1704        ck::NROS_RMW_EVENT_SUBSCRIPTION_INCOMPATIBLE_TYPE
1705            == nros_rmw::EventKind::SubscriptionIncompatibleType as ck::Type
1706    );
1707};
1708
1709unsafe extern "C" fn subscription_event_init_trampoline<R: RustBackend>(
1710    subscriber: *const NrosRmwSubscription,
1711    kind: NrosRmwEventKind,
1712    deadline_ms: u32,
1713    cb: NrosRmwEventCallback,
1714    user_context: *mut c_void,
1715) -> NrosRmwRet {
1716    let Some(s) = (unsafe { subscription_mut::<R::Subscription>(subscriber) }) else {
1717        return NROS_RMW_RET_INVALID_ARGUMENT;
1718    };
1719    // Explicit conversion, not a transmute: `NrosRmwEventKind` is
1720    // C-uint-sized (#238) while trait `EventKind` is `#[repr(u8)]` —
1721    // reinterpreting bytes would be UB.
1722    let trait_kind: nros_rmw::EventKind = event_kind_from_c(kind);
1723    // The generated callback type is nullable; a NULL callback can't be
1724    // forwarded into the trait's non-null callback surface.
1725    let Some(cb) = cb else {
1726        return NROS_RMW_RET_INVALID_ARGUMENT;
1727    };
1728    // SAFETY: see module-level note. `cb`'s parameter types
1729    // `(NrosRmwEventKind, *const NrosRmwEventPayload, *mut c_void)` are
1730    // ABI-compatible with the trait's `(EventKind, *const c_void, *mut c_void)`.
1731    let trait_cb: nros_rmw::EventCallback = unsafe { core::mem::transmute(cb) };
1732    let res = unsafe {
1733        Subscription::register_event_callback(s, trait_kind, deadline_ms, trait_cb, user_context)
1734    };
1735    match res {
1736        Ok(()) => NROS_RMW_RET_OK,
1737        Err(e) => {
1738            // Backend's `Unsupported` mapping for events is its
1739            // `serialization_error()` by trait-doc default; the cffi
1740            // contract for "this event kind unsupported" is its own
1741            // ret code, so map any error here to UNSUPPORTED rather
1742            // than e.g. SERIALIZATION_ERROR — the caller's mental
1743            // model is "vtable said no events," not "marshalling
1744            // failed."
1745            let _ = e;
1746            NROS_RMW_RET_UNSUPPORTED
1747        }
1748    }
1749}
1750
1751unsafe extern "C" fn publisher_event_init_trampoline<R: RustBackend>(
1752    publisher: *const NrosRmwPublisher,
1753    kind: NrosRmwEventKind,
1754    deadline_ms: u32,
1755    cb: NrosRmwEventCallback,
1756    user_context: *mut c_void,
1757) -> NrosRmwRet {
1758    // Publisher::register_event_callback takes `&mut self`. Need a
1759    // mut-ptr to the boxed handle.
1760    if publisher.is_null() {
1761        return NROS_RMW_RET_INVALID_ARGUMENT;
1762    }
1763    let p_ptr = unsafe { (*publisher).backend_data } as *mut R::Publisher;
1764    if p_ptr.is_null() {
1765        return NROS_RMW_RET_INVALID_ARGUMENT;
1766    }
1767    let p = unsafe { &mut *p_ptr };
1768    // Explicit conversion, not a transmute (see subscriber trampoline / #238).
1769    let trait_kind: nros_rmw::EventKind = event_kind_from_c(kind);
1770    let Some(cb) = cb else {
1771        return NROS_RMW_RET_INVALID_ARGUMENT;
1772    };
1773    let trait_cb: nros_rmw::EventCallback = unsafe { core::mem::transmute(cb) };
1774    let res = unsafe {
1775        Publisher::register_event_callback(p, trait_kind, deadline_ms, trait_cb, user_context)
1776    };
1777    match res {
1778        Ok(()) => NROS_RMW_RET_OK,
1779        Err(_) => NROS_RMW_RET_UNSUPPORTED,
1780    }
1781}
1782
1783// --------------------------------------------------------------------------
1784// Granted-QoS read-back — the six slots
1785// --------------------------------------------------------------------------
1786//
1787// A Rust backend answers these from its `actual_qos` family, whose trait
1788// default is `QOS_PROFILE_UNKNOWN`. Installed UNCONDITIONALLY for every `R`,
1789// on the same reasoning as `supported_qos_policies`: the default answer is
1790// every policy an absence, which is exactly what a NULL slot already means to
1791// the caller (`read_granted_qos` stores `qos_unknown_c()` for one), so
1792// installing them for a backend that determines nothing changes nothing.
1793//
1794// The lowering is `NrosRmwQos::try_from`, whose one failure is a depth past
1795// `u16::MAX`. A backend cannot GRANT a depth it could not have been asked for
1796// — the request came through the same conversion — so this is a real
1797// `NROS_RMW_RET_ERROR` rather than a case to paper over.
1798
1799/// Write one Rust-side profile into a C read-back out-parameter.
1800///
1801/// # Safety
1802/// `out` must be non-null and point at writable `rmw_qos_profile_t` storage.
1803unsafe fn write_actual_qos(out: *mut NrosRmwQos, qos: QoSProfile) -> NrosRmwRet {
1804    let Ok(lowered) = NrosRmwQos::try_from(qos) else {
1805        return crate::NROS_RMW_RET_ERROR;
1806    };
1807    // SAFETY: the caller's contract.
1808    unsafe { *out = lowered };
1809    NROS_RMW_RET_OK
1810}
1811
1812unsafe extern "C" fn publisher_get_actual_qos_trampoline<R: RustBackend>(
1813    publisher: *const NrosRmwPublisher,
1814    out: *mut NrosRmwQos,
1815) -> NrosRmwRet {
1816    if out.is_null() {
1817        return NROS_RMW_RET_INVALID_ARGUMENT;
1818    }
1819    let Some(p) = (unsafe { publisher_ref::<R::Publisher>(publisher) }) else {
1820        return NROS_RMW_RET_INVALID_ARGUMENT;
1821    };
1822    // SAFETY: checked non-null above.
1823    unsafe { write_actual_qos(out, Publisher::actual_qos(p)) }
1824}
1825
1826unsafe extern "C" fn subscription_get_actual_qos_trampoline<R: RustBackend>(
1827    subscription: *const NrosRmwSubscription,
1828    out: *mut NrosRmwQos,
1829) -> NrosRmwRet {
1830    if out.is_null() {
1831        return NROS_RMW_RET_INVALID_ARGUMENT;
1832    }
1833    let Some(sub) = (unsafe { subscription_ref::<R::Subscription>(subscription) }) else {
1834        return NROS_RMW_RET_INVALID_ARGUMENT;
1835    };
1836    // SAFETY: checked non-null above.
1837    unsafe { write_actual_qos(out, Subscription::actual_qos(sub)) }
1838}
1839
1840unsafe extern "C" fn client_request_publisher_get_actual_qos_trampoline<R: RustBackend>(
1841    client: *const NrosRmwClient,
1842    out: *mut NrosRmwQos,
1843) -> NrosRmwRet {
1844    if out.is_null() {
1845        return NROS_RMW_RET_INVALID_ARGUMENT;
1846    }
1847    let Some(c) = (unsafe { client_mut::<R::Client>(client) }) else {
1848        return NROS_RMW_RET_INVALID_ARGUMENT;
1849    };
1850    // SAFETY: checked non-null above.
1851    unsafe { write_actual_qos(out, ClientTrait::request_publisher_actual_qos(c)) }
1852}
1853
1854unsafe extern "C" fn client_response_subscription_get_actual_qos_trampoline<R: RustBackend>(
1855    client: *const NrosRmwClient,
1856    out: *mut NrosRmwQos,
1857) -> NrosRmwRet {
1858    if out.is_null() {
1859        return NROS_RMW_RET_INVALID_ARGUMENT;
1860    }
1861    let Some(c) = (unsafe { client_mut::<R::Client>(client) }) else {
1862        return NROS_RMW_RET_INVALID_ARGUMENT;
1863    };
1864    // SAFETY: checked non-null above.
1865    unsafe { write_actual_qos(out, ClientTrait::response_subscription_actual_qos(c)) }
1866}
1867
1868unsafe extern "C" fn service_request_subscription_get_actual_qos_trampoline<R: RustBackend>(
1869    service: *const NrosRmwService,
1870    out: *mut NrosRmwQos,
1871) -> NrosRmwRet {
1872    if out.is_null() {
1873        return NROS_RMW_RET_INVALID_ARGUMENT;
1874    }
1875    let Some(sv) = (unsafe { service_ref::<R::Service>(service) }) else {
1876        return NROS_RMW_RET_INVALID_ARGUMENT;
1877    };
1878    // SAFETY: checked non-null above.
1879    unsafe { write_actual_qos(out, ServiceTrait::request_subscription_actual_qos(sv)) }
1880}
1881
1882unsafe extern "C" fn service_response_publisher_get_actual_qos_trampoline<R: RustBackend>(
1883    service: *const NrosRmwService,
1884    out: *mut NrosRmwQos,
1885) -> NrosRmwRet {
1886    if out.is_null() {
1887        return NROS_RMW_RET_INVALID_ARGUMENT;
1888    }
1889    let Some(sv) = (unsafe { service_ref::<R::Service>(service) }) else {
1890        return NROS_RMW_RET_INVALID_ARGUMENT;
1891    };
1892    // SAFETY: checked non-null above.
1893    unsafe { write_actual_qos(out, ServiceTrait::response_publisher_actual_qos(sv)) }
1894}
1895
1896unsafe extern "C" fn publisher_assert_liveliness_trampoline<R: RustBackend>(
1897    publisher: *const NrosRmwPublisher,
1898) -> NrosRmwRet {
1899    let Some(p) = (unsafe { publisher_ref::<R::Publisher>(publisher) }) else {
1900        return NROS_RMW_RET_INVALID_ARGUMENT;
1901    };
1902    match Publisher::assert_liveliness(p) {
1903        Ok(()) => NROS_RMW_RET_OK,
1904        Err(e) => ret_from_error(&e),
1905    }
1906}
1907
1908/// Upstream `rmw_get_gid_for_publisher` — the phase-467 RMW gap-closure
1909/// design study's Q1.
1910///
1911/// This slot was NULL on the Rust adapter table until that study, so a Rust
1912/// backend with a real publisher identity — zenoh has one, the gid it stamps
1913/// into every outgoing attachment — could not report it through any language
1914/// surface. `Publisher::get_gid`'s default is `Unsupported`, so a backend
1915/// that does not override it still answers honestly here.
1916///
1917/// Only `data` is written. `implementation_identifier` is left as the caller
1918/// found it: `CffiPublisher::get_gid` discards it (the identifier a caller
1919/// needs is the one on the session it asked), and writing a pointer the
1920/// trampoline would have to source from a second slot is a second place for
1921/// it to disagree.
1922unsafe extern "C" fn get_gid_for_publisher_trampoline<R: RustBackend>(
1923    publisher: *const NrosRmwPublisher,
1924    gid: *mut NrosRmwGid,
1925) -> NrosRmwRet {
1926    if gid.is_null() {
1927        return NROS_RMW_RET_INVALID_ARGUMENT;
1928    }
1929    let Some(p) = (unsafe { publisher_ref::<R::Publisher>(publisher) }) else {
1930        return NROS_RMW_RET_INVALID_ARGUMENT;
1931    };
1932    match Publisher::get_gid(p) {
1933        Ok(bytes) => {
1934            // SAFETY: checked non-null above; the caller owns the storage.
1935            unsafe {
1936                (*gid).data = bytes;
1937            }
1938            NROS_RMW_RET_OK
1939        }
1940        Err(e) => ret_from_error(&e),
1941    }
1942}
1943
1944unsafe extern "C" fn next_deadline_ms_trampoline<R: RustBackend>(
1945    session: *const NrosRmwSession,
1946    out_ms: *mut u32,
1947    has_deadline: *mut bool,
1948) -> NrosRmwRet {
1949    // Phase 376 W3.d step A — value and presence out, status returned.
1950    if out_ms.is_null() || has_deadline.is_null() {
1951        return NROS_RMW_RET_INVALID_ARGUMENT;
1952    }
1953    // A NULL session was previously reported as `-1`, indistinguishable from a
1954    // link with nothing scheduled. It is an invalid argument.
1955    let Some(s) = (unsafe { session_ref::<R::Session>(session) }) else {
1956        return NROS_RMW_RET_INVALID_ARGUMENT;
1957    };
1958    match Session::next_deadline_ms(s) {
1959        Some(ms) => {
1960            // SAFETY: both checked non-null above.
1961            unsafe {
1962                *out_ms = ms;
1963                *has_deadline = true;
1964            }
1965        }
1966        None => {
1967            // SAFETY: checked non-null above.
1968            unsafe { *has_deadline = false };
1969        }
1970    }
1971    NROS_RMW_RET_OK
1972}
1973
1974unsafe extern "C" fn set_wake_callback_trampoline<R: RustBackend>(
1975    session: *mut NrosRmwSession,
1976    cb: Option<unsafe extern "C" fn(ctx: *mut core::ffi::c_void)>,
1977    ctx: *mut core::ffi::c_void,
1978) -> NrosRmwRet {
1979    let Some(s) = (unsafe { session_mut::<R::Session>(session) }) else {
1980        return NROS_RMW_RET_INVALID_ARGUMENT;
1981    };
1982    // Phase 124.B.1 — delegate to the Rust backend. Default trait
1983    // body ignores; concrete backends opt in.
1984    // SAFETY: this trampoline forwards the C ABI's callback/context
1985    // lifetime contract to the Rust backend.
1986    unsafe { Session::set_wake_callback(s, cb, ctx) };
1987    NROS_RMW_RET_OK
1988}
1989
1990unsafe extern "C" fn readiness_source_trampoline<R: RustBackend>(
1991    session: *const NrosRmwSession,
1992    out_kind: *mut u8,
1993    out_handle: *mut i32,
1994) -> NrosRmwRet {
1995    if out_kind.is_null() || out_handle.is_null() {
1996        return NROS_RMW_RET_INVALID_ARGUMENT;
1997    }
1998    let Some(s) = (unsafe { session_ref::<R::Session>(session) }) else {
1999        return NROS_RMW_RET_INVALID_ARGUMENT;
2000    };
2001    let (kind, handle) = match Session::readiness_source(s) {
2002        nros_rmw::ReadinessSource::None => (crate::generated::NROS_RMW_READINESS_NONE, -1),
2003        nros_rmw::ReadinessSource::Notify => (crate::generated::NROS_RMW_READINESS_NOTIFY, -1),
2004        nros_rmw::ReadinessSource::Waitable(fd) => {
2005            (crate::generated::NROS_RMW_READINESS_WAITABLE, fd)
2006        }
2007    };
2008    // SAFETY: both checked non-null above.
2009    unsafe {
2010        *out_kind = kind as u8;
2011        *out_handle = handle;
2012    }
2013    NROS_RMW_RET_OK
2014}
2015
2016/// Issue 1940 — the bytes the adapter allocates for a subscription, service
2017/// or client handle of type `T`: the handle plus its wake registration
2018/// (`crate::entity_wake::Hosted`). For a test that refuses that allocation,
2019/// such as `tests/adapter_oom.rs`, which must not re-derive the wrapper's
2020/// layout.
2021#[doc(hidden)]
2022pub const fn entity_box_size<T>() -> usize {
2023    core::mem::size_of::<Hosted<T>>()
2024}
2025
2026// Issue 1940 — the per-entity new-data callback slots. The registration lives in the
2027// entity's `Hosted` wrapper; see `crate::entity_wake`.
2028unsafe extern "C" fn subscription_set_on_new_message_callback_trampoline<R: RustBackend>(
2029    subscription: *mut NrosRmwSubscription,
2030    cb: Option<EntityWakeCb>,
2031    user_data: *const c_void,
2032) -> NrosRmwRet {
2033    if subscription.is_null() {
2034        return NROS_RMW_RET_INVALID_ARGUMENT;
2035    }
2036    let data = unsafe { *subscription_data_mut(subscription) };
2037    unsafe { Hosted::<R::Subscription>::set_wake(data, cb, user_data) }
2038}
2039
2040unsafe extern "C" fn service_set_on_new_request_callback_trampoline<R: RustBackend>(
2041    service: *mut NrosRmwService,
2042    cb: Option<EntityWakeCb>,
2043    user_data: *const c_void,
2044) -> NrosRmwRet {
2045    if service.is_null() {
2046        return NROS_RMW_RET_INVALID_ARGUMENT;
2047    }
2048    let data = unsafe { *service_data_mut(service) };
2049    unsafe { Hosted::<R::Service>::set_wake(data, cb, user_data) }
2050}
2051
2052unsafe extern "C" fn client_set_on_new_response_callback_trampoline<R: RustBackend>(
2053    client: *mut NrosRmwClient,
2054    cb: Option<EntityWakeCb>,
2055    user_data: *const c_void,
2056) -> NrosRmwRet {
2057    if client.is_null() {
2058        return NROS_RMW_RET_INVALID_ARGUMENT;
2059    }
2060    let data = unsafe { *client_data_mut(client) };
2061    unsafe { Hosted::<R::Client>::set_wake(data, cb, user_data) }
2062}
2063
2064/// phase-467 Row 8 — the graph-CHANGE edge for every Rust backend.
2065///
2066/// Unlike `set_wake_callback_trampoline` above, the STATUS is carried: a
2067/// backend with no graph must be able to say `UNSUPPORTED`, because the
2068/// runtime probes with a `NULL` clear before it consumes an executor handle
2069/// slot, and a trampoline that always answered `OK` would hand back a guard
2070/// condition nothing can trigger. That is precisely the silent difference this
2071/// row exists to remove.
2072unsafe extern "C" fn node_get_graph_guard_condition_trampoline<R: RustBackend>(
2073    session: *mut NrosRmwSession,
2074    callback: crate::generated::rmw_event_callback_t,
2075    user_data: *const core::ffi::c_void,
2076) -> NrosRmwRet {
2077    let Some(s) = (unsafe { session_mut::<R::Session>(session) }) else {
2078        return NROS_RMW_RET_INVALID_ARGUMENT;
2079    };
2080    // SAFETY: forwards the C ABI's callback/user_data lifetime contract to the
2081    // Rust backend; the installer owes the matching clear.
2082    match unsafe { Session::set_graph_change_callback(s, callback, user_data) } {
2083        Ok(()) => NROS_RMW_RET_OK,
2084        Err(e) => ret_from_error(&e),
2085    }
2086}
2087
2088unsafe extern "C" fn service_server_is_available_trampoline<R: RustBackend>(
2089    client: *const NrosRmwClient,
2090    out_available: *mut bool,
2091) -> NrosRmwRet {
2092    // Phase 124.C.1 — delegate to the Rust backend's
2093    // `ClientTrait::server_available` impl. Default trait
2094    // body returns `Err(TransportError::Unsupported)`; concrete
2095    // backends opt in by overriding.
2096    //
2097    // Phase 376 W3.d step A — status in the return, answer in the
2098    // out-parameter. `*out_available` is written ONLY on OK, so an
2099    // error leaves whatever the caller initialised it to; that is
2100    // upstream's contract and it is also what keeps a caller who
2101    // ignores the status from reading a stale `true` as fresh.
2102    if out_available.is_null() {
2103        return NROS_RMW_RET_INVALID_ARGUMENT;
2104    }
2105    let Some(c) = (unsafe { client_mut::<R::Client>(client) }) else {
2106        return NROS_RMW_RET_INVALID_ARGUMENT;
2107    };
2108    match ClientTrait::service_is_ready(c) {
2109        Ok(available) => {
2110            // SAFETY: checked non-null above; the caller owns a `bool`.
2111            unsafe { *out_available = available };
2112            NROS_RMW_RET_OK
2113        }
2114        Err(e) => ret_from_error(&e),
2115    }
2116}
2117
2118unsafe extern "C" fn ping_session_trampoline<R: RustBackend>(
2119    session: *mut NrosRmwSession,
2120    timeout_ms: i32,
2121) -> NrosRmwRet {
2122    // Phase 124.F.1 — delegate to the Rust backend's
2123    // `Session::ping_session` impl. Default trait body returns
2124    // `Err(TransportError::Unsupported)`; concrete backends opt in
2125    // by overriding.
2126    let Some(s) = (unsafe { session_mut::<R::Session>(session) }) else {
2127        return NROS_RMW_RET_INVALID_ARGUMENT;
2128    };
2129    match Session::ping_session(s, timeout_ms) {
2130        Ok(()) => NROS_RMW_RET_OK,
2131        Err(e) => ret_from_error(&e),
2132    }
2133}
2134
2135/// phase-381 W3 — bridge the C visitor to the Rust `Session::get_node_names`.
2136///
2137/// The C side hands a `rmw_node_visit_fn` plus an opaque `ctx`; the Rust trait
2138/// takes a closure. This wraps the former in the latter, and the strings it
2139/// passes are NUL-terminated on the stack because the C contract borrows them
2140/// for the call only — no allocation on either side of the seam.
2141///
2142/// A name that does not fit the stack buffer is SKIPPED rather than truncated:
2143/// a truncated node name is a different, plausible node.
2144unsafe extern "C" fn get_node_names_trampoline<R: RustBackend>(
2145    session: *const NrosRmwSession,
2146    // phase-406 W2 — the pair as one argument. The NODE visitor: its callback
2147    // takes an enclave, which the names-and-types one does not.
2148    visitor: crate::generated::rmw_node_visitor_t,
2149) -> NrosRmwRet {
2150    let (visit, ctx) = (visitor.visit, visitor.ctx);
2151    let Some(visit) = visit else {
2152        return NROS_RMW_RET_INVALID_ARGUMENT;
2153    };
2154    let Some(s) = (unsafe { session_mut::<R::Session>(session.cast_mut()) }) else {
2155        return NROS_RMW_RET_INVALID_ARGUMENT;
2156    };
2157
2158    /// Longest node name / namespace this seam will pass through.
2159    const NAME_MAX: usize = 256;
2160
2161    let mut cb = |name: &str, namespace: &str, enclave: Option<&str>| -> bool {
2162        let mut name_buf = [0u8; NAME_MAX];
2163        let mut ns_buf = [0u8; NAME_MAX];
2164        let mut enc_buf = [0u8; NAME_MAX];
2165        // `< NAME_MAX`, not `<= `: the buffer must hold the bytes AND the NUL
2166        // the C side reads to. Spelled as the strict compare clippy wants
2167        // (`int_plus_one`) rather than the `len + 1 <= cap` that says it.
2168        if name.len() >= NAME_MAX || namespace.len() >= NAME_MAX {
2169            return true; // skip this one, keep enumerating
2170        }
2171        name_buf[..name.len()].copy_from_slice(name.as_bytes());
2172        ns_buf[..namespace.len()].copy_from_slice(namespace.as_bytes());
2173        let enc_ptr = match enclave {
2174            Some(e) if e.len() < NAME_MAX => {
2175                enc_buf[..e.len()].copy_from_slice(e.as_bytes());
2176                enc_buf.as_ptr() as *const c_char
2177            }
2178            // NULL is the contract's "this backend does not track one", which
2179            // is what lets one slot answer both `rmw_get_node_names` and
2180            // `rmw_get_node_names_with_enclaves`.
2181            _ => core::ptr::null(),
2182        };
2183        unsafe {
2184            visit(
2185                ctx,
2186                name_buf.as_ptr() as *const c_char,
2187                ns_buf.as_ptr() as *const c_char,
2188                enc_ptr,
2189            )
2190        }
2191    };
2192
2193    match Session::get_node_names(s, &mut cb) {
2194        Ok(()) => NROS_RMW_RET_OK,
2195        Err(e) => ret_from_error(&e),
2196    }
2197}
2198
2199/// phase-381 W3 — `count_publishers` / `count_subscribers` share one body.
2200macro_rules! count_trampoline {
2201    ($name:ident, $method:ident) => {
2202        unsafe extern "C" fn $name<R: RustBackend>(
2203            session: *const NrosRmwSession,
2204            topic_name: *const c_char,
2205            count: *mut usize,
2206        ) -> NrosRmwRet {
2207            if topic_name.is_null() || count.is_null() {
2208                return NROS_RMW_RET_INVALID_ARGUMENT;
2209            }
2210            let Some(s) = (unsafe { session_mut::<R::Session>(session.cast_mut()) }) else {
2211                return NROS_RMW_RET_INVALID_ARGUMENT;
2212            };
2213            let Ok(topic) = (unsafe { core::ffi::CStr::from_ptr(topic_name) }).to_str() else {
2214                return NROS_RMW_RET_INVALID_ARGUMENT;
2215            };
2216            match Session::$method(s, topic) {
2217                Ok(n) => {
2218                    unsafe { *count = n };
2219                    NROS_RMW_RET_OK
2220                }
2221                Err(e) => ret_from_error(&e),
2222            }
2223        }
2224    };
2225}
2226
2227count_trampoline!(count_publishers_trampoline, count_publishers);
2228count_trampoline!(count_subscribers_trampoline, count_subscribers);
2229// Issue 1978 — the service twins. The name is a SERVICE name; the body is the
2230// same.
2231count_trampoline!(count_clients_trampoline, count_clients);
2232count_trampoline!(count_services_trampoline, count_services);
2233
2234/// Issue 1978 — upstream (Jazzy) `rmw_get_gid_for_client`, the client twin of
2235/// [`get_gid_for_publisher_trampoline`], and like it writes only `data`.
2236unsafe extern "C" fn get_gid_for_client_trampoline<R: RustBackend>(
2237    client: *const NrosRmwClient,
2238    gid: *mut NrosRmwGid,
2239) -> NrosRmwRet {
2240    if gid.is_null() {
2241        return NROS_RMW_RET_INVALID_ARGUMENT;
2242    }
2243    let Some(c) = (unsafe { client_mut::<R::Client>(client) }) else {
2244        return NROS_RMW_RET_INVALID_ARGUMENT;
2245    };
2246    match ClientTrait::get_gid(&*c) {
2247        Ok(bytes) => {
2248            // SAFETY: checked non-null above; the caller owns the storage.
2249            unsafe {
2250                (*gid).data = bytes;
2251            }
2252            NROS_RMW_RET_OK
2253        }
2254        Err(e) => ret_from_error(&e),
2255    }
2256}
2257
2258/// Issue 1978 — upstream (Jazzy) `rmw_event_type_is_supported`, answered from
2259/// the backend TYPE (`Session::event_type_is_supported`, a static fn: the slot
2260/// carries no session). A discriminant outside the nine known kinds is
2261/// `false` — `event_kind_from_c` folds unknown values into `MessageLost`,
2262/// which would answer for a kind nobody asked about.
2263unsafe extern "C" fn event_type_is_supported_trampoline<R: RustBackend>(
2264    event_type: NrosRmwEventKind,
2265) -> bool {
2266    use crate::rmw_event_type_t as ck;
2267    let known = matches!(
2268        event_type,
2269        ck::NROS_RMW_EVENT_LIVELINESS_CHANGED
2270            | ck::NROS_RMW_EVENT_REQUESTED_DEADLINE_MISSED
2271            | ck::NROS_RMW_EVENT_MESSAGE_LOST
2272            | ck::NROS_RMW_EVENT_LIVELINESS_LOST
2273            | ck::NROS_RMW_EVENT_OFFERED_DEADLINE_MISSED
2274            | ck::NROS_RMW_EVENT_PUBLICATION_MATCHED
2275            | ck::NROS_RMW_EVENT_SUBSCRIPTION_MATCHED
2276            | ck::NROS_RMW_EVENT_PUBLISHER_INCOMPATIBLE_TYPE
2277            | ck::NROS_RMW_EVENT_SUBSCRIPTION_INCOMPATIBLE_TYPE
2278    );
2279    known && <R::Session as Session>::event_type_is_supported(event_kind_from_c(event_type))
2280}
2281
2282/// phase-381 W3 — `get_topic_names_and_types` / `get_service_names_and_types`.
2283///
2284/// The C visitor takes `(name, types[], types_count)`. Rust hands over
2285/// `&[&str]`, so each call builds a bounded array of NUL-terminated pointers on
2286/// the stack — the strings are borrowed for the call only, which is the
2287/// contract on both sides.
2288///
2289/// A name or type that does not fit is SKIPPED, never truncated: a truncated
2290/// type name is a different, plausible type.
2291macro_rules! names_and_types_trampoline {
2292    // The topic form carries `no_demangle`; the service form does not — that
2293    // asymmetry is upstream's, mirrored rather than smoothed over.
2294    ($name:ident, $method:ident, demangle_flag) => {
2295        unsafe extern "C" fn $name<R: RustBackend>(
2296            session: *const NrosRmwSession,
2297            no_demangle: bool,
2298            // phase-406 W2 — the pair as one argument.
2299            visitor: crate::generated::rmw_names_and_types_visitor_t,
2300        ) -> NrosRmwRet {
2301            // `no_demangle` asks for the WIRE spelling. This backend reports ROS
2302            // names, so honouring it would mean re-mangling what was just
2303            // demangled; refused rather than silently ignored, because ignoring
2304            // it answers a different question than the caller asked.
2305            if no_demangle {
2306                return NROS_RMW_RET_UNSUPPORTED;
2307            }
2308            $crate::names_and_types_body!(R, $method, session, visitor)
2309        }
2310    };
2311    ($name:ident, $method:ident) => {
2312        unsafe extern "C" fn $name<R: RustBackend>(
2313            session: *const NrosRmwSession,
2314            // phase-406 W2 — the pair as one argument.
2315            visitor: crate::generated::rmw_names_and_types_visitor_t,
2316        ) -> NrosRmwRet {
2317            $crate::names_and_types_body!(R, $method, session, visitor)
2318        }
2319    };
2320}
2321
2322/// The shared body, so the two arms above differ only by the parameter list.
2323#[macro_export]
2324#[doc(hidden)]
2325macro_rules! names_and_types_body {
2326    ($R:ident, $method:ident, $session:ident, $visitor:ident) => {{
2327        // phase-406 W2 — unpack HERE: a `let` in the caller's expansion is not
2328        // visible in this one (macro hygiene), so the struct crosses instead.
2329        let (visit_fn, ctx) = ($visitor.visit, $visitor.ctx);
2330        let Some(visit) = visit_fn else {
2331            return NROS_RMW_RET_INVALID_ARGUMENT;
2332        };
2333        let Some(s) = (unsafe { session_mut::<$R::Session>($session.cast_mut()) }) else {
2334            return NROS_RMW_RET_INVALID_ARGUMENT;
2335        };
2336
2337        const NAME_MAX: usize = 256;
2338        const TYPES_MAX: usize = 8;
2339
2340        let mut cb = |name: &str, types: &[&str]| -> bool {
2341            let mut name_buf = [0u8; NAME_MAX];
2342            if name.len() >= NAME_MAX {
2343                return true; // skip, keep enumerating
2344            }
2345            name_buf[..name.len()].copy_from_slice(name.as_bytes());
2346
2347            let mut type_bufs = [[0u8; NAME_MAX]; TYPES_MAX];
2348            let mut ptrs: [*const c_char; TYPES_MAX] = [core::ptr::null(); TYPES_MAX];
2349            let mut n = 0usize;
2350            for t in types.iter().take(TYPES_MAX) {
2351                if t.len() >= NAME_MAX {
2352                    continue;
2353                }
2354                type_bufs[n][..t.len()].copy_from_slice(t.as_bytes());
2355                ptrs[n] = type_bufs[n].as_ptr() as *const c_char;
2356                n += 1;
2357            }
2358            unsafe { visit(ctx, name_buf.as_ptr() as *const c_char, ptrs.as_ptr(), n) }
2359        };
2360
2361        match Session::$method(s, &mut cb) {
2362            Ok(()) => NROS_RMW_RET_OK,
2363            Err(e) => ret_from_error(&e),
2364        }
2365    }};
2366}
2367
2368names_and_types_trampoline!(
2369    get_topic_names_and_types_trampoline,
2370    get_topic_names_and_types,
2371    demangle_flag
2372);
2373names_and_types_trampoline!(
2374    get_service_names_and_types_trampoline,
2375    get_service_names_and_types
2376);
2377
2378/// phase-381 W3 — the four `*_by_node` slots, one body.
2379///
2380/// Longhand callers, shared implementation: they differ only by which
2381/// `GraphEntityKind` they pass, and four copies of this marshalling is four
2382/// chances for one of them to drift.
2383///
2384/// # Safety
2385/// Same contract as the slots that call it.
2386unsafe fn by_node_impl<R: RustBackend>(
2387    session: *const NrosRmwSession,
2388    node_name: *const c_char,
2389    node_namespace: *const c_char,
2390    no_demangle: bool,
2391    // phase-406 W2 — the pair as one argument.
2392    visitor: crate::generated::rmw_names_and_types_visitor_t,
2393    kind: nros_rmw::GraphEntityKind,
2394) -> NrosRmwRet {
2395    let (visit, ctx) = (visitor.visit, visitor.ctx);
2396    // `no_demangle` asks for the WIRE spelling; we report ROS names, so
2397    // honouring it would mean re-mangling what was just demangled. Refused
2398    // rather than ignored — ignoring answers a different question than asked.
2399    if no_demangle {
2400        return NROS_RMW_RET_UNSUPPORTED;
2401    }
2402    if node_name.is_null() || node_namespace.is_null() {
2403        return NROS_RMW_RET_INVALID_ARGUMENT;
2404    }
2405    let Some(visit) = visit else {
2406        return NROS_RMW_RET_INVALID_ARGUMENT;
2407    };
2408    let Some(s) = (unsafe { session_mut::<R::Session>(session.cast_mut()) }) else {
2409        return NROS_RMW_RET_INVALID_ARGUMENT;
2410    };
2411    let (Ok(name), Ok(ns)) = (
2412        unsafe { core::ffi::CStr::from_ptr(node_name) }.to_str(),
2413        unsafe { core::ffi::CStr::from_ptr(node_namespace) }.to_str(),
2414    ) else {
2415        return NROS_RMW_RET_INVALID_ARGUMENT;
2416    };
2417
2418    const NAME_MAX: usize = 256;
2419    const TYPES_MAX: usize = 8;
2420    let mut cb = |n: &str, types: &[&str]| -> bool {
2421        let mut name_buf = [0u8; NAME_MAX];
2422        if n.len() >= NAME_MAX {
2423            return true;
2424        }
2425        name_buf[..n.len()].copy_from_slice(n.as_bytes());
2426        let mut type_bufs = [[0u8; NAME_MAX]; TYPES_MAX];
2427        let mut ptrs: [*const c_char; TYPES_MAX] = [core::ptr::null(); TYPES_MAX];
2428        let mut count = 0usize;
2429        for t in types.iter().take(TYPES_MAX) {
2430            if t.len() >= NAME_MAX {
2431                continue;
2432            }
2433            type_bufs[count][..t.len()].copy_from_slice(t.as_bytes());
2434            ptrs[count] = type_bufs[count].as_ptr() as *const c_char;
2435            count += 1;
2436        }
2437        unsafe {
2438            visit(
2439                ctx,
2440                name_buf.as_ptr() as *const c_char,
2441                ptrs.as_ptr(),
2442                count,
2443            )
2444        }
2445    };
2446    match Session::get_names_and_types_by_node(s, kind, name, ns, &mut cb) {
2447        Ok(()) => NROS_RMW_RET_OK,
2448        Err(e) => ret_from_error(&e),
2449    }
2450}
2451
2452macro_rules! by_node_trampoline {
2453    // Upstream gives `no_demangle` to the publisher and subscriber forms and
2454    // NOT to the service and client ones. Mirrored rather than smoothed over —
2455    // RFC-0054 makes these headers the ABI SSoT, so an asymmetry upstream has
2456    // is one we have.
2457    ($name:ident, $kind:ident, no_demangle) => {
2458        unsafe extern "C" fn $name<R: RustBackend>(
2459            session: *const NrosRmwSession,
2460            node_name: *const c_char,
2461            node_namespace: *const c_char,
2462            no_demangle: bool,
2463            // phase-406 W2 — the pair as one argument.
2464            visitor: crate::generated::rmw_names_and_types_visitor_t,
2465        ) -> NrosRmwRet {
2466            unsafe {
2467                by_node_impl::<R>(
2468                    session,
2469                    node_name,
2470                    node_namespace,
2471                    no_demangle,
2472                    visitor,
2473                    nros_rmw::GraphEntityKind::$kind,
2474                )
2475            }
2476        }
2477    };
2478    // The service and client forms, which upstream gives no `no_demangle`.
2479    ($name:ident, $kind:ident) => {
2480        unsafe extern "C" fn $name<R: RustBackend>(
2481            session: *const NrosRmwSession,
2482            node_name: *const c_char,
2483            node_namespace: *const c_char,
2484            // phase-406 W2 — the pair as one argument.
2485            visitor: crate::generated::rmw_names_and_types_visitor_t,
2486        ) -> NrosRmwRet {
2487            unsafe {
2488                by_node_impl::<R>(
2489                    session,
2490                    node_name,
2491                    node_namespace,
2492                    false,
2493                    visitor,
2494                    nros_rmw::GraphEntityKind::$kind,
2495                )
2496            }
2497        }
2498    };
2499}
2500
2501by_node_trampoline!(
2502    get_publisher_names_and_types_by_node_trampoline,
2503    Publisher,
2504    no_demangle
2505);
2506by_node_trampoline!(
2507    get_subscriber_names_and_types_by_node_trampoline,
2508    Subscriber,
2509    no_demangle
2510);
2511by_node_trampoline!(get_service_names_and_types_by_node_trampoline, Service);
2512by_node_trampoline!(get_client_names_and_types_by_node_trampoline, Client);
2513
2514/// phase-381 W3 — the two `*_info_by_topic` slots, one body.
2515///
2516/// # Safety
2517/// Same contract as the slots that call it.
2518unsafe fn endpoint_info_impl<R: RustBackend>(
2519    session: *const NrosRmwSession,
2520    topic_name: *const c_char,
2521    no_mangle: bool,
2522    // phase-406 W2 — the pair as one argument.
2523    visitor: crate::generated::rmw_topic_endpoint_info_visitor_t,
2524    publishers: bool,
2525) -> NrosRmwRet {
2526    let (visit, ctx) = (visitor.visit, visitor.ctx);
2527    if no_mangle {
2528        return NROS_RMW_RET_UNSUPPORTED;
2529    }
2530    if topic_name.is_null() {
2531        return NROS_RMW_RET_INVALID_ARGUMENT;
2532    }
2533    let Some(visit) = visit else {
2534        return NROS_RMW_RET_INVALID_ARGUMENT;
2535    };
2536    let Some(s) = (unsafe { session_mut::<R::Session>(session.cast_mut()) }) else {
2537        return NROS_RMW_RET_INVALID_ARGUMENT;
2538    };
2539    let Ok(topic) = (unsafe { core::ffi::CStr::from_ptr(topic_name) }).to_str() else {
2540        return NROS_RMW_RET_INVALID_ARGUMENT;
2541    };
2542
2543    const NAME_MAX: usize = 256;
2544    let mut cb = |info: &nros_rmw::GraphEndpointInfo<'_>| -> bool {
2545        let mut name_buf = [0u8; NAME_MAX];
2546        let mut ns_buf = [0u8; NAME_MAX];
2547        let mut ty_buf = [0u8; NAME_MAX];
2548        if info.node_name.len() >= NAME_MAX
2549            || info.node_namespace.len() >= NAME_MAX
2550            || info.topic_type.len() >= NAME_MAX
2551        {
2552            return true; // skip, never truncate
2553        }
2554        name_buf[..info.node_name.len()].copy_from_slice(info.node_name.as_bytes());
2555        ns_buf[..info.node_namespace.len()].copy_from_slice(info.node_namespace.as_bytes());
2556        ty_buf[..info.topic_type.len()].copy_from_slice(info.topic_type.as_bytes());
2557        // The struct has no "qos known" flag, by design: the ABI expresses an
2558        // unreadable policy with the `*_UNKNOWN` sentinels and the contract on
2559        // `publisher_get_actual_qos` spells it out — write UNKNOWN for what you
2560        // cannot determine and return OK; `UNSUPPORTED` means no read-back AT
2561        // ALL, not "I know some of it".
2562        //
2563        // issue 1973 — the backend's own answer crosses now
2564        // (`GraphEndpointInfo::qos`; zenoh decodes the token's QoS chunk). It
2565        // is already `QOS_PROFILE_UNKNOWN` where the backend could not read
2566        // one; a depth the C ABI's u16 cannot carry also degrades to that,
2567        // never to a truncated guess.
2568        let qos: rmw_qos_profile_t = match NrosRmwQos::try_from(info.qos) {
2569            Ok(q) => q,
2570            Err(_) => {
2571                let mut q: rmw_qos_profile_t = unsafe { core::mem::zeroed() };
2572                q.reliability = NROS_RMW_RELIABILITY_UNKNOWN as u8;
2573                q.durability = NROS_RMW_DURABILITY_UNKNOWN as u8;
2574                q.history = NROS_RMW_HISTORY_UNKNOWN as u8;
2575                q.liveliness_kind = rmw_liveliness_kind_t::NROS_RMW_LIVELINESS_UNKNOWN as u8;
2576                q
2577            }
2578        };
2579        let c_info = rmw_topic_endpoint_info_t {
2580            node_name: name_buf.as_ptr() as *const c_char,
2581            node_namespace: ns_buf.as_ptr() as *const c_char,
2582            topic_type: ty_buf.as_ptr() as *const c_char,
2583            endpoint_type: if info.is_publisher {
2584                rmw_endpoint_type_t::RMW_ENDPOINT_PUBLISHER
2585            } else {
2586                rmw_endpoint_type_t::RMW_ENDPOINT_SUBSCRIPTION
2587            },
2588            endpoint_gid: rmw_gid_t {
2589                implementation_identifier: core::ptr::null(),
2590                data: info.endpoint_gid,
2591            },
2592            qos_profile: qos,
2593        };
2594        unsafe { visit(ctx, &c_info as *const rmw_topic_endpoint_info_t) }
2595    };
2596    match Session::get_endpoint_info_by_topic(s, publishers, topic, &mut cb) {
2597        Ok(()) => NROS_RMW_RET_OK,
2598        Err(e) => ret_from_error(&e),
2599    }
2600}
2601
2602macro_rules! endpoint_info_trampoline {
2603    ($name:ident, $publishers:expr) => {
2604        unsafe extern "C" fn $name<R: RustBackend>(
2605            session: *const NrosRmwSession,
2606            topic_name: *const c_char,
2607            no_mangle: bool,
2608            // phase-406 W2 — the pair as one argument.
2609            visitor: crate::generated::rmw_topic_endpoint_info_visitor_t,
2610        ) -> NrosRmwRet {
2611            unsafe { endpoint_info_impl::<R>(session, topic_name, no_mangle, visitor, $publishers) }
2612        }
2613    };
2614}
2615
2616endpoint_info_trampoline!(get_publishers_info_by_topic_trampoline, true);
2617endpoint_info_trampoline!(get_subscriptions_info_by_topic_trampoline, false);
2618
2619unsafe extern "C" fn publish_streamed_trampoline<R: RustBackend>(
2620    publisher: *mut NrosRmwPublisher,
2621    size_cb: Option<unsafe extern "C" fn(out_total_len: *mut usize, user_ctx: *mut c_void)>,
2622    chunk_cb: Option<
2623        unsafe extern "C" fn(
2624            out_buf: *mut u8,
2625            cap: usize,
2626            out_written: *mut usize,
2627            user_ctx: *mut c_void,
2628        ),
2629    >,
2630    user_ctx: *mut c_void,
2631) -> NrosRmwRet {
2632    // Phase 124.E.1 — delegate to the Rust backend's
2633    // `Publisher::publish_streamed` impl. Default trait body fires
2634    // the staging-buffer fallback (124.E.2); concrete backends opt
2635    // in by overriding for true streamed publish into the network
2636    // buffer.
2637    let Some(p) = (unsafe { publisher_ref::<R::Publisher>(publisher) }) else {
2638        return NROS_RMW_RET_INVALID_ARGUMENT;
2639    };
2640    // Generated ABI marks the callbacks nullable; the trait surface is not.
2641    let (Some(size_cb), Some(chunk_cb)) = (size_cb, chunk_cb) else {
2642        return NROS_RMW_RET_INVALID_ARGUMENT;
2643    };
2644    // SAFETY: this trampoline is entered from the C ABI with the same
2645    // callback/user_ctx lifetime contract required by `Publisher::publish_streamed`.
2646    match unsafe { Publisher::publish_streamed(p, size_cb, chunk_cb, user_ctx) } {
2647        Ok(()) => NROS_RMW_RET_OK,
2648        Err(e) => ret_from_error(&e),
2649    }
2650}
2651
2652unsafe extern "C" fn take_sequence_trampoline<R: RustBackend>(
2653    subscriber: *const NrosRmwSubscription,
2654    buf: *mut u8,
2655    per_msg_cap: usize,
2656    max_msgs: usize,
2657    out_lens: *mut usize,
2658    taken: *mut usize,
2659) -> NrosRmwRet {
2660    // Phase 376 W3.b/W3.d step A — the COUNT is an out-parameter, matching
2661    // upstream's `size_t *taken`.
2662    if taken.is_null() {
2663        return NROS_RMW_RET_INVALID_ARGUMENT;
2664    }
2665    // Phase 124.D.1 — delegate to the Rust backend's
2666    // `Subscription::take_sequence` impl. Default trait body
2667    // loop-drives `take_serialized`; concrete backends opt in by
2668    // overriding for a native batch take.
2669    let Some(s) = (unsafe { subscription_mut::<R::Subscription>(subscriber) }) else {
2670        return NROS_RMW_RET_INVALID_ARGUMENT;
2671    };
2672    if buf.is_null() || out_lens.is_null() || per_msg_cap == 0 {
2673        return NROS_RMW_RET_INVALID_ARGUMENT;
2674    }
2675    // SAFETY: caller pinky-promised a contiguous block of
2676    // `max_msgs * per_msg_cap` bytes at `buf` and at least
2677    // `max_msgs` `usize` slots at `out_lens`. The buffer is
2678    // exclusively borrowed for the duration of this call.
2679    let buf_slice =
2680        unsafe { core::slice::from_raw_parts_mut(buf, max_msgs.saturating_mul(per_msg_cap)) };
2681    let lens_slice = unsafe { core::slice::from_raw_parts_mut(out_lens, max_msgs) };
2682    match Subscription::take_sequence(s, buf_slice, per_msg_cap, max_msgs, lens_slice) {
2683        Ok(count) => {
2684            // SAFETY: checked non-null above.
2685            unsafe { *taken = count };
2686            NROS_RMW_RET_OK
2687        }
2688        Err(e) => ret_from_error(&e),
2689    }
2690}
2691
2692// ============================================================================
2693// Pointer plumbing — keep one place where each entity's
2694// `backend_data` is dereferenced.
2695// ============================================================================
2696//
2697// Validity contract:
2698//
2699//   - The C-side wrappers (`CffiSession`, `CffiPublisher`, …) hand
2700//     each trampoline an entity-struct pointer whose `backend_data`
2701//     was last written by *this* adapter's create-trampoline. That
2702//     write is `box_handle(handle)` (a fallible `Box`) of `R::Session` /
2703//     `R::Publisher` / … so the pointer aliases a valid Box<T>.
2704//   - `_mut` variants give `&mut T`; the runtime serialises access
2705//     per the executor's single-thread invariant.
2706//   - `take_box` reclaims ownership; subsequent calls see a null
2707//     `backend_data` and return `INVALID_ARGUMENT`.
2708
2709#[inline]
2710unsafe fn session_data_mut(session: *mut NrosRmwSession) -> &'static mut *mut c_void {
2711    unsafe { &mut (*session).backend_data }
2712}
2713
2714#[inline]
2715unsafe fn publisher_data_mut(publisher: *mut NrosRmwPublisher) -> &'static mut *mut c_void {
2716    unsafe { &mut (*publisher).backend_data }
2717}
2718
2719#[inline]
2720unsafe fn subscription_data_mut(subscriber: *mut NrosRmwSubscription) -> &'static mut *mut c_void {
2721    unsafe { &mut (*subscriber).backend_data }
2722}
2723
2724#[inline]
2725unsafe fn service_data_mut(server: *mut NrosRmwService) -> &'static mut *mut c_void {
2726    unsafe { &mut (*server).backend_data }
2727}
2728
2729#[inline]
2730unsafe fn client_data_mut(client: *mut NrosRmwClient) -> &'static mut *mut c_void {
2731    unsafe { &mut (*client).backend_data }
2732}
2733
2734#[inline]
2735unsafe fn session_mut<'a, T>(session: *mut NrosRmwSession) -> Option<&'a mut T> {
2736    if session.is_null() {
2737        return None;
2738    }
2739    let p = unsafe { (*session).backend_data } as *mut T;
2740    if p.is_null() {
2741        None
2742    } else {
2743        Some(unsafe { &mut *p })
2744    }
2745}
2746
2747#[inline]
2748unsafe fn session_ref<'a, T>(session: *const NrosRmwSession) -> Option<&'a T> {
2749    if session.is_null() {
2750        return None;
2751    }
2752    let p = unsafe { (*session).backend_data } as *const T;
2753    if p.is_null() {
2754        None
2755    } else {
2756        Some(unsafe { &*p })
2757    }
2758}
2759
2760#[inline]
2761unsafe fn publisher_ref<'a, T>(publisher: *const NrosRmwPublisher) -> Option<&'a T> {
2762    if publisher.is_null() {
2763        return None;
2764    }
2765    let p = unsafe { (*publisher).backend_data } as *const T;
2766    if p.is_null() {
2767        None
2768    } else {
2769        Some(unsafe { &*p })
2770    }
2771}
2772
2773#[inline]
2774unsafe fn subscription_mut<'a, T>(subscriber: *const NrosRmwSubscription) -> Option<&'a mut T> {
2775    if subscriber.is_null() {
2776        return None;
2777    }
2778    let p = unsafe { (*subscriber).backend_data } as *mut T;
2779    if p.is_null() {
2780        None
2781    } else {
2782        Some(unsafe { &mut *p })
2783    }
2784}
2785
2786#[inline]
2787unsafe fn subscription_ref<'a, T>(subscriber: *const NrosRmwSubscription) -> Option<&'a T> {
2788    if subscriber.is_null() {
2789        return None;
2790    }
2791    let p = unsafe { (*subscriber).backend_data } as *const T;
2792    if p.is_null() {
2793        None
2794    } else {
2795        Some(unsafe { &*p })
2796    }
2797}
2798
2799#[inline]
2800unsafe fn service_mut<'a, T>(server: *const NrosRmwService) -> Option<&'a mut T> {
2801    if server.is_null() {
2802        return None;
2803    }
2804    let p = unsafe { (*server).backend_data } as *mut T;
2805    if p.is_null() {
2806        None
2807    } else {
2808        Some(unsafe { &mut *p })
2809    }
2810}
2811
2812#[inline]
2813unsafe fn service_ref<'a, T>(server: *const NrosRmwService) -> Option<&'a T> {
2814    if server.is_null() {
2815        return None;
2816    }
2817    let p = unsafe { (*server).backend_data } as *const T;
2818    if p.is_null() {
2819        None
2820    } else {
2821        Some(unsafe { &*p })
2822    }
2823}
2824
2825#[inline]
2826unsafe fn client_mut<'a, T>(client: *const NrosRmwClient) -> Option<&'a mut T> {
2827    if client.is_null() {
2828        return None;
2829    }
2830    let p = unsafe { (*client).backend_data } as *mut T;
2831    if p.is_null() {
2832        None
2833    } else {
2834        Some(unsafe { &mut *p })
2835    }
2836}
2837
2838/// Issue 1551 (defect 2) — box a handle the backend just created, FALLIBLY.
2839///
2840/// This used to be `Box::into_raw(Box::new(handle))` at five sites. `Box::new`
2841/// cannot fail: on an exhausted heap it calls `handle_alloc_error`, and in a
2842/// `std` build libstd's OOM hook writes to fd 2 — which on Zephyr
2843/// `native_sim` is issue 0589's `zvfs_write` recursion, so the image died
2844/// with SIGSEGV (exit 139, ~104,735 frames deep) and the hook's own message
2845/// never appeared. Every C caller of these trampolines already handles
2846/// `NROS_RMW_RET_BAD_ALLOC`; the allocation simply never gave it the chance.
2847///
2848/// On `Err` the handle comes BACK to the caller, which disposes of it (for an
2849/// entity, dropping it undeclares what the backend declared; a session is
2850/// closed first, as `destroy_session_trampoline` does). The diagnostic goes
2851/// through `nros_log` (issue 0589: never std stdio), whose formatting is a
2852/// fixed stack buffer, so reporting an exhausted heap does not itself need
2853/// the heap.
2854fn box_handle<T>(kind: &str, value: T) -> Result<*mut c_void, T> {
2855    match nros_rmw::fallible::try_box(value) {
2856        Ok(boxed) => Ok(Box::into_raw(boxed) as *mut c_void),
2857        Err(value) => {
2858            nros_log::log_error!(
2859                nros_log::get_logger("nros_rmw_cffi"),
2860                "heap exhausted: could not allocate {} bytes for a {} handle; \
2861                 the {} is NOT created (NROS_RMW_RET_BAD_ALLOC). Raise the \
2862                 platform heap (Zephyr: CONFIG_NROS_ZEPHYR_HEAP_SIZE)",
2863                core::mem::size_of::<T>(),
2864                kind,
2865                kind
2866            );
2867            Err(value)
2868        }
2869    }
2870}
2871
2872#[inline]
2873unsafe fn take_box<T>(slot: &mut *mut c_void) -> Option<Box<T>> {
2874    if slot.is_null() {
2875        return None;
2876    }
2877    let ptr = *slot as *mut T;
2878    *slot = core::ptr::null_mut();
2879    if ptr.is_null() {
2880        None
2881    } else {
2882        // SAFETY: this pointer was minted by `Box::into_raw` in the
2883        // corresponding create-trampoline; we are taking ownership
2884        // back and the slot has been cleared so no second take is
2885        // possible.
2886        Some(unsafe { Box::from_raw(ptr) })
2887    }
2888}
2889
2890#[cfg(test)]
2891mod tests {
2892    use super::*;
2893
2894    /// issue 0829 — the inbound edge RAISES a 0 policy back to the sentinel, so
2895    /// a C caller's "unstated" survives to the Rust backend that has to resolve
2896    /// it. Before this, `qos_from_cffi` folded 0 to RELIABLE / VOLATILE /
2897    /// KEEP_LAST here at the edge, which made every Rust backend answer the
2898    /// sentinel identically — the thing upstream deliberately does not do.
2899    ///
2900    /// This is the round trip: profile -> C struct -> profile.
2901    #[test]
2902    fn a_zero_filled_c_profile_reads_back_as_the_sentinel() {
2903        let raised = qos_from_cffi(&crate::NROS_RMW_QOS_PROFILE_SYSTEM_DEFAULT);
2904        assert_eq!(raised, nros_rmw::QoSProfile::QOS_PROFILE_SYSTEM_DEFAULT);
2905        assert!(raised.has_unresolved_system_default());
2906    }
2907
2908    /// UNKNOWN is NOT the sentinel and must keep its old fold: it is a
2909    /// read-back artefact ("the backend could not determine this"), not a
2910    /// request, so there is no absence to carry.
2911    #[test]
2912    fn unknown_is_not_raised_to_the_sentinel() {
2913        let mut q = crate::NROS_RMW_QOS_PROFILE_SYSTEM_DEFAULT;
2914        q.reliability = crate::generated::NROS_RMW_RELIABILITY_UNKNOWN as u8;
2915        let raised = qos_from_cffi(&q);
2916        assert_eq!(
2917            raised.reliability,
2918            nros_rmw::QoSReliabilityPolicy::Reliable,
2919            "UNKNOWN must keep folding to the ROS default, not become a sentinel"
2920        );
2921    }
2922}