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