Skip to main content

hdds_c/
lib.rs

1// SPDX-License-Identifier: Apache-2.0 OR MIT
2// Copyright (c) 2025-2026 naskel.com
3
4//! # HDDS C FFI Bindings
5//!
6//! This crate provides C-compatible FFI bindings for the HDDS DDS implementation.
7//!
8//! # Safety
9//!
10//! All public functions are `unsafe` and require the caller to uphold the
11//! invariants documented in each function's safety comment.
12
13mod info;
14mod listener;
15mod logging;
16mod pubsub;
17mod qos;
18#[cfg(feature = "rmw")]
19mod rmw;
20mod telemetry;
21mod waitset;
22
23// Re-export new modules
24pub use info::*;
25pub use listener::*;
26pub use logging::*;
27pub use pubsub::*;
28pub use telemetry::*;
29
30// Re-export QoS types
31pub use qos::HddsQoS;
32
33use std::collections::HashMap;
34use std::convert::TryFrom;
35use std::ffi::CStr;
36use std::os::raw::{c_char, c_void};
37use std::ptr;
38use std::sync::{Arc, Mutex, OnceLock};
39use std::time::Duration;
40use waitset::ForeignWaitSet;
41
42use hdds::api::{DataReader, DataWriter, GuardCondition, Participant, QoS, StatusCondition, DDS};
43use hdds::core::types::{Distro, TypeDescriptor, TypeObjectHandle, ROS_HASH_SIZE};
44use hdds::dds::Condition;
45use serde::{Deserialize, Serialize};
46
47// XTypes imports (for type support registration)
48#[cfg(feature = "xtypes")]
49use hdds::xtypes::{rosidl_message_type_support_t, RosidlError};
50
51// RMW-only imports
52#[cfg(feature = "rmw")]
53use hdds::api::Error as ApiError;
54#[cfg(feature = "rmw")]
55use rmw::{
56    decode_special, deserialize_dynamic_to_ros, encode_special, map_api_error,
57    ros2_type_to_descriptor, serialize_from_ros, ForeignRmwContext, ForeignRmwWaitSet,
58    Ros2CodecKind,
59};
60#[cfg(feature = "rmw")]
61use std::ffi::CString;
62#[cfg(feature = "rmw")]
63use std::slice;
64
65#[cfg(feature = "rmw")]
66type HddsNodeVisitor = Option<unsafe extern "C" fn(*const c_char, *const c_char, *mut c_void)>;
67#[cfg(feature = "rmw")]
68type HddsNodeEnclaveVisitor =
69    Option<unsafe extern "C" fn(*const c_char, *const c_char, *const c_char, *mut c_void)>;
70#[cfg(feature = "rmw")]
71type HddsEndpointVisitor = Option<
72    unsafe extern "C" fn(
73        *const c_char,
74        *const c_char,
75        *const u8,
76        *const HddsRmwQosProfile,
77        *mut c_void,
78    ),
79>;
80
81#[cfg(feature = "rmw")]
82fn normalize_ros_type_name(type_name: &str) -> String {
83    if type_name.contains("::") {
84        type_name.replace("::", "/")
85    } else {
86        type_name.to_string()
87    }
88}
89
90/// Opaque handle to a Participant
91#[repr(C)]
92pub struct HddsParticipant {
93    _private: [u8; 0],
94}
95
96/// Opaque handle to a `DataWriter`
97#[repr(C)]
98pub struct HddsDataWriter {
99    _private: [u8; 0],
100}
101
102/// Opaque handle to a `DataReader`
103#[repr(C)]
104pub struct HddsDataReader {
105    _private: [u8; 0],
106}
107
108/// Opaque handle to a WaitSet
109#[repr(C)]
110pub struct HddsWaitSet {
111    _private: [u8; 0],
112}
113
114/// Opaque handle to a GuardCondition
115#[repr(C)]
116pub struct HddsGuardCondition {
117    _private: [u8; 0],
118}
119
120/// Opaque handle to a StatusCondition
121#[repr(C)]
122pub struct HddsStatusCondition {
123    _private: [u8; 0],
124}
125
126/// Opaque handle to an rmw context
127#[cfg(feature = "rmw")]
128#[repr(C)]
129pub struct HddsRmwContext {
130    _private: [u8; 0],
131}
132
133#[cfg(feature = "rmw")]
134pub const HDDS_RMW_GID_SIZE: usize = hdds::rmw::graph::RMW_GID_STORAGE_SIZE;
135
136#[cfg(feature = "rmw")]
137#[repr(C)]
138#[derive(Clone, Copy, Debug, Default)]
139pub struct HddsRmwQosProfile {
140    pub history: u8,
141    pub depth: u32,
142    pub reliability: u8,
143    pub durability: u8,
144    pub deadline_ns: u64,
145    pub lifespan_ns: u64,
146    pub liveliness: u8,
147    pub liveliness_lease_ns: u64,
148    pub avoid_ros_namespace_conventions: bool,
149}
150
151/// Opaque handle to an rmw waitset
152#[cfg(feature = "rmw")]
153#[repr(C)]
154pub struct HddsRmwWaitSet {
155    _private: [u8; 0],
156}
157
158#[cfg(feature = "rmw")]
159pub type HddsTopicVisitor =
160    unsafe extern "C" fn(*const c_char, *const c_char, u32, u32, *mut c_void);
161
162#[cfg(feature = "rmw")]
163pub type HddsLocatorVisitor = unsafe extern "C" fn(*const c_char, u16, *mut c_void);
164
165#[cfg(feature = "xtypes")]
166#[repr(C)]
167pub struct HddsTypeObject {
168    _private: [u8; 0],
169}
170
171struct GuardRegistryEntry {
172    guard: Arc<GuardCondition>,
173    handles: usize,
174}
175
176struct StatusRegistryEntry {
177    status: Arc<StatusCondition>,
178    handles: usize,
179}
180
181fn guard_registry() -> &'static Mutex<HashMap<usize, GuardRegistryEntry>> {
182    static REGISTRY: OnceLock<Mutex<HashMap<usize, GuardRegistryEntry>>> = OnceLock::new();
183    REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
184}
185
186fn status_registry() -> &'static Mutex<HashMap<usize, StatusRegistryEntry>> {
187    static REGISTRY: OnceLock<Mutex<HashMap<usize, StatusRegistryEntry>>> = OnceLock::new();
188    REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
189}
190
191fn guard_registry_add_handle(raw: *const HddsGuardCondition, guard: Arc<GuardCondition>) {
192    if raw.is_null() {
193        return;
194    }
195
196    let mut registry = guard_registry()
197        .lock()
198        .unwrap_or_else(|err| err.into_inner());
199    let entry = registry
200        .entry(raw as usize)
201        .or_insert_with(|| GuardRegistryEntry { guard, handles: 0 });
202    entry.handles = entry.handles.saturating_add(1);
203}
204
205fn guard_registry_clone(raw: *const HddsGuardCondition) -> Option<Arc<GuardCondition>> {
206    if raw.is_null() {
207        return None;
208    }
209
210    let registry = guard_registry()
211        .lock()
212        .unwrap_or_else(|err| err.into_inner());
213    registry
214        .get(&(raw as usize))
215        .map(|entry| entry.guard.clone())
216}
217
218fn guard_registry_release(raw: *const HddsGuardCondition) -> bool {
219    if raw.is_null() {
220        return false;
221    }
222
223    let mut registry = guard_registry()
224        .lock()
225        .unwrap_or_else(|err| err.into_inner());
226    let Some(entry) = registry.get_mut(&(raw as usize)) else {
227        return false;
228    };
229
230    if entry.handles == 0 {
231        return false;
232    }
233
234    entry.handles -= 1;
235    if entry.handles == 0 {
236        registry.remove(&(raw as usize));
237    }
238
239    true
240}
241
242fn status_registry_add_handle(raw: *const HddsStatusCondition, status: Arc<StatusCondition>) {
243    if raw.is_null() {
244        return;
245    }
246
247    let mut registry = status_registry()
248        .lock()
249        .unwrap_or_else(|err| err.into_inner());
250    let entry = registry
251        .entry(raw as usize)
252        .or_insert_with(|| StatusRegistryEntry { status, handles: 0 });
253    entry.handles = entry.handles.saturating_add(1);
254}
255
256fn status_registry_clone(raw: *const HddsStatusCondition) -> Option<Arc<StatusCondition>> {
257    if raw.is_null() {
258        return None;
259    }
260
261    let registry = status_registry()
262        .lock()
263        .unwrap_or_else(|err| err.into_inner());
264    registry
265        .get(&(raw as usize))
266        .map(|entry| entry.status.clone())
267}
268
269fn status_registry_release(raw: *const HddsStatusCondition) -> bool {
270    if raw.is_null() {
271        return false;
272    }
273
274    let mut registry = status_registry()
275        .lock()
276        .unwrap_or_else(|err| err.into_inner());
277    let Some(entry) = registry.get_mut(&(raw as usize)) else {
278        return false;
279    };
280
281    if entry.handles == 0 {
282        return false;
283    }
284
285    entry.handles -= 1;
286    if entry.handles == 0 {
287        registry.remove(&(raw as usize));
288    }
289
290    true
291}
292
293#[cfg(feature = "xtypes")]
294fn distro_from_u32(value: u32) -> Option<Distro> {
295    match value {
296        0 => Some(Distro::Humble),
297        1 => Some(Distro::Iron),
298        2 => Some(Distro::Jazzy),
299        _ => None,
300    }
301}
302
303#[cfg(feature = "xtypes")]
304fn map_rosidl_error(err: &RosidlError) -> HddsError {
305    match err {
306        RosidlError::NullTypeSupport
307        | RosidlError::NullMembers
308        | RosidlError::InvalidUtf8(_)
309        | RosidlError::BoundOverflow { .. }
310        | RosidlError::UnsupportedType(_) => HddsError::HddsInvalidArgument,
311        RosidlError::MissingHash | RosidlError::Builder(_) => HddsError::HddsOperationFailed,
312    }
313}
314
315/// Generic payload wrapper for serialization
316#[derive(Serialize, Deserialize, Clone)]
317pub(crate) struct BytePayload {
318    data: Vec<u8>,
319}
320
321/// Implement DDS trait for `BytePayload`
322impl BytePayload {
323    #[cfg(test)]
324    #[allow(dead_code)]
325    pub(crate) fn as_slice(&self) -> &[u8] {
326        &self.data
327    }
328}
329
330impl DDS for BytePayload {
331    fn type_descriptor() -> &'static TypeDescriptor {
332        // Simple type descriptor for opaque byte array
333        // type_name MUST be "RawBytes" to match hdds-ws (create_raw_writer/reader)
334        // so that SEDP type matching succeeds in cross-process communication.
335        static DESCRIPTOR: TypeDescriptor = TypeDescriptor {
336            type_id: 0x0000_0000, // Special ID for opaque data
337            type_name: "RawBytes",
338            size_bytes: 0, // Variable size
339            alignment: 1,  // Raw byte alignment
340            is_variable_size: true,
341            fields: &[],
342        };
343        &DESCRIPTOR
344    }
345
346    fn encode_cdr2(&self, buf: &mut [u8]) -> hdds::api::Result<usize> {
347        // Raw byte payload (no length prefix)
348        if buf.len() < self.data.len() {
349            return Err(hdds::api::Error::BufferTooSmall);
350        }
351
352        buf[..self.data.len()].copy_from_slice(&self.data);
353        Ok(self.data.len())
354    }
355
356    fn decode_cdr2(buf: &[u8]) -> hdds::api::Result<Self> {
357        Ok(BytePayload { data: buf.to_vec() })
358    }
359}
360
361/// Error codes (C-compatible enum)
362///
363/// # Error Code Categories
364///
365/// - **0-9**: Success and generic errors
366/// - **10-19**: Configuration errors
367/// - **20-29**: I/O and transport errors
368/// - **30-39**: Type and serialization errors
369/// - **40-49**: QoS and resource errors
370/// - **50-59**: Security errors
371#[repr(C)]
372#[derive(Debug, Clone, Copy, PartialEq, Eq)]
373pub enum HddsError {
374    /// Operation completed successfully
375    HddsOk = 0,
376    /// Invalid argument provided (null pointer, invalid value)
377    HddsInvalidArgument = 1,
378    /// Requested resource not found
379    HddsNotFound = 2,
380    /// Generic operation failure
381    HddsOperationFailed = 3,
382    /// Memory allocation failed
383    HddsOutOfMemory = 4,
384
385    // === Configuration errors (10-19) ===
386    /// Invalid configuration settings
387    HddsConfigError = 10,
388    /// Invalid domain ID (must be 0-232)
389    HddsInvalidDomainId = 11,
390    /// Invalid participant ID (must be 0-119)
391    HddsInvalidParticipantId = 12,
392    /// No available participant ID (all 120 ports occupied)
393    HddsNoAvailableParticipantId = 13,
394    /// Invalid entity state for requested operation
395    HddsInvalidState = 14,
396
397    // === I/O and transport errors (20-29) ===
398    /// Generic I/O error
399    HddsIoError = 20,
400    /// UDP transport send/receive failed
401    HddsTransportError = 21,
402    /// Topic registration failed
403    HddsRegistrationFailed = 22,
404    /// Operation would block but non-blocking mode requested
405    HddsWouldBlock = 23,
406
407    // === Type and serialization errors (30-39) ===
408    /// Type mismatch between writer and reader
409    HddsTypeMismatch = 30,
410    /// CDR serialization failed
411    HddsSerializationError = 31,
412    /// Buffer too small for encoding
413    HddsBufferTooSmall = 32,
414    /// CDR endianness mismatch
415    HddsEndianMismatch = 33,
416
417    // === QoS and resource errors (40-49) ===
418    /// QoS policies are incompatible between endpoints
419    HddsQosIncompatible = 40,
420    /// Requested feature or operation is not supported
421    HddsUnsupported = 41,
422
423    // === Security errors (50-59) ===
424    /// Permission denied by access control (DDS Security)
425    HddsPermissionDenied = 50,
426    /// Authentication failed
427    HddsAuthenticationFailed = 51,
428}
429
430/// Transport mode for participant creation
431#[repr(C)]
432#[derive(Debug, Clone, Copy, PartialEq, Eq)]
433pub enum HddsTransportMode {
434    /// Intra-process only (no network, fastest for same-process communication)
435    HddsTransportIntraProcess = 0,
436    /// UDP multicast for network discovery and communication (default for DDS interop)
437    HddsTransportUdpMulticast = 1,
438}
439
440/// Create a new DDS Participant with default settings (UdpMulticast transport)
441///
442/// For network DDS communication, this is the recommended function.
443/// Use `hdds_participant_create_with_transport` if you need intra-process mode.
444///
445/// # Safety
446/// - `name` must be a valid null-terminated C string.
447/// - The returned handle must be released with `hdds_participant_destroy`.
448#[no_mangle]
449pub unsafe extern "C" fn hdds_participant_create(name: *const c_char) -> *mut HddsParticipant {
450    hdds_participant_create_with_transport(name, HddsTransportMode::HddsTransportUdpMulticast)
451}
452
453/// Create a new DDS Participant with specified transport mode
454///
455/// # Safety
456/// - `name` must be a valid null-terminated C string.
457/// - The returned handle must be released with `hdds_participant_destroy`.
458///
459/// # Arguments
460/// * `name` - Participant name (null-terminated C string)
461/// * `transport` - Transport mode:
462///   - `HddsTransportMode::HddsTransportIntraProcess` (0): No network, intra-process only
463///   - `HddsTransportMode::HddsTransportUdpMulticast` (1): UDP multicast for network discovery
464///
465/// # Returns
466/// Opaque participant handle, or NULL on failure
467#[no_mangle]
468pub unsafe extern "C" fn hdds_participant_create_with_transport(
469    name: *const c_char,
470    transport: HddsTransportMode,
471) -> *mut HddsParticipant {
472    // Initialize logger (only once, subsequent calls are no-op)
473    use std::sync::Once;
474    static INIT: Once = Once::new();
475    INIT.call_once(|| {
476        let _ = env_logger::try_init();
477    });
478
479    if name.is_null() {
480        return ptr::null_mut();
481    }
482
483    let Ok(name_str) = CStr::from_ptr(name).to_str() else {
484        return ptr::null_mut();
485    };
486
487    use hdds::TransportMode;
488    let mode = match transport {
489        HddsTransportMode::HddsTransportIntraProcess => TransportMode::IntraProcess,
490        HddsTransportMode::HddsTransportUdpMulticast => TransportMode::UdpMulticast,
491    };
492
493    // Port assignment evolution:
494    // - v0.8.1: Added explicit .with_discovery_ports(7400, 7410, 7411) for RTPS compliance
495    // - v1.0.6: REMOVED with_discovery_ports() to enable multi-process support
496    //
497    // The builder now handles port assignment via (in priority order):
498    //   1. Code: .participant_id(Some(X))
499    //   2. Env:  HDDS_PARTICIPANT_ID=X
500    //   3. Auto: probe ports 7410+pid*2, 7411+pid*2 until free pair found
501    //
502    // For multi-process on same machine, set HDDS_REUSEPORT=1 and unique HDDS_PARTICIPANT_ID:
503    //   HDDS_REUSEPORT=1 HDDS_PARTICIPANT_ID=0 ./daemon   # ports 7410, 7411
504    //   HDDS_REUSEPORT=1 HDDS_PARTICIPANT_ID=1 ./alpha    # ports 7412, 7413
505    //   HDDS_REUSEPORT=1 HDDS_PARTICIPANT_ID=2 ./beta     # ports 7414, 7415
506    // Domain ID: check HDDS_DOMAIN_ID env var (same as rmw-hdds), default to 0
507    let domain_id = std::env::var("HDDS_DOMAIN_ID")
508        .ok()
509        .and_then(|v| v.parse::<u32>().ok())
510        .unwrap_or(0);
511
512    if domain_id != 0 {
513        log::info!(
514            "[HDDS-C] Using domain_id={} from HDDS_DOMAIN_ID env",
515            domain_id
516        );
517    }
518
519    let Ok(participant) = Participant::builder(name_str)
520        .domain_id(domain_id)
521        .with_transport(mode)
522        .build()
523    else {
524        return ptr::null_mut();
525    };
526
527    // Store Arc<Participant> in a Box so we can get a stable pointer to the Arc itself
528    Box::into_raw(Box::new(participant)).cast::<HddsParticipant>()
529}
530
531/// Destroy a Participant
532///
533/// # Safety
534/// - `participant` must be a valid handle from `hdds_participant_create`, or NULL (no-op).
535/// - Must not be called more than once with the same pointer.
536#[no_mangle]
537pub unsafe extern "C" fn hdds_participant_destroy(participant: *mut HddsParticipant) {
538    if !participant.is_null() {
539        // Participant was stored as Box<Arc<Participant>>
540        let _ = Box::from_raw(participant.cast::<Arc<Participant>>());
541    }
542}
543
544/// Get the participant-level graph guard condition.
545///
546/// # Safety
547/// - `participant` must be a valid handle from `hdds_participant_create`.
548#[no_mangle]
549pub unsafe extern "C" fn hdds_participant_graph_guard_condition(
550    participant: *mut HddsParticipant,
551) -> *const HddsGuardCondition {
552    if participant.is_null() {
553        return ptr::null();
554    }
555
556    let participant_ref = &*participant.cast::<Arc<Participant>>();
557    let guard = participant_ref.graph_guard();
558    let raw = Arc::into_raw(guard.clone()) as *const HddsGuardCondition;
559    guard_registry_add_handle(raw, guard);
560    raw
561}
562
563/// Register a ROS 2 type support with the participant.
564///
565/// # Safety
566/// - `participant` must be a valid handle from `hdds_participant_create`.
567/// - `type_support` must be a valid `rosidl_message_type_support_t` pointer.
568/// - `out_handle` must be a valid pointer to write the result.
569#[cfg(feature = "xtypes")]
570#[no_mangle]
571pub unsafe extern "C" fn hdds_participant_register_type_support(
572    participant: *mut HddsParticipant,
573    distro: u32,
574    type_support: *const rosidl_message_type_support_t,
575    out_handle: *mut *const HddsTypeObject,
576) -> HddsError {
577    if participant.is_null() || type_support.is_null() || out_handle.is_null() {
578        return HddsError::HddsInvalidArgument;
579    }
580
581    let Some(distro) = distro_from_u32(distro) else {
582        return HddsError::HddsInvalidArgument;
583    };
584
585    let participant_ref = &*participant.cast::<Arc<Participant>>();
586    out_handle.write(std::ptr::null());
587
588    match unsafe { participant_ref.register_type_from_type_support(distro, type_support) } {
589        Ok(handle) => {
590            let raw: *const TypeObjectHandle = Arc::into_raw(handle);
591            out_handle.write(raw.cast::<HddsTypeObject>());
592            HddsError::HddsOk
593        }
594        Err(err) => map_rosidl_error(&err),
595    }
596}
597
598/// Release a type object handle.
599///
600/// # Safety
601/// - `handle` must be a valid handle from `hdds_participant_register_type_support`, or NULL.
602#[cfg(feature = "xtypes")]
603#[no_mangle]
604pub unsafe extern "C" fn hdds_type_object_release(handle: *const HddsTypeObject) {
605    if !handle.is_null() {
606        let _ = Arc::from_raw(handle.cast::<TypeObjectHandle>());
607    }
608}
609
610/// Get the type hash from a type object handle.
611///
612/// # Safety
613/// - `handle` must be a valid handle from `hdds_participant_register_type_support`.
614/// - `out_value` must point to a buffer of at least `value_len` bytes.
615#[cfg(feature = "xtypes")]
616#[no_mangle]
617pub unsafe extern "C" fn hdds_type_object_hash(
618    handle: *const HddsTypeObject,
619    out_version: *mut u8,
620    out_value: *mut u8,
621    value_len: usize,
622) -> HddsError {
623    if handle.is_null() || out_value.is_null() {
624        return HddsError::HddsInvalidArgument;
625    }
626
627    if value_len < ROS_HASH_SIZE {
628        return HddsError::HddsInvalidArgument;
629    }
630
631    let arc = Arc::from_raw(handle.cast::<TypeObjectHandle>());
632    if !out_version.is_null() {
633        *out_version = arc.ros_hash_version;
634    }
635    ptr::copy_nonoverlapping(arc.ros_hash.as_ref().as_ptr(), out_value, ROS_HASH_SIZE);
636    let _ = Arc::into_raw(arc);
637    HddsError::HddsOk
638}
639
640/// Get HDDS library version string
641///
642/// # Safety
643/// The returned pointer is valid for the lifetime of the process (static storage).
644#[no_mangle]
645pub unsafe extern "C" fn hdds_version() -> *const c_char {
646    static VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "\0");
647    VERSION.as_ptr().cast::<c_char>()
648}
649
650/// Create a `DataWriter` for a topic
651///
652/// # Safety
653/// - `participant` must be a valid pointer returned from `hdds_participant_create`
654/// - `topic_name` must be a valid null-terminated C string
655#[no_mangle]
656pub unsafe extern "C" fn hdds_writer_create(
657    participant: *mut HddsParticipant,
658    topic_name: *const c_char,
659) -> *mut HddsDataWriter {
660    if participant.is_null() || topic_name.is_null() {
661        return ptr::null_mut();
662    }
663
664    let Ok(topic_str) = CStr::from_ptr(topic_name).to_str() else {
665        return ptr::null_mut();
666    };
667
668    let participant_ref = &*participant.cast::<Arc<Participant>>();
669
670    let Ok(writer) = participant_ref.create_writer::<BytePayload>(topic_str, QoS::default()) else {
671        return ptr::null_mut();
672    };
673
674    Box::into_raw(Box::new(writer)).cast::<HddsDataWriter>()
675}
676
677/// Write data to a topic
678///
679/// # Safety
680/// - `writer` must be a valid pointer returned from `hdds_writer_create`
681/// - `data` must point to valid memory of at least `len` bytes
682#[no_mangle]
683pub unsafe extern "C" fn hdds_writer_write(
684    writer: *mut HddsDataWriter,
685    data: *const c_void,
686    len: usize,
687) -> HddsError {
688    if writer.is_null() || data.is_null() {
689        return HddsError::HddsInvalidArgument;
690    }
691
692    let writer_ref = &*writer.cast::<DataWriter<BytePayload>>();
693    let data_slice = std::slice::from_raw_parts(data.cast::<u8>(), len);
694
695    let payload = BytePayload {
696        data: data_slice.to_vec(),
697    };
698
699    match writer_ref.write(&payload) {
700        Ok(()) => HddsError::HddsOk,
701        Err(_) => HddsError::HddsOperationFailed,
702    }
703}
704
705/// Destroy a `DataWriter`
706///
707/// # Safety
708/// - `writer` must be a valid pointer returned from `hdds_writer_create`
709/// - Must not be called more than once with the same pointer
710#[no_mangle]
711pub unsafe extern "C" fn hdds_writer_destroy(writer: *mut HddsDataWriter) {
712    if !writer.is_null() {
713        let _ = Box::from_raw(writer.cast::<DataWriter<BytePayload>>());
714    }
715}
716
717/// Create a `DataWriter` for a topic with custom QoS
718///
719/// # Safety
720/// - `participant` must be a valid pointer returned from `hdds_participant_create`
721/// - `topic_name` must be a valid null-terminated C string
722/// - `qos` must be a valid pointer returned from `hdds_qos_*` functions (or NULL for default)
723#[no_mangle]
724pub unsafe extern "C" fn hdds_writer_create_with_qos(
725    participant: *mut HddsParticipant,
726    topic_name: *const c_char,
727    qos: *const HddsQoS,
728) -> *mut HddsDataWriter {
729    if participant.is_null() || topic_name.is_null() {
730        return ptr::null_mut();
731    }
732
733    let Ok(topic_str) = CStr::from_ptr(topic_name).to_str() else {
734        return ptr::null_mut();
735    };
736
737    let participant_ref = &*participant.cast::<Arc<Participant>>();
738
739    // Use provided QoS or default
740    let qos_value = if qos.is_null() {
741        QoS::default()
742    } else {
743        (*qos.cast::<QoS>()).clone()
744    };
745
746    let Ok(writer) = participant_ref.create_writer::<BytePayload>(topic_str, qos_value) else {
747        return ptr::null_mut();
748    };
749
750    Box::into_raw(Box::new(writer)).cast::<HddsDataWriter>()
751}
752
753/// Create a `DataWriter` for a topic with custom QoS and explicit type name.
754///
755/// The `type_name` is announced via SEDP and must match the remote endpoint's
756/// type name for topic matching (e.g. `"P_Mount_PSM::C_Rotational_Mount_setPosition"`).
757/// If `type_name` is NULL, falls back to the default `"RawBytes"` type name.
758///
759/// # Safety
760/// - `participant` must be a valid pointer returned from `hdds_participant_create`
761/// - `topic_name` must be a valid null-terminated C string
762/// - `type_name` must be a valid null-terminated C string (or NULL for default)
763/// - `qos` must be a valid pointer returned from `hdds_qos_*` functions (or NULL for default)
764#[no_mangle]
765pub unsafe extern "C" fn hdds_writer_create_with_type(
766    participant: *mut HddsParticipant,
767    topic_name: *const c_char,
768    type_name: *const c_char,
769    qos: *const HddsQoS,
770) -> *mut HddsDataWriter {
771    if participant.is_null() || topic_name.is_null() {
772        return ptr::null_mut();
773    }
774
775    let Ok(topic_str) = CStr::from_ptr(topic_name).to_str() else {
776        return ptr::null_mut();
777    };
778
779    let participant_ref = &*participant.cast::<Arc<Participant>>();
780
781    let qos_value = if qos.is_null() {
782        QoS::default()
783    } else {
784        (*qos.cast::<QoS>()).clone()
785    };
786
787    // If type_name is provided, use create_writer_with_type for SEDP type matching
788    if !type_name.is_null() {
789        if let Ok(type_str) = CStr::from_ptr(type_name).to_str() {
790            if !type_str.is_empty() {
791                log::info!(
792                    "[HDDS-C] Creating writer topic='{}' type='{}'",
793                    topic_str,
794                    type_str
795                );
796                let Ok(writer) = participant_ref
797                    .create_writer_with_type::<BytePayload>(topic_str, qos_value, type_str, None)
798                else {
799                    return ptr::null_mut();
800                };
801                return Box::into_raw(Box::new(writer)).cast::<HddsDataWriter>();
802            }
803        }
804    }
805
806    // Fallback: no type override
807    let Ok(writer) = participant_ref.create_writer::<BytePayload>(topic_str, qos_value) else {
808        return ptr::null_mut();
809    };
810
811    Box::into_raw(Box::new(writer)).cast::<HddsDataWriter>()
812}
813
814/// Create a `DataReader` for a topic
815///
816/// # Safety
817/// - `participant` must be a valid pointer returned from `hdds_participant_create`
818/// - `topic_name` must be a valid null-terminated C string
819#[no_mangle]
820pub unsafe extern "C" fn hdds_reader_create(
821    participant: *mut HddsParticipant,
822    topic_name: *const c_char,
823) -> *mut HddsDataReader {
824    if participant.is_null() || topic_name.is_null() {
825        return ptr::null_mut();
826    }
827
828    let Ok(topic_str) = CStr::from_ptr(topic_name).to_str() else {
829        return ptr::null_mut();
830    };
831
832    let participant_ref = &*participant.cast::<Arc<Participant>>();
833
834    let Ok(reader) = participant_ref.create_reader::<BytePayload>(topic_str, QoS::default()) else {
835        return ptr::null_mut();
836    };
837
838    Box::into_raw(Box::new(reader)).cast::<HddsDataReader>()
839}
840
841/// Create a `DataReader` for a topic with custom QoS
842///
843/// # Safety
844/// - `participant` must be a valid pointer returned from `hdds_participant_create`
845/// - `topic_name` must be a valid null-terminated C string
846/// - `qos` must be a valid pointer returned from `hdds_qos_*` functions (or NULL for default)
847#[no_mangle]
848pub unsafe extern "C" fn hdds_reader_create_with_qos(
849    participant: *mut HddsParticipant,
850    topic_name: *const c_char,
851    qos: *const HddsQoS,
852) -> *mut HddsDataReader {
853    if participant.is_null() || topic_name.is_null() {
854        return ptr::null_mut();
855    }
856
857    let Ok(topic_str) = CStr::from_ptr(topic_name).to_str() else {
858        return ptr::null_mut();
859    };
860
861    let participant_ref = &*participant.cast::<Arc<Participant>>();
862
863    // Use provided QoS or default
864    let qos_value = if qos.is_null() {
865        QoS::default()
866    } else {
867        (*qos.cast::<QoS>()).clone()
868    };
869
870    let Ok(reader) = participant_ref.create_reader::<BytePayload>(topic_str, qos_value) else {
871        return ptr::null_mut();
872    };
873
874    Box::into_raw(Box::new(reader)).cast::<HddsDataReader>()
875}
876
877/// Create a `DataReader` for a topic with custom QoS and explicit type name.
878///
879/// The `type_name` is announced via SEDP and must match the remote endpoint's
880/// type name for topic matching (e.g. `"P_Mount_PSM::C_Rotational_Mount_setPosition"`).
881/// If `type_name` is NULL, falls back to the default `"RawBytes"` type name.
882///
883/// # Safety
884/// - `participant` must be a valid pointer returned from `hdds_participant_create`
885/// - `topic_name` must be a valid null-terminated C string
886/// - `type_name` must be a valid null-terminated C string (or NULL for default)
887/// - `qos` must be a valid pointer returned from `hdds_qos_*` functions (or NULL for default)
888#[no_mangle]
889pub unsafe extern "C" fn hdds_reader_create_with_type(
890    participant: *mut HddsParticipant,
891    topic_name: *const c_char,
892    type_name: *const c_char,
893    qos: *const HddsQoS,
894) -> *mut HddsDataReader {
895    if participant.is_null() || topic_name.is_null() {
896        return ptr::null_mut();
897    }
898
899    let Ok(topic_str) = CStr::from_ptr(topic_name).to_str() else {
900        return ptr::null_mut();
901    };
902
903    let participant_ref = &*participant.cast::<Arc<Participant>>();
904
905    let qos_value = if qos.is_null() {
906        QoS::default()
907    } else {
908        (*qos.cast::<QoS>()).clone()
909    };
910
911    // If type_name is provided, use create_reader_with_type for SEDP type matching
912    if !type_name.is_null() {
913        if let Ok(type_str) = CStr::from_ptr(type_name).to_str() {
914            if !type_str.is_empty() {
915                log::info!(
916                    "[HDDS-C] Creating reader topic='{}' type='{}'",
917                    topic_str,
918                    type_str
919                );
920                let Ok(reader) = participant_ref
921                    .create_reader_with_type::<BytePayload>(topic_str, qos_value, type_str, None)
922                else {
923                    return ptr::null_mut();
924                };
925                return Box::into_raw(Box::new(reader)).cast::<HddsDataReader>();
926            }
927        }
928    }
929
930    // Fallback: no type override
931    let Ok(reader) = participant_ref.create_reader::<BytePayload>(topic_str, qos_value) else {
932        return ptr::null_mut();
933    };
934
935    Box::into_raw(Box::new(reader)).cast::<HddsDataReader>()
936}
937
938/// Take data from a topic (non-blocking)
939///
940/// # Safety
941/// - `reader` must be a valid pointer returned from `hdds_reader_create`
942/// - `data_out` must point to a valid buffer of at least `max_len` bytes
943/// - `len_out` must be a valid pointer to write the actual data length
944#[no_mangle]
945pub unsafe extern "C" fn hdds_reader_take(
946    reader: *mut HddsDataReader,
947    data_out: *mut c_void,
948    max_len: usize,
949    len_out: *mut usize,
950) -> HddsError {
951    if reader.is_null() || data_out.is_null() || len_out.is_null() {
952        return HddsError::HddsInvalidArgument;
953    }
954
955    let reader_ref = &*reader.cast::<DataReader<BytePayload>>();
956
957    match reader_ref.take() {
958        Ok(Some(payload)) => {
959            let required = payload.data.len();
960            *len_out = required;
961            if required > max_len {
962                return HddsError::HddsOutOfMemory;
963            }
964
965            std::ptr::copy_nonoverlapping(payload.data.as_ptr(), data_out.cast::<u8>(), required);
966            HddsError::HddsOk
967        }
968        Ok(None) => HddsError::HddsNotFound,
969        Err(_) => HddsError::HddsOperationFailed,
970    }
971}
972
973/// Destroy a `DataReader`
974///
975/// # Safety
976/// - `reader` must be a valid pointer returned from `hdds_reader_create`
977/// - Must not be called more than once with the same pointer
978#[no_mangle]
979pub unsafe extern "C" fn hdds_reader_destroy(reader: *mut HddsDataReader) {
980    if !reader.is_null() {
981        let _ = Box::from_raw(reader.cast::<DataReader<BytePayload>>());
982    }
983}
984
985/// Get the status condition associated with a reader.
986///
987/// # Safety
988/// - `reader` must be a valid handle from `hdds_reader_create`.
989#[no_mangle]
990pub unsafe extern "C" fn hdds_reader_get_status_condition(
991    reader: *mut HddsDataReader,
992) -> *const HddsStatusCondition {
993    if reader.is_null() {
994        return ptr::null();
995    }
996
997    let reader_ref = &*reader.cast::<DataReader<BytePayload>>();
998    let condition = reader_ref.get_status_condition();
999    let raw = Arc::into_raw(condition.clone()) as *const HddsStatusCondition;
1000    status_registry_add_handle(raw, condition);
1001    raw
1002}
1003
1004/// Release a previously acquired status condition.
1005///
1006/// # Safety
1007/// - `condition` must be a valid handle from `hdds_reader_get_status_condition`.
1008#[no_mangle]
1009pub unsafe extern "C" fn hdds_status_condition_release(condition: *const HddsStatusCondition) {
1010    if status_registry_release(condition) {
1011        let _ = Arc::from_raw(condition.cast::<StatusCondition>());
1012    }
1013}
1014
1015/// Create a new guard condition.
1016///
1017/// # Safety
1018/// The returned handle must be released with `hdds_guard_condition_release`.
1019#[no_mangle]
1020pub unsafe extern "C" fn hdds_guard_condition_create() -> *const HddsGuardCondition {
1021    let guard = Arc::new(GuardCondition::new());
1022    let raw = Arc::into_raw(guard.clone()) as *const HddsGuardCondition;
1023    guard_registry_add_handle(raw, guard);
1024    raw
1025}
1026
1027/// Release a guard condition.
1028///
1029/// # Safety
1030/// - `condition` must be a valid handle from `hdds_guard_condition_create`.
1031#[no_mangle]
1032pub unsafe extern "C" fn hdds_guard_condition_release(condition: *const HddsGuardCondition) {
1033    if guard_registry_release(condition) {
1034        let _ = Arc::from_raw(condition.cast::<GuardCondition>());
1035    }
1036}
1037
1038/// Set a guard condition's trigger value.
1039///
1040/// # Safety
1041/// - `condition` must be a valid handle from `hdds_guard_condition_create`.
1042#[no_mangle]
1043pub unsafe extern "C" fn hdds_guard_condition_set_trigger(
1044    condition: *const HddsGuardCondition,
1045    active: bool,
1046) {
1047    let Some(guard) = guard_registry_clone(condition) else {
1048        return;
1049    };
1050    guard.set_trigger_value(active);
1051}
1052
1053/// Read a guard condition's current trigger value without modifying it.
1054///
1055/// # Safety
1056/// - `condition` must be a valid handle from `hdds_guard_condition_create`.
1057#[no_mangle]
1058pub unsafe extern "C" fn hdds_guard_condition_get_trigger(
1059    condition: *const HddsGuardCondition,
1060) -> bool {
1061    let Some(guard) = guard_registry_clone(condition) else {
1062        return false;
1063    };
1064    guard.get_trigger_value()
1065}
1066
1067/// Create a waitset.
1068///
1069/// # Safety
1070/// The returned handle must be released with `hdds_waitset_destroy`.
1071#[no_mangle]
1072pub unsafe extern "C" fn hdds_waitset_create() -> *mut HddsWaitSet {
1073    Box::into_raw(Box::new(ForeignWaitSet::new())) as *mut HddsWaitSet
1074}
1075
1076/// Destroy a waitset.
1077///
1078/// # Safety
1079/// - `waitset` must be a valid handle from `hdds_waitset_create`, or NULL (no-op).
1080#[no_mangle]
1081pub unsafe extern "C" fn hdds_waitset_destroy(waitset: *mut HddsWaitSet) {
1082    if !waitset.is_null() {
1083        let _ = Box::from_raw(waitset.cast::<ForeignWaitSet>());
1084    }
1085}
1086
1087/// Attach a status condition to a waitset.
1088///
1089/// # Safety
1090/// - `waitset` must be a valid handle from `hdds_waitset_create`.
1091/// - `condition` must be a valid handle from `hdds_reader_get_status_condition`.
1092#[no_mangle]
1093pub unsafe extern "C" fn hdds_waitset_attach_status_condition(
1094    waitset: *mut HddsWaitSet,
1095    condition: *const HddsStatusCondition,
1096) -> HddsError {
1097    if waitset.is_null() || condition.is_null() {
1098        return HddsError::HddsInvalidArgument;
1099    }
1100
1101    let waitset_ref = &*waitset.cast::<ForeignWaitSet>();
1102    let Some(clone) = status_registry_clone(condition) else {
1103        return HddsError::HddsInvalidArgument;
1104    };
1105
1106    match waitset_ref.attach_status(clone, condition.cast()) {
1107        Ok(()) => HddsError::HddsOk,
1108        Err(err) => err.into(),
1109    }
1110}
1111
1112/// Attach a guard condition to a waitset.
1113///
1114/// # Safety
1115/// - `waitset` must be a valid handle from `hdds_waitset_create`.
1116/// - `condition` must be a valid handle from `hdds_guard_condition_create`.
1117#[no_mangle]
1118pub unsafe extern "C" fn hdds_waitset_attach_guard_condition(
1119    waitset: *mut HddsWaitSet,
1120    condition: *const HddsGuardCondition,
1121) -> HddsError {
1122    if waitset.is_null() || condition.is_null() {
1123        return HddsError::HddsInvalidArgument;
1124    }
1125
1126    let waitset_ref = &*waitset.cast::<ForeignWaitSet>();
1127    let Some(clone) = guard_registry_clone(condition) else {
1128        return HddsError::HddsInvalidArgument;
1129    };
1130
1131    match waitset_ref.attach_guard(clone, condition.cast()) {
1132        Ok(()) => HddsError::HddsOk,
1133        Err(err) => err.into(),
1134    }
1135}
1136
1137/// Detach a condition (status or guard) from a waitset.
1138///
1139/// # Safety
1140/// - `waitset` must be a valid handle from `hdds_waitset_create`.
1141/// - `condition` must be a handle previously attached to this waitset.
1142#[no_mangle]
1143pub unsafe extern "C" fn hdds_waitset_detach_condition(
1144    waitset: *mut HddsWaitSet,
1145    condition: *const c_void,
1146) -> HddsError {
1147    if waitset.is_null() || condition.is_null() {
1148        return HddsError::HddsInvalidArgument;
1149    }
1150
1151    let waitset_ref = &*waitset.cast::<ForeignWaitSet>();
1152    match waitset_ref.detach(condition) {
1153        Ok(()) => HddsError::HddsOk,
1154        Err(err) => err.into(),
1155    }
1156}
1157
1158/// Wait for any attached condition to trigger.
1159///
1160/// # Safety
1161/// - `waitset` must be a valid handle from `hdds_waitset_create`.
1162/// - `out_conditions` must point to an array of at least `max_conditions` pointers.
1163/// - `out_len` must be a valid pointer.
1164#[no_mangle]
1165pub unsafe extern "C" fn hdds_waitset_wait(
1166    waitset: *mut HddsWaitSet,
1167    timeout_ns: i64,
1168    out_conditions: *mut *const c_void,
1169    max_conditions: usize,
1170    out_len: *mut usize,
1171) -> HddsError {
1172    if waitset.is_null() || out_conditions.is_null() || out_len.is_null() {
1173        return HddsError::HddsInvalidArgument;
1174    }
1175
1176    *out_len = 0;
1177
1178    let waitset_ref = &*waitset.cast::<ForeignWaitSet>();
1179    let timeout = if timeout_ns < 0 {
1180        None
1181    } else {
1182        match u64::try_from(timeout_ns) {
1183            Ok(nanos) => Some(Duration::from_nanos(nanos)),
1184            Err(_) => None,
1185        }
1186    };
1187
1188    let triggered = match waitset_ref.wait(timeout) {
1189        Ok(list) => list,
1190        Err(err) => return err.into(),
1191    };
1192
1193    if triggered.len() > max_conditions {
1194        return HddsError::HddsOperationFailed;
1195    }
1196
1197    for (idx, ptr_value) in triggered.iter().enumerate() {
1198        *out_conditions.add(idx) = *ptr_value;
1199    }
1200
1201    *out_len = triggered.len();
1202    HddsError::HddsOk
1203}
1204
1205// =============================================================================
1206// RMW (ROS Middleware) API - only available with "rmw" feature
1207// =============================================================================
1208
1209/// Create a new rmw context.
1210///
1211/// # Safety
1212/// Caller must ensure all pointer arguments are valid or NULL.
1213#[cfg(feature = "rmw")]
1214#[no_mangle]
1215pub unsafe extern "C" fn hdds_rmw_context_create(name: *const c_char) -> *mut HddsRmwContext {
1216    if name.is_null() {
1217        return ptr::null_mut();
1218    }
1219
1220    let Ok(name_str) = CStr::from_ptr(name).to_str() else {
1221        return ptr::null_mut();
1222    };
1223
1224    match ForeignRmwContext::create(name_str) {
1225        Ok(ctx) => {
1226            #[allow(clippy::arc_with_non_send_sync)]
1227            let arc = Arc::new(ctx);
1228            Box::into_raw(Box::new(arc)).cast::<HddsRmwContext>()
1229        }
1230        Err(_err) => ptr::null_mut(),
1231    }
1232}
1233
1234/// Destroy an rmw context.
1235///
1236/// # Safety
1237/// Caller must ensure all pointer arguments are valid or NULL.
1238#[cfg(feature = "rmw")]
1239#[no_mangle]
1240pub unsafe extern "C" fn hdds_rmw_context_destroy(ctx: *mut HddsRmwContext) {
1241    if !ctx.is_null() {
1242        let _ = Box::from_raw(ctx.cast::<Arc<ForeignRmwContext>>());
1243    }
1244}
1245
1246/// Get the graph guard key associated with the context.
1247///
1248/// # Safety
1249/// Caller must ensure all pointer arguments are valid or NULL.
1250#[cfg(feature = "rmw")]
1251#[no_mangle]
1252pub unsafe extern "C" fn hdds_rmw_context_graph_guard_key(ctx: *mut HddsRmwContext) -> u64 {
1253    if ctx.is_null() {
1254        return 0;
1255    }
1256    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1257    ctx_ref.as_ref().graph_guard_key()
1258}
1259
1260/// Copy the participant GUID prefix (12 bytes) into `out_prefix`.
1261///
1262/// Returns the participant's stable GUID prefix, suitable for building
1263/// cross-process unique GIDs (rmw_gid_t).
1264///
1265/// # Safety
1266/// Caller must ensure all pointer arguments are valid or NULL.
1267#[cfg(feature = "rmw")]
1268#[no_mangle]
1269pub unsafe extern "C" fn hdds_rmw_context_guid_prefix(
1270    ctx: *mut HddsRmwContext,
1271    out_prefix: *mut u8,
1272) -> HddsError {
1273    if ctx.is_null() || out_prefix.is_null() {
1274        return HddsError::HddsInvalidArgument;
1275    }
1276    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1277    let prefix = ctx_ref.guid_prefix();
1278    std::ptr::copy_nonoverlapping(prefix.as_ptr(), out_prefix, 12);
1279    HddsError::HddsOk
1280}
1281
1282/// Get the graph guard condition associated with the context.
1283///
1284/// # Safety
1285/// Caller must ensure all pointer arguments are valid or NULL.
1286#[cfg(feature = "rmw")]
1287#[no_mangle]
1288pub unsafe extern "C" fn hdds_rmw_context_graph_guard_condition(
1289    ctx: *mut HddsRmwContext,
1290) -> *const HddsGuardCondition {
1291    if ctx.is_null() {
1292        return ptr::null();
1293    }
1294
1295    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1296    let guard = ctx_ref.as_ref().graph_guard_condition();
1297    let raw = Arc::into_raw(guard.clone());
1298    guard_registry_add_handle(raw.cast::<HddsGuardCondition>(), guard);
1299    ctx_ref.as_ref().register_graph_guard_ptr(raw);
1300    raw.cast::<HddsGuardCondition>()
1301}
1302
1303/// Attach a guard condition to the rmw waitset.
1304///
1305/// # Safety
1306/// Caller must ensure all pointer arguments are valid or NULL.
1307#[cfg(feature = "rmw")]
1308#[no_mangle]
1309pub unsafe extern "C" fn hdds_rmw_context_attach_guard_condition(
1310    ctx: *mut HddsRmwContext,
1311    guard: *const HddsGuardCondition,
1312    out_key: *mut u64,
1313) -> HddsError {
1314    if ctx.is_null() || guard.is_null() || out_key.is_null() {
1315        return HddsError::HddsInvalidArgument;
1316    }
1317
1318    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1319    let Some(guard_clone) = guard_registry_clone(guard) else {
1320        return HddsError::HddsInvalidArgument;
1321    };
1322
1323    match ctx_ref
1324        .as_ref()
1325        .attach_guard(guard_clone, guard.cast::<GuardCondition>())
1326    {
1327        Ok(key) => {
1328            *out_key = key;
1329            HddsError::HddsOk
1330        }
1331        Err(err) => map_api_error(err),
1332    }
1333}
1334
1335/// Attach a status condition to the rmw waitset.
1336///
1337/// # Safety
1338/// Caller must ensure all pointer arguments are valid or NULL.
1339#[cfg(feature = "rmw")]
1340#[no_mangle]
1341pub unsafe extern "C" fn hdds_rmw_context_attach_status_condition(
1342    ctx: *mut HddsRmwContext,
1343    status: *const HddsStatusCondition,
1344    out_key: *mut u64,
1345) -> HddsError {
1346    if ctx.is_null() || status.is_null() || out_key.is_null() {
1347        return HddsError::HddsInvalidArgument;
1348    }
1349
1350    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1351    let Some(status_clone) = status_registry_clone(status) else {
1352        return HddsError::HddsInvalidArgument;
1353    };
1354
1355    match ctx_ref
1356        .as_ref()
1357        .attach_status(status_clone, status.cast::<StatusCondition>())
1358    {
1359        Ok(key) => {
1360            *out_key = key;
1361            HddsError::HddsOk
1362        }
1363        Err(err) => map_api_error(err),
1364    }
1365}
1366
1367/// Attach a reader to the rmw waitset (convenience helper).
1368///
1369/// # Safety
1370/// Caller must ensure all pointer arguments are valid or NULL.
1371#[cfg(feature = "rmw")]
1372#[no_mangle]
1373pub unsafe extern "C" fn hdds_rmw_context_attach_reader(
1374    ctx: *mut HddsRmwContext,
1375    reader: *mut HddsDataReader,
1376    out_key: *mut u64,
1377) -> HddsError {
1378    if ctx.is_null() || reader.is_null() || out_key.is_null() {
1379        return HddsError::HddsInvalidArgument;
1380    }
1381
1382    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1383    let reader_ptr = reader.cast::<c_void>();
1384    let status_arc = match ctx_ref.status_for_reader(reader_ptr) {
1385        Ok(status) => status,
1386        Err(err) => return map_api_error(err),
1387    };
1388    let raw_status = Arc::as_ptr(&status_arc);
1389
1390    match ctx_ref.attach_reader(reader_ptr, status_arc, raw_status) {
1391        Ok(key) => {
1392            *out_key = key;
1393            HddsError::HddsOk
1394        }
1395        Err(err) => map_api_error(err),
1396    }
1397}
1398
1399/// Create a DataReader bound to the rmw context participant.
1400///
1401/// # Safety
1402/// Caller must ensure all pointer arguments are valid or NULL.
1403#[cfg(feature = "rmw")]
1404#[no_mangle]
1405pub unsafe extern "C" fn hdds_rmw_context_create_reader(
1406    ctx: *mut HddsRmwContext,
1407    topic_name: *const c_char,
1408    out_reader: *mut *mut HddsDataReader,
1409) -> HddsError {
1410    if ctx.is_null() || topic_name.is_null() || out_reader.is_null() {
1411        return HddsError::HddsInvalidArgument;
1412    }
1413
1414    let Ok(topic_str) = CStr::from_ptr(topic_name).to_str() else {
1415        return HddsError::HddsInvalidArgument;
1416    };
1417
1418    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1419    match ctx_ref.create_reader_raw(topic_str) {
1420        Ok(reader_ptr) => {
1421            out_reader.write(reader_ptr.cast::<HddsDataReader>());
1422            HddsError::HddsOk
1423        }
1424        Err(err) => map_api_error(err),
1425    }
1426}
1427
1428#[cfg(feature = "rmw")]
1429/// Create a DataReader bound to the rmw context participant with custom QoS.
1430///
1431/// # Safety
1432/// Caller must ensure all pointer arguments are valid or NULL.
1433#[no_mangle]
1434pub unsafe extern "C" fn hdds_rmw_context_create_reader_with_qos(
1435    ctx: *mut HddsRmwContext,
1436    topic_name: *const c_char,
1437    qos: *const HddsQoS,
1438    out_reader: *mut *mut HddsDataReader,
1439) -> HddsError {
1440    if ctx.is_null() || topic_name.is_null() || out_reader.is_null() {
1441        return HddsError::HddsInvalidArgument;
1442    }
1443
1444    let Ok(topic_str) = CStr::from_ptr(topic_name).to_str() else {
1445        return HddsError::HddsInvalidArgument;
1446    };
1447
1448    let qos_ref = if qos.is_null() {
1449        QoS::default()
1450    } else {
1451        (*qos.cast::<QoS>()).clone()
1452    };
1453
1454    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1455    match ctx_ref.create_reader_raw_with_qos(topic_str, &qos_ref) {
1456        Ok(reader_ptr) => {
1457            out_reader.write(reader_ptr.cast::<HddsDataReader>());
1458            HddsError::HddsOk
1459        }
1460        Err(err) => map_api_error(err),
1461    }
1462}
1463
1464/// Destroy a DataReader created via the rmw context.
1465///
1466/// # Safety
1467/// Caller must ensure all pointer arguments are valid or NULL.
1468#[cfg(feature = "rmw")]
1469#[no_mangle]
1470pub unsafe extern "C" fn hdds_rmw_context_destroy_reader(
1471    ctx: *mut HddsRmwContext,
1472    reader: *mut HddsDataReader,
1473) -> HddsError {
1474    if ctx.is_null() || reader.is_null() {
1475        return HddsError::HddsInvalidArgument;
1476    }
1477
1478    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1479    match ctx_ref.destroy_reader_raw(reader.cast::<c_void>()) {
1480        Ok(()) => HddsError::HddsOk,
1481        Err(err) => map_api_error(err),
1482    }
1483}
1484///
1485/// # Safety
1486/// Caller must ensure all pointer arguments are valid or NULL.
1487#[cfg(feature = "rmw")]
1488#[no_mangle]
1489pub unsafe extern "C" fn hdds_rmw_context_create_writer(
1490    ctx: *mut HddsRmwContext,
1491    topic_name: *const c_char,
1492    out_writer: *mut *mut HddsDataWriter,
1493) -> HddsError {
1494    if ctx.is_null() || topic_name.is_null() || out_writer.is_null() {
1495        return HddsError::HddsInvalidArgument;
1496    }
1497
1498    let Ok(topic_str) = CStr::from_ptr(topic_name).to_str() else {
1499        return HddsError::HddsInvalidArgument;
1500    };
1501
1502    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1503    match ctx_ref.create_writer_raw(topic_str) {
1504        Ok(writer_ptr) => {
1505            out_writer.write(writer_ptr.cast::<HddsDataWriter>());
1506            HddsError::HddsOk
1507        }
1508        Err(err) => map_api_error(err),
1509    }
1510}
1511///
1512/// # Safety
1513/// Caller must ensure all pointer arguments are valid or NULL.
1514#[cfg(feature = "rmw")]
1515#[no_mangle]
1516pub unsafe extern "C" fn hdds_rmw_context_create_writer_with_qos(
1517    ctx: *mut HddsRmwContext,
1518    topic_name: *const c_char,
1519    qos: *const HddsQoS,
1520    out_writer: *mut *mut HddsDataWriter,
1521) -> HddsError {
1522    if ctx.is_null() || topic_name.is_null() || out_writer.is_null() {
1523        return HddsError::HddsInvalidArgument;
1524    }
1525
1526    let Ok(topic_str) = CStr::from_ptr(topic_name).to_str() else {
1527        return HddsError::HddsInvalidArgument;
1528    };
1529
1530    let qos_ref = if qos.is_null() {
1531        QoS::default()
1532    } else {
1533        (*qos.cast::<QoS>()).clone()
1534    };
1535
1536    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1537    match ctx_ref.create_writer_raw_with_qos(topic_str, &qos_ref) {
1538        Ok(writer_ptr) => {
1539            out_writer.write(writer_ptr.cast::<HddsDataWriter>());
1540            HddsError::HddsOk
1541        }
1542        Err(err) => map_api_error(err),
1543    }
1544}
1545///
1546/// # Safety
1547/// Caller must ensure all pointer arguments are valid or NULL.
1548#[cfg(feature = "rmw")]
1549#[no_mangle]
1550pub unsafe extern "C" fn hdds_rmw_context_bind_topic_type(
1551    ctx: *mut HddsRmwContext,
1552    topic_name: *const c_char,
1553    type_support: *const rosidl_message_type_support_t,
1554) -> HddsError {
1555    if ctx.is_null() || topic_name.is_null() || type_support.is_null() {
1556        return HddsError::HddsInvalidArgument;
1557    }
1558
1559    let Ok(topic_str) = CStr::from_ptr(topic_name).to_str() else {
1560        return HddsError::HddsInvalidArgument;
1561    };
1562
1563    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1564    match ctx_ref.bind_topic_type(topic_str, type_support) {
1565        Ok(()) => HddsError::HddsOk,
1566        Err(ApiError::Config) => HddsError::HddsOk,
1567        Err(ApiError::Unsupported) => HddsError::HddsOk,
1568        Err(err) => map_api_error(err),
1569    }
1570}
1571///
1572/// # Safety
1573/// Caller must ensure all pointer arguments are valid or NULL.
1574#[cfg(feature = "rmw")]
1575#[no_mangle]
1576pub unsafe extern "C" fn hdds_rmw_context_destroy_writer(
1577    ctx: *mut HddsRmwContext,
1578    writer: *mut HddsDataWriter,
1579) -> HddsError {
1580    if ctx.is_null() || writer.is_null() {
1581        return HddsError::HddsInvalidArgument;
1582    }
1583
1584    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1585    match ctx_ref.destroy_writer_raw(writer.cast::<c_void>()) {
1586        Ok(()) => HddsError::HddsOk,
1587        Err(err) => map_api_error(err),
1588    }
1589}
1590///
1591/// # Safety
1592/// Caller must ensure all pointer arguments are valid or NULL.
1593#[cfg(feature = "rmw")]
1594#[no_mangle]
1595pub unsafe extern "C" fn hdds_rmw_context_register_node(
1596    ctx: *mut HddsRmwContext,
1597    node_name: *const c_char,
1598    node_namespace: *const c_char,
1599    node_enclave: *const c_char,
1600) -> HddsError {
1601    if ctx.is_null() || node_name.is_null() || node_namespace.is_null() {
1602        return HddsError::HddsInvalidArgument;
1603    }
1604
1605    let Ok(name_str) = CStr::from_ptr(node_name).to_str() else {
1606        return HddsError::HddsInvalidArgument;
1607    };
1608
1609    let Ok(namespace_str) = CStr::from_ptr(node_namespace).to_str() else {
1610        return HddsError::HddsInvalidArgument;
1611    };
1612
1613    let enclave_str = if node_enclave.is_null() {
1614        ""
1615    } else {
1616        match CStr::from_ptr(node_enclave).to_str() {
1617            Ok(value) => value,
1618            Err(_) => return HddsError::HddsInvalidArgument,
1619        }
1620    };
1621
1622    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1623    ctx_ref.register_node_info(name_str, namespace_str, enclave_str);
1624    HddsError::HddsOk
1625}
1626///
1627/// # Safety
1628/// Caller must ensure all pointer arguments are valid or NULL.
1629#[cfg(feature = "rmw")]
1630#[no_mangle]
1631pub unsafe extern "C" fn hdds_rmw_context_unregister_node(
1632    ctx: *mut HddsRmwContext,
1633    node_name: *const c_char,
1634    node_namespace: *const c_char,
1635) -> HddsError {
1636    if ctx.is_null() || node_name.is_null() || node_namespace.is_null() {
1637        return HddsError::HddsInvalidArgument;
1638    }
1639
1640    let Ok(name_str) = CStr::from_ptr(node_name).to_str() else {
1641        return HddsError::HddsInvalidArgument;
1642    };
1643
1644    let Ok(namespace_str) = CStr::from_ptr(node_namespace).to_str() else {
1645        return HddsError::HddsInvalidArgument;
1646    };
1647
1648    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1649    ctx_ref.unregister_node_info(name_str, namespace_str);
1650    HddsError::HddsOk
1651}
1652///
1653/// # Safety
1654/// Caller must ensure all pointer arguments are valid or NULL.
1655#[cfg(feature = "rmw")]
1656#[no_mangle]
1657pub unsafe extern "C" fn hdds_rmw_context_register_publisher_endpoint(
1658    ctx: *mut HddsRmwContext,
1659    node_name: *const c_char,
1660    node_namespace: *const c_char,
1661    topic_name: *const c_char,
1662    type_support: *const rosidl_message_type_support_t,
1663    endpoint_gid: *const u8,
1664    qos_profile: *const HddsRmwQosProfile,
1665) -> HddsError {
1666    if ctx.is_null() || node_name.is_null() || node_namespace.is_null() || topic_name.is_null() {
1667        return HddsError::HddsInvalidArgument;
1668    }
1669
1670    let Ok(name_str) = CStr::from_ptr(node_name).to_str() else {
1671        return HddsError::HddsInvalidArgument;
1672    };
1673
1674    let Ok(namespace_str) = CStr::from_ptr(node_namespace).to_str() else {
1675        return HddsError::HddsInvalidArgument;
1676    };
1677
1678    let Ok(topic_str) = CStr::from_ptr(topic_name).to_str() else {
1679        return HddsError::HddsInvalidArgument;
1680    };
1681
1682    let mut gid = [0u8; HDDS_RMW_GID_SIZE];
1683    if !endpoint_gid.is_null() {
1684        let src = slice::from_raw_parts(endpoint_gid, gid.len());
1685        gid.copy_from_slice(src);
1686    }
1687
1688    let qos = if qos_profile.is_null() {
1689        hdds::rmw::graph::EndpointQos::default()
1690    } else {
1691        hdds::rmw::graph::EndpointQos::from(*qos_profile)
1692    };
1693
1694    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1695    match ctx_ref.register_publisher_endpoint(
1696        name_str,
1697        namespace_str,
1698        topic_str,
1699        type_support,
1700        gid,
1701        qos,
1702    ) {
1703        Ok(()) => HddsError::HddsOk,
1704        Err(ApiError::Config) => HddsError::HddsOk,
1705        Err(ApiError::Unsupported) => HddsError::HddsOk,
1706        Err(err) => map_api_error(err),
1707    }
1708}
1709///
1710/// # Safety
1711/// Caller must ensure all pointer arguments are valid or NULL.
1712#[cfg(feature = "rmw")]
1713#[no_mangle]
1714pub unsafe extern "C" fn hdds_rmw_context_unregister_publisher_endpoint(
1715    ctx: *mut HddsRmwContext,
1716    node_name: *const c_char,
1717    node_namespace: *const c_char,
1718    topic_name: *const c_char,
1719    endpoint_gid: *const u8,
1720) -> HddsError {
1721    if ctx.is_null() || node_name.is_null() || node_namespace.is_null() || topic_name.is_null() {
1722        return HddsError::HddsInvalidArgument;
1723    }
1724
1725    let Ok(name_str) = CStr::from_ptr(node_name).to_str() else {
1726        return HddsError::HddsInvalidArgument;
1727    };
1728
1729    let Ok(namespace_str) = CStr::from_ptr(node_namespace).to_str() else {
1730        return HddsError::HddsInvalidArgument;
1731    };
1732
1733    let Ok(topic_str) = CStr::from_ptr(topic_name).to_str() else {
1734        return HddsError::HddsInvalidArgument;
1735    };
1736
1737    let mut gid = [0u8; HDDS_RMW_GID_SIZE];
1738    if !endpoint_gid.is_null() {
1739        let src = slice::from_raw_parts(endpoint_gid, gid.len());
1740        gid.copy_from_slice(src);
1741    }
1742
1743    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1744    ctx_ref.unregister_publisher_endpoint(name_str, namespace_str, topic_str, &gid);
1745    HddsError::HddsOk
1746}
1747///
1748/// # Safety
1749/// Caller must ensure all pointer arguments are valid or NULL.
1750#[cfg(feature = "rmw")]
1751#[no_mangle]
1752pub unsafe extern "C" fn hdds_rmw_context_register_subscription_endpoint(
1753    ctx: *mut HddsRmwContext,
1754    node_name: *const c_char,
1755    node_namespace: *const c_char,
1756    topic_name: *const c_char,
1757    type_support: *const rosidl_message_type_support_t,
1758    endpoint_gid: *const u8,
1759    qos_profile: *const HddsRmwQosProfile,
1760) -> HddsError {
1761    if ctx.is_null() || node_name.is_null() || node_namespace.is_null() || topic_name.is_null() {
1762        return HddsError::HddsInvalidArgument;
1763    }
1764
1765    let Ok(name_str) = CStr::from_ptr(node_name).to_str() else {
1766        return HddsError::HddsInvalidArgument;
1767    };
1768
1769    let Ok(namespace_str) = CStr::from_ptr(node_namespace).to_str() else {
1770        return HddsError::HddsInvalidArgument;
1771    };
1772
1773    let Ok(topic_str) = CStr::from_ptr(topic_name).to_str() else {
1774        return HddsError::HddsInvalidArgument;
1775    };
1776
1777    let mut gid = [0u8; HDDS_RMW_GID_SIZE];
1778    if !endpoint_gid.is_null() {
1779        let src = slice::from_raw_parts(endpoint_gid, gid.len());
1780        gid.copy_from_slice(src);
1781    }
1782
1783    let qos = if qos_profile.is_null() {
1784        hdds::rmw::graph::EndpointQos::default()
1785    } else {
1786        hdds::rmw::graph::EndpointQos::from(*qos_profile)
1787    };
1788
1789    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1790    match ctx_ref.register_subscription_endpoint(
1791        name_str,
1792        namespace_str,
1793        topic_str,
1794        type_support,
1795        gid,
1796        qos,
1797    ) {
1798        Ok(()) => HddsError::HddsOk,
1799        Err(ApiError::Config) => HddsError::HddsOk,
1800        Err(ApiError::Unsupported) => HddsError::HddsOk,
1801        Err(err) => map_api_error(err),
1802    }
1803}
1804///
1805/// # Safety
1806/// Caller must ensure all pointer arguments are valid or NULL.
1807#[cfg(feature = "rmw")]
1808#[no_mangle]
1809pub unsafe extern "C" fn hdds_rmw_context_unregister_subscription_endpoint(
1810    ctx: *mut HddsRmwContext,
1811    node_name: *const c_char,
1812    node_namespace: *const c_char,
1813    topic_name: *const c_char,
1814    endpoint_gid: *const u8,
1815) -> HddsError {
1816    if ctx.is_null() || node_name.is_null() || node_namespace.is_null() || topic_name.is_null() {
1817        return HddsError::HddsInvalidArgument;
1818    }
1819
1820    let Ok(name_str) = CStr::from_ptr(node_name).to_str() else {
1821        return HddsError::HddsInvalidArgument;
1822    };
1823
1824    let Ok(namespace_str) = CStr::from_ptr(node_namespace).to_str() else {
1825        return HddsError::HddsInvalidArgument;
1826    };
1827
1828    let Ok(topic_str) = CStr::from_ptr(topic_name).to_str() else {
1829        return HddsError::HddsInvalidArgument;
1830    };
1831
1832    let mut gid = [0u8; HDDS_RMW_GID_SIZE];
1833    if !endpoint_gid.is_null() {
1834        let src = slice::from_raw_parts(endpoint_gid, gid.len());
1835        gid.copy_from_slice(src);
1836    }
1837
1838    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1839    ctx_ref.unregister_subscription_endpoint(name_str, namespace_str, topic_str, &gid);
1840    HddsError::HddsOk
1841}
1842///
1843/// # Safety
1844/// Caller must ensure all pointer arguments are valid or NULL.
1845#[cfg(feature = "rmw")]
1846#[no_mangle]
1847pub unsafe extern "C" fn hdds_rmw_context_for_each_node(
1848    ctx: *mut HddsRmwContext,
1849    visitor: HddsNodeVisitor,
1850    user_data: *mut c_void,
1851    out_version: *mut u64,
1852    out_count: *mut usize,
1853) -> HddsError {
1854    if ctx.is_null() {
1855        return HddsError::HddsInvalidArgument;
1856    }
1857
1858    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1859    let mut error: Option<HddsError> = None;
1860
1861    let (version, count) = ctx_ref.list_nodes_with(|name, namespace_| {
1862        if error.is_some() {
1863            return;
1864        }
1865
1866        if let Some(cb) = visitor {
1867            let Ok(name_cstr) = CString::new(name) else {
1868                error = Some(HddsError::HddsInvalidArgument);
1869                return;
1870            };
1871            let Ok(namespace_cstr) = CString::new(namespace_) else {
1872                error = Some(HddsError::HddsInvalidArgument);
1873                return;
1874            };
1875
1876            unsafe {
1877                cb(name_cstr.as_ptr(), namespace_cstr.as_ptr(), user_data);
1878            }
1879        }
1880    });
1881
1882    if !out_version.is_null() {
1883        out_version.write(version);
1884    }
1885
1886    if !out_count.is_null() {
1887        out_count.write(count);
1888    }
1889
1890    if let Some(err) = error {
1891        err
1892    } else {
1893        HddsError::HddsOk
1894    }
1895}
1896///
1897/// # Safety
1898/// Caller must ensure all pointer arguments are valid or NULL.
1899#[cfg(feature = "rmw")]
1900#[no_mangle]
1901pub unsafe extern "C" fn hdds_rmw_context_for_each_node_with_enclave(
1902    ctx: *mut HddsRmwContext,
1903    visitor: HddsNodeEnclaveVisitor,
1904    user_data: *mut c_void,
1905    out_version: *mut u64,
1906    out_count: *mut usize,
1907) -> HddsError {
1908    if ctx.is_null() {
1909        return HddsError::HddsInvalidArgument;
1910    }
1911
1912    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1913    let mut error: Option<HddsError> = None;
1914
1915    let (version, count) = ctx_ref.list_nodes_with_enclave(|name, namespace_, enclave| {
1916        if error.is_some() {
1917            return;
1918        }
1919
1920        if let Some(cb) = visitor {
1921            let Ok(name_cstr) = CString::new(name) else {
1922                error = Some(HddsError::HddsInvalidArgument);
1923                return;
1924            };
1925            let Ok(namespace_cstr) = CString::new(namespace_) else {
1926                error = Some(HddsError::HddsInvalidArgument);
1927                return;
1928            };
1929            let Ok(enclave_cstr) = CString::new(enclave) else {
1930                error = Some(HddsError::HddsInvalidArgument);
1931                return;
1932            };
1933
1934            unsafe {
1935                cb(
1936                    name_cstr.as_ptr(),
1937                    namespace_cstr.as_ptr(),
1938                    enclave_cstr.as_ptr(),
1939                    user_data,
1940                );
1941            }
1942        }
1943    });
1944
1945    if !out_version.is_null() {
1946        out_version.write(version);
1947    }
1948
1949    if !out_count.is_null() {
1950        out_count.write(count);
1951    }
1952
1953    if let Some(err) = error {
1954        err
1955    } else {
1956        HddsError::HddsOk
1957    }
1958}
1959///
1960/// # Safety
1961/// Caller must ensure all pointer arguments are valid or NULL.
1962#[cfg(feature = "rmw")]
1963#[no_mangle]
1964pub unsafe extern "C" fn hdds_rmw_context_for_each_publisher_endpoint(
1965    ctx: *mut HddsRmwContext,
1966    node_name: *const c_char,
1967    node_namespace: *const c_char,
1968    visitor: HddsEndpointVisitor,
1969    user_data: *mut c_void,
1970    out_version: *mut u64,
1971    out_count: *mut usize,
1972) -> HddsError {
1973    if ctx.is_null() || node_name.is_null() || node_namespace.is_null() {
1974        return HddsError::HddsInvalidArgument;
1975    }
1976
1977    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
1978    let Ok(name_str) = CStr::from_ptr(node_name).to_str() else {
1979        return HddsError::HddsInvalidArgument;
1980    };
1981    let Ok(namespace_str) = CStr::from_ptr(node_namespace).to_str() else {
1982        return HddsError::HddsInvalidArgument;
1983    };
1984
1985    let mut error: Option<HddsError> = None;
1986    let visit_result = ctx_ref.visit_publishers_with(name_str, namespace_str, |endpoint| {
1987        if error.is_some() {
1988            return;
1989        }
1990
1991        if let Some(cb) = visitor {
1992            let Ok(topic_cstr) = CString::new(endpoint.topic.as_str()) else {
1993                error = Some(HddsError::HddsInvalidArgument);
1994                return;
1995            };
1996            let type_name = normalize_ros_type_name(endpoint.type_name.as_str());
1997            let Ok(type_cstr) = CString::new(type_name.as_str()) else {
1998                error = Some(HddsError::HddsInvalidArgument);
1999                return;
2000            };
2001
2002            let qos_profile = HddsRmwQosProfile::from(&endpoint.qos);
2003            unsafe {
2004                cb(
2005                    topic_cstr.as_ptr(),
2006                    type_cstr.as_ptr(),
2007                    endpoint.gid.as_ptr(),
2008                    &qos_profile,
2009                    user_data,
2010                );
2011            }
2012        }
2013    });
2014
2015    match visit_result {
2016        Ok((version, count)) => {
2017            if !out_version.is_null() {
2018                out_version.write(version);
2019            }
2020            if !out_count.is_null() {
2021                out_count.write(count);
2022            }
2023
2024            if let Some(err) = error {
2025                err
2026            } else {
2027                HddsError::HddsOk
2028            }
2029        }
2030        Err(err) => map_api_error(err),
2031    }
2032}
2033///
2034/// # Safety
2035/// Caller must ensure all pointer arguments are valid or NULL.
2036#[cfg(feature = "rmw")]
2037#[no_mangle]
2038pub unsafe extern "C" fn hdds_rmw_context_for_each_subscription_endpoint(
2039    ctx: *mut HddsRmwContext,
2040    node_name: *const c_char,
2041    node_namespace: *const c_char,
2042    visitor: HddsEndpointVisitor,
2043    user_data: *mut c_void,
2044    out_version: *mut u64,
2045    out_count: *mut usize,
2046) -> HddsError {
2047    if ctx.is_null() || node_name.is_null() || node_namespace.is_null() {
2048        return HddsError::HddsInvalidArgument;
2049    }
2050
2051    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
2052    let Ok(name_str) = CStr::from_ptr(node_name).to_str() else {
2053        return HddsError::HddsInvalidArgument;
2054    };
2055    let Ok(namespace_str) = CStr::from_ptr(node_namespace).to_str() else {
2056        return HddsError::HddsInvalidArgument;
2057    };
2058
2059    let mut error: Option<HddsError> = None;
2060    let visit_result = ctx_ref.visit_subscriptions_with(name_str, namespace_str, |endpoint| {
2061        if error.is_some() {
2062            return;
2063        }
2064
2065        if let Some(cb) = visitor {
2066            let Ok(topic_cstr) = CString::new(endpoint.topic.as_str()) else {
2067                error = Some(HddsError::HddsInvalidArgument);
2068                return;
2069            };
2070            let type_name = normalize_ros_type_name(endpoint.type_name.as_str());
2071            let Ok(type_cstr) = CString::new(type_name.as_str()) else {
2072                error = Some(HddsError::HddsInvalidArgument);
2073                return;
2074            };
2075
2076            let qos_profile = HddsRmwQosProfile::from(&endpoint.qos);
2077            unsafe {
2078                cb(
2079                    topic_cstr.as_ptr(),
2080                    type_cstr.as_ptr(),
2081                    endpoint.gid.as_ptr(),
2082                    &qos_profile,
2083                    user_data,
2084                );
2085            }
2086        }
2087    });
2088
2089    match visit_result {
2090        Ok((version, count)) => {
2091            if !out_version.is_null() {
2092                out_version.write(version);
2093            }
2094            if !out_count.is_null() {
2095                out_count.write(count);
2096            }
2097
2098            if let Some(err) = error {
2099                err
2100            } else {
2101                HddsError::HddsOk
2102            }
2103        }
2104        Err(err) => map_api_error(err),
2105    }
2106}
2107///
2108/// # Safety
2109/// Caller must ensure all pointer arguments are valid or NULL.
2110#[cfg(feature = "rmw")]
2111#[no_mangle]
2112pub unsafe extern "C" fn hdds_rmw_context_for_each_topic(
2113    ctx: *mut HddsRmwContext,
2114    visitor: Option<HddsTopicVisitor>,
2115    user_data: *mut c_void,
2116    out_version: *mut u64,
2117) -> HddsError {
2118    if ctx.is_null() {
2119        return HddsError::HddsInvalidArgument;
2120    }
2121
2122    let Some(callback) = visitor else {
2123        return HddsError::HddsInvalidArgument;
2124    };
2125
2126    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
2127    let mut status = HddsError::HddsOk;
2128    let version = ctx_ref.for_each_topic(|topic, type_name, writers, readers| {
2129        if status != HddsError::HddsOk {
2130            return;
2131        }
2132
2133        let topic_c = match CString::new(topic) {
2134            Ok(value) => value,
2135            Err(_) => {
2136                status = HddsError::HddsInvalidArgument;
2137                return;
2138            }
2139        };
2140
2141        let type_c = match CString::new(type_name) {
2142            Ok(value) => value,
2143            Err(_) => {
2144                status = HddsError::HddsInvalidArgument;
2145                return;
2146            }
2147        };
2148
2149        unsafe {
2150            callback(
2151                topic_c.as_ptr(),
2152                type_c.as_ptr(),
2153                writers,
2154                readers,
2155                user_data,
2156            );
2157        }
2158    });
2159
2160    if !out_version.is_null() {
2161        out_version.write(version);
2162    }
2163
2164    status
2165}
2166///
2167/// # Safety
2168/// Caller must ensure all pointer arguments are valid or NULL.
2169#[cfg(feature = "rmw")]
2170#[no_mangle]
2171pub unsafe extern "C" fn hdds_rmw_context_for_each_user_locator(
2172    ctx: *mut HddsRmwContext,
2173    visitor: Option<HddsLocatorVisitor>,
2174    user_data: *mut c_void,
2175    out_count: *mut usize,
2176) -> HddsError {
2177    if ctx.is_null() {
2178        return HddsError::HddsInvalidArgument;
2179    }
2180
2181    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
2182    let locators = ctx_ref.user_unicast_locators();
2183    if !out_count.is_null() {
2184        out_count.write(locators.len());
2185    }
2186
2187    let Some(callback) = visitor else {
2188        return HddsError::HddsOk;
2189    };
2190
2191    for locator in locators {
2192        let addr = locator.ip().to_string();
2193        let Ok(addr_cstr) = CString::new(addr.as_str()) else {
2194            return HddsError::HddsInvalidArgument;
2195        };
2196        callback(addr_cstr.as_ptr(), locator.port(), user_data);
2197    }
2198
2199    HddsError::HddsOk
2200}
2201///
2202/// # Safety
2203/// Caller must ensure all pointer arguments are valid or NULL.
2204#[cfg(feature = "rmw")]
2205#[no_mangle]
2206pub unsafe extern "C" fn hdds_rmw_context_publish(
2207    ctx: *mut HddsRmwContext,
2208    writer: *mut HddsDataWriter,
2209    type_support: *const rosidl_message_type_support_t,
2210    ros_message: *const c_void,
2211) -> HddsError {
2212    if ctx.is_null() || writer.is_null() || type_support.is_null() || ros_message.is_null() {
2213        return HddsError::HddsInvalidArgument;
2214    }
2215
2216    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
2217    let payload = match serialize_from_ros(type_support, ros_message) {
2218        Ok(p) => Some(p),
2219        Err(ApiError::Config) => None,
2220        Err(err) => return map_api_error(err),
2221    };
2222
2223    if let Some(payload) = payload {
2224        match ctx_ref.publish_writer(writer.cast::<c_void>(), &payload) {
2225            Ok(()) => HddsError::HddsOk,
2226            Err(err) => map_api_error(err),
2227        }
2228    } else {
2229        HddsError::HddsOk
2230    }
2231}
2232///
2233/// # Safety
2234/// Caller must ensure all pointer arguments are valid or NULL.
2235#[cfg(feature = "rmw")]
2236#[no_mangle]
2237pub unsafe extern "C" fn hdds_rmw_context_publish_with_codec(
2238    ctx: *mut HddsRmwContext,
2239    writer: *mut HddsDataWriter,
2240    codec_kind: u8,
2241    ros_message: *const c_void,
2242) -> HddsError {
2243    if ctx.is_null() || writer.is_null() || ros_message.is_null() {
2244        return HddsError::HddsInvalidArgument;
2245    }
2246
2247    let Some(codec) = Ros2CodecKind::try_from(codec_kind) else {
2248        return HddsError::HddsInvalidArgument;
2249    };
2250
2251    if codec == Ros2CodecKind::None {
2252        return HddsError::HddsInvalidArgument;
2253    }
2254
2255    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
2256    match encode_special(codec, ros_message) {
2257        Ok(Some(payload)) => match ctx_ref.publish_writer(writer.cast(), &payload) {
2258            Ok(()) => HddsError::HddsOk,
2259            Err(err) => {
2260                // No subscribers or backpressure is not a hard error for rmw publish.
2261                if matches!(err, ApiError::WouldBlock) {
2262                    HddsError::HddsOk
2263                } else {
2264                    map_api_error(err)
2265                }
2266            }
2267        },
2268        Ok(None) => HddsError::HddsOperationFailed,
2269        Err(err) => map_api_error(err),
2270    }
2271}
2272
2273/// Try to read from SHM ring buffer for a topic (inter-process fast path).
2274///
2275/// Returns OK with `*len_out > 0` if data was read from SHM.
2276/// Returns NOT_FOUND if no SHM data available (caller should fall back to RTPS).
2277///
2278/// # Safety
2279/// - `ctx` must be a valid `HddsRmwContext`
2280/// - `topic` must be a valid C string
2281/// - `data_out` must point to a buffer of at least `max_len` bytes
2282/// - `len_out` must be a valid pointer
2283#[cfg(feature = "rmw")]
2284#[no_mangle]
2285pub unsafe extern "C" fn hdds_rmw_context_shm_try_take(
2286    ctx: *mut HddsRmwContext,
2287    topic: *const c_char,
2288    data_out: *mut c_void,
2289    max_len: usize,
2290    len_out: *mut usize,
2291) -> HddsError {
2292    if ctx.is_null() || topic.is_null() || data_out.is_null() || len_out.is_null() {
2293        return HddsError::HddsInvalidArgument;
2294    }
2295
2296    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
2297    let topic_str = match CStr::from_ptr(topic).to_str() {
2298        Ok(s) => s,
2299        Err(_) => return HddsError::HddsInvalidArgument,
2300    };
2301
2302    let buf = std::slice::from_raw_parts_mut(data_out.cast::<u8>(), max_len);
2303    match ctx_ref.try_shm_take(topic_str, buf) {
2304        Some(len) => {
2305            *len_out = len;
2306            HddsError::HddsOk
2307        }
2308        None => {
2309            *len_out = 0;
2310            HddsError::HddsNotFound
2311        }
2312    }
2313}
2314
2315/// Check if SHM data is available for a topic (non-blocking).
2316///
2317/// Returns `true` (1) if data is available, `false` (0) otherwise.
2318///
2319/// # Safety
2320/// - `ctx` must be a valid `HddsRmwContext`
2321/// - `topic` must be a valid C string
2322#[cfg(feature = "rmw")]
2323#[no_mangle]
2324pub unsafe extern "C" fn hdds_rmw_context_shm_has_data(
2325    ctx: *mut HddsRmwContext,
2326    topic: *const c_char,
2327) -> bool {
2328    if ctx.is_null() || topic.is_null() {
2329        return false;
2330    }
2331
2332    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
2333    let Ok(topic_str) = CStr::from_ptr(topic).to_str() else {
2334        return false;
2335    };
2336
2337    ctx_ref.shm_has_data(topic_str)
2338}
2339///
2340/// # Safety
2341/// Caller must ensure all pointer arguments are valid or NULL.
2342#[cfg(feature = "rmw")]
2343#[no_mangle]
2344pub unsafe extern "C" fn hdds_rmw_deserialize_with_codec(
2345    codec_kind: u8,
2346    data: *const u8,
2347    data_len: usize,
2348    ros_message: *mut c_void,
2349) -> HddsError {
2350    if ros_message.is_null() {
2351        return HddsError::HddsInvalidArgument;
2352    }
2353
2354    let Some(codec) = Ros2CodecKind::try_from(codec_kind) else {
2355        return HddsError::HddsInvalidArgument;
2356    };
2357
2358    if codec == Ros2CodecKind::None {
2359        return HddsError::HddsInvalidArgument;
2360    }
2361
2362    let slice = if data_len == 0 {
2363        &[]
2364    } else if data.is_null() {
2365        return HddsError::HddsInvalidArgument;
2366    } else {
2367        slice::from_raw_parts(data, data_len)
2368    };
2369
2370    match decode_special(codec, slice, ros_message) {
2371        Ok(true) => HddsError::HddsOk,
2372        Ok(false) => HddsError::HddsOperationFailed,
2373        Err(err) => map_api_error(err),
2374    }
2375}
2376
2377/// Check if a ROS2 type has a dynamic TypeDescriptor available.
2378/// Returns true if the type is supported for dynamic deserialization.
2379///
2380/// # Safety
2381/// Caller must ensure all pointer arguments are valid or NULL.
2382#[cfg(feature = "rmw")]
2383#[no_mangle]
2384pub unsafe extern "C" fn hdds_rmw_has_type_descriptor(type_name: *const c_char) -> bool {
2385    if type_name.is_null() {
2386        return false;
2387    }
2388
2389    let type_str = match CStr::from_ptr(type_name).to_str() {
2390        Ok(s) => s,
2391        Err(_) => return false,
2392    };
2393
2394    ros2_type_to_descriptor(type_str).is_some()
2395}
2396
2397/// Deserialize CDR data to a ROS2 message using dynamic types.
2398/// Returns Ok if successful, InvalidArgument if type not supported.
2399///
2400/// # Safety
2401/// Caller must ensure all pointer arguments are valid or NULL.
2402#[cfg(feature = "rmw")]
2403#[no_mangle]
2404pub unsafe extern "C" fn hdds_rmw_deserialize_dynamic(
2405    type_name: *const c_char,
2406    data: *const u8,
2407    data_len: usize,
2408    ros_message: *mut c_void,
2409) -> HddsError {
2410    if type_name.is_null() || ros_message.is_null() {
2411        return HddsError::HddsInvalidArgument;
2412    }
2413
2414    let type_str = match CStr::from_ptr(type_name).to_str() {
2415        Ok(s) => s,
2416        Err(_) => return HddsError::HddsInvalidArgument,
2417    };
2418
2419    let slice = if data_len == 0 {
2420        &[]
2421    } else if data.is_null() {
2422        return HddsError::HddsInvalidArgument;
2423    } else {
2424        slice::from_raw_parts(data, data_len)
2425    };
2426
2427    match deserialize_dynamic_to_ros(type_str, slice, ros_message) {
2428        Ok(()) => HddsError::HddsOk,
2429        Err(_) => HddsError::HddsOperationFailed,
2430    }
2431}
2432
2433/// Detach a condition previously attached to the rmw waitset.
2434///
2435/// # Safety
2436/// Caller must ensure all pointer arguments are valid or NULL.
2437#[cfg(feature = "rmw")]
2438#[no_mangle]
2439pub unsafe extern "C" fn hdds_rmw_context_detach_condition(
2440    ctx: *mut HddsRmwContext,
2441    key: u64,
2442) -> HddsError {
2443    if ctx.is_null() {
2444        return HddsError::HddsInvalidArgument;
2445    }
2446
2447    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
2448    match ctx_ref.detach_condition(key) {
2449        Ok(()) => HddsError::HddsOk,
2450        Err(err) => map_api_error(err),
2451    }
2452}
2453
2454/// Detach a reader previously attached to the rmw waitset.
2455///
2456/// # Safety
2457/// Caller must ensure all pointer arguments are valid or NULL.
2458#[cfg(feature = "rmw")]
2459#[no_mangle]
2460pub unsafe extern "C" fn hdds_rmw_context_detach_reader(
2461    ctx: *mut HddsRmwContext,
2462    reader: *mut HddsDataReader,
2463) -> HddsError {
2464    if ctx.is_null() || reader.is_null() {
2465        return HddsError::HddsInvalidArgument;
2466    }
2467
2468    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
2469    match ctx_ref.as_ref().detach_reader(reader.cast::<c_void>()) {
2470        Ok(()) => HddsError::HddsOk,
2471        Err(err) => map_api_error(err),
2472    }
2473}
2474
2475/// Wait for the rmw context waitset.
2476///
2477/// # Safety
2478/// Caller must ensure all pointer arguments are valid or NULL.
2479#[cfg(feature = "rmw")]
2480#[no_mangle]
2481pub unsafe extern "C" fn hdds_rmw_context_wait(
2482    ctx: *mut HddsRmwContext,
2483    timeout_ns: i64,
2484    out_keys: *mut u64,
2485    out_conditions: *mut *const c_void,
2486    max_conditions: usize,
2487    out_len: *mut usize,
2488) -> HddsError {
2489    if ctx.is_null() || out_len.is_null() || out_keys.is_null() || out_conditions.is_null() {
2490        return HddsError::HddsInvalidArgument;
2491    }
2492
2493    *out_len = 0;
2494
2495    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
2496    let ctx_ref = Arc::clone(ctx_ref);
2497    let timeout = if timeout_ns < 0 {
2498        None
2499    } else {
2500        match u64::try_from(timeout_ns) {
2501            Ok(nanos) => Some(Duration::from_nanos(nanos)),
2502            Err(_) => None,
2503        }
2504    };
2505
2506    let hits = match ctx_ref.wait(timeout) {
2507        Ok(list) => list,
2508        Err(err) => return map_api_error(err),
2509    };
2510
2511    if hits.len() > max_conditions {
2512        return HddsError::HddsOperationFailed;
2513    }
2514
2515    for (idx, hit) in hits.iter().enumerate() {
2516        *out_keys.add(idx) = hit.key;
2517        *out_conditions.add(idx) = hit.condition_ptr;
2518    }
2519
2520    *out_len = hits.len();
2521    HddsError::HddsOk
2522}
2523
2524/// Wait for reader notifications and report guard hits.
2525///
2526/// # Safety
2527/// Caller must ensure all pointer arguments are valid or NULL.
2528#[cfg(feature = "rmw")]
2529#[no_mangle]
2530pub unsafe extern "C" fn hdds_rmw_context_wait_readers(
2531    ctx: *mut HddsRmwContext,
2532    timeout_ns: i64,
2533    out_readers: *mut *mut HddsDataReader,
2534    max_readers: usize,
2535    out_len: *mut usize,
2536    out_guard_triggered: *mut bool,
2537) -> HddsError {
2538    if ctx.is_null() || out_readers.is_null() || out_len.is_null() {
2539        return HddsError::HddsInvalidArgument;
2540    }
2541
2542    if !out_guard_triggered.is_null() {
2543        *out_guard_triggered = false;
2544    }
2545
2546    *out_len = 0;
2547
2548    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
2549    let ctx_ref = Arc::clone(ctx_ref);
2550    let timeout = if timeout_ns < 0 {
2551        None
2552    } else {
2553        match u64::try_from(timeout_ns) {
2554            Ok(nanos) => Some(Duration::from_nanos(nanos)),
2555            Err(_) => None,
2556        }
2557    };
2558
2559    let hits = match ctx_ref.wait(timeout) {
2560        Ok(list) => list,
2561        Err(err) => return map_api_error(err),
2562    };
2563
2564    let mut reader_count = 0usize;
2565
2566    for hit in hits {
2567        if hit.key == ctx_ref.graph_guard_key() {
2568            if !out_guard_triggered.is_null() {
2569                *out_guard_triggered = true;
2570            }
2571            continue;
2572        }
2573
2574        if let Some(reader_ptr) = hit.reader_ptr {
2575            if reader_count >= max_readers {
2576                return HddsError::HddsOperationFailed;
2577            }
2578            *out_readers.add(reader_count) =
2579                reader_ptr.cast::<HddsDataReader>() as *mut HddsDataReader;
2580            reader_count += 1;
2581        }
2582    }
2583
2584    *out_len = reader_count;
2585    HddsError::HddsOk
2586}
2587
2588/// Create an rmw waitset bound to a context.
2589///
2590/// # Safety
2591/// Caller must ensure all pointer arguments are valid or NULL.
2592#[cfg(feature = "rmw")]
2593#[no_mangle]
2594pub unsafe extern "C" fn hdds_rmw_waitset_create(ctx: *mut HddsRmwContext) -> *mut HddsRmwWaitSet {
2595    if ctx.is_null() {
2596        return ptr::null_mut();
2597    }
2598
2599    let ctx_ref = &*ctx.cast::<Arc<ForeignRmwContext>>();
2600    let waitset = ForeignRmwWaitSet::new(Arc::clone(ctx_ref));
2601    Box::into_raw(Box::new(waitset)).cast::<HddsRmwWaitSet>()
2602}
2603
2604/// Destroy an rmw waitset.
2605///
2606/// # Safety
2607/// Caller must ensure all pointer arguments are valid or NULL.
2608#[cfg(feature = "rmw")]
2609#[no_mangle]
2610pub unsafe extern "C" fn hdds_rmw_waitset_destroy(waitset: *mut HddsRmwWaitSet) {
2611    if !waitset.is_null() {
2612        let boxed = Box::from_raw(waitset.cast::<ForeignRmwWaitSet>());
2613        boxed.detach_all();
2614    }
2615}
2616
2617/// Attach a reader to an rmw waitset.
2618///
2619/// # Safety
2620/// Caller must ensure all pointer arguments are valid or NULL.
2621#[cfg(feature = "rmw")]
2622#[no_mangle]
2623pub unsafe extern "C" fn hdds_rmw_waitset_attach_reader(
2624    waitset: *mut HddsRmwWaitSet,
2625    reader: *mut HddsDataReader,
2626) -> HddsError {
2627    if waitset.is_null() || reader.is_null() {
2628        return HddsError::HddsInvalidArgument;
2629    }
2630
2631    let waitset_ref = &*waitset.cast::<ForeignRmwWaitSet>();
2632    match waitset_ref.attach_reader(reader.cast::<c_void>()) {
2633        Ok(()) => HddsError::HddsOk,
2634        Err(err) => map_api_error(err),
2635    }
2636}
2637
2638/// Detach a reader from an rmw waitset.
2639///
2640/// # Safety
2641/// Caller must ensure all pointer arguments are valid or NULL.
2642#[cfg(feature = "rmw")]
2643#[no_mangle]
2644pub unsafe extern "C" fn hdds_rmw_waitset_detach_reader(
2645    waitset: *mut HddsRmwWaitSet,
2646    reader: *mut HddsDataReader,
2647) -> HddsError {
2648    if waitset.is_null() || reader.is_null() {
2649        return HddsError::HddsInvalidArgument;
2650    }
2651
2652    let waitset_ref = &*waitset.cast::<ForeignRmwWaitSet>();
2653    match waitset_ref.detach_reader(reader.cast::<c_void>()) {
2654        Ok(()) => HddsError::HddsOk,
2655        Err(err) => map_api_error(err),
2656    }
2657}
2658
2659/// Wait on an rmw waitset and report triggered readers and guard state.
2660///
2661/// # Safety
2662/// Caller must ensure all pointer arguments are valid or NULL.
2663#[cfg(feature = "rmw")]
2664#[no_mangle]
2665pub unsafe extern "C" fn hdds_rmw_waitset_wait(
2666    waitset: *mut HddsRmwWaitSet,
2667    timeout_ns: i64,
2668    out_readers: *mut *mut HddsDataReader,
2669    max_readers: usize,
2670    out_len: *mut usize,
2671    out_guard_triggered: *mut bool,
2672) -> HddsError {
2673    // Allow out_readers to be NULL when max_readers is 0 (no subscriptions case)
2674    if waitset.is_null() || out_len.is_null() {
2675        return HddsError::HddsInvalidArgument;
2676    }
2677    if out_readers.is_null() && max_readers > 0 {
2678        return HddsError::HddsInvalidArgument;
2679    }
2680
2681    if !out_guard_triggered.is_null() {
2682        *out_guard_triggered = false;
2683    }
2684
2685    *out_len = 0;
2686
2687    let waitset_ref = &*waitset.cast::<ForeignRmwWaitSet>();
2688    let timeout = if timeout_ns < 0 {
2689        None
2690    } else {
2691        match u64::try_from(timeout_ns) {
2692            Ok(nanos) => Some(Duration::from_nanos(nanos)),
2693            Err(_) => None,
2694        }
2695    };
2696
2697    let (readers, guard_hit) = match waitset_ref.wait(timeout) {
2698        Ok(result) => result,
2699        Err(err) => return map_api_error(err),
2700    };
2701
2702    if readers.len() > max_readers {
2703        return HddsError::HddsOperationFailed;
2704    }
2705
2706    for (idx, reader_ptr) in readers.iter().enumerate() {
2707        *out_readers.add(idx) = (*reader_ptr).cast::<HddsDataReader>() as *mut HddsDataReader;
2708    }
2709
2710    if !out_guard_triggered.is_null() {
2711        *out_guard_triggered = guard_hit;
2712    }
2713
2714    *out_len = readers.len();
2715    HddsError::HddsOk
2716}
2717
2718#[cfg(test)]
2719mod tests {
2720    #![allow(clippy::all)]
2721    use super::*;
2722    use std::ffi::CString;
2723    use std::ptr;
2724
2725    #[cfg(feature = "xtypes")]
2726    use hdds::core::types::ROS_HASH_SIZE;
2727    #[cfg(feature = "xtypes")]
2728    use hdds::xtypes::{
2729        rosidl_message_type_support_t, rosidl_type_hash_t,
2730        rosidl_typesupport_introspection_c__MessageMember,
2731        rosidl_typesupport_introspection_c__MessageMembers,
2732    };
2733
2734    #[cfg(feature = "xtypes")]
2735    #[allow(clippy::cast_possible_truncation)]
2736    const fn hash_bytes() -> [u8; ROS_HASH_SIZE] {
2737        let mut arr = [0u8; ROS_HASH_SIZE];
2738        let mut i = 0;
2739        while i < ROS_HASH_SIZE {
2740            arr[i] = i as u8;
2741            i += 1;
2742        }
2743        arr
2744    }
2745
2746    #[cfg(feature = "xtypes")]
2747    const HASH_BYTES: [u8; ROS_HASH_SIZE] = hash_bytes();
2748
2749    #[cfg(feature = "xtypes")]
2750    static HASH: rosidl_type_hash_t = rosidl_type_hash_t {
2751        version: 1,
2752        value: hash_bytes(),
2753    };
2754
2755    #[cfg(feature = "xtypes")]
2756    unsafe extern "C" fn stub_hash(
2757        _: *const rosidl_message_type_support_t,
2758    ) -> *const rosidl_type_hash_t {
2759        std::ptr::from_ref(&HASH)
2760    }
2761
2762    #[test]
2763    fn test_participant_create_destroy() {
2764        unsafe {
2765            let name = CString::new("test_participant").unwrap();
2766            let participant = hdds_participant_create(name.as_ptr());
2767            assert!(!participant.is_null());
2768            hdds_participant_destroy(participant);
2769        }
2770    }
2771
2772    #[cfg(feature = "rmw")]
2773    #[test]
2774    fn test_rmw_context_graph_guard_wait() {
2775        unsafe {
2776            let name = CString::new("rmw_ctx_guard").unwrap();
2777            let ctx = hdds_rmw_context_create(name.as_ptr());
2778            assert!(!ctx.is_null());
2779
2780            let guard = hdds_rmw_context_graph_guard_condition(ctx);
2781            assert!(!guard.is_null());
2782
2783            hdds_guard_condition_set_trigger(guard, true);
2784
2785            let mut keys = [0u64; 4];
2786            let mut ptrs = [ptr::null(); 4];
2787            let mut len = 0usize;
2788
2789            let ret = hdds_rmw_context_wait(
2790                ctx,
2791                1_000_000,
2792                keys.as_mut_ptr(),
2793                ptrs.as_mut_ptr(),
2794                keys.len(),
2795                &mut len,
2796            );
2797
2798            assert_eq!(ret, HddsError::HddsOk);
2799            assert_eq!(len, 1);
2800            assert_eq!(keys[0], hdds_rmw_context_graph_guard_key(ctx));
2801            assert_eq!(ptrs[0], guard.cast());
2802
2803            let mut readers = [ptr::null_mut(); 4];
2804            let mut reader_len = 0usize;
2805            let mut guard_hit = false;
2806            let ret = hdds_rmw_context_wait_readers(
2807                ctx,
2808                1,
2809                readers.as_mut_ptr(),
2810                readers.len(),
2811                &mut reader_len,
2812                &mut guard_hit,
2813            );
2814            assert_eq!(ret, HddsError::HddsOk);
2815            assert_eq!(reader_len, 0);
2816            assert!(guard_hit);
2817
2818            hdds_guard_condition_release(guard);
2819            hdds_rmw_context_destroy(ctx);
2820        }
2821    }
2822
2823    #[cfg(feature = "rmw")]
2824    #[test]
2825    fn test_rmw_context_attach_reader_lifecycle() {
2826        unsafe {
2827            let ctx_name = CString::new("rmw_ctx_reader").unwrap();
2828            let ctx = hdds_rmw_context_create(ctx_name.as_ptr());
2829            assert!(!ctx.is_null());
2830
2831            let topic = CString::new("rmw_ctx_reader_topic").unwrap();
2832            let mut reader = ptr::null_mut();
2833            let create_ret = hdds_rmw_context_create_reader(ctx, topic.as_ptr(), &mut reader);
2834            assert_eq!(create_ret, HddsError::HddsOk);
2835            assert!(!reader.is_null());
2836
2837            let mut key = 0u64;
2838            let ret = hdds_rmw_context_attach_reader(ctx, reader, &mut key);
2839            assert_eq!(ret, HddsError::HddsOk);
2840            assert_ne!(key, 0);
2841
2842            // Duplicate attach should fail
2843            let ret_dup = hdds_rmw_context_attach_reader(ctx, reader, &mut key);
2844            assert_eq!(ret_dup, HddsError::HddsInvalidArgument);
2845
2846            // Detach the reader and ensure it can be re-attached
2847            let ret = hdds_rmw_context_detach_reader(ctx, reader);
2848            assert_eq!(ret, HddsError::HddsOk);
2849
2850            let mut new_key = 0u64;
2851            let ret = hdds_rmw_context_attach_reader(ctx, reader, &mut new_key);
2852            assert_eq!(ret, HddsError::HddsOk);
2853            assert_ne!(new_key, 0);
2854
2855            let ret = hdds_rmw_context_detach_reader(ctx, reader);
2856            assert_eq!(ret, HddsError::HddsOk);
2857
2858            let destroy_ret = hdds_rmw_context_destroy_reader(ctx, reader);
2859            assert_eq!(destroy_ret, HddsError::HddsOk);
2860            hdds_rmw_context_destroy(ctx);
2861        }
2862    }
2863
2864    #[cfg(feature = "rmw")]
2865    #[test]
2866    fn test_rmw_waitset_basic_flow() {
2867        unsafe {
2868            let ctx_name = CString::new("rmw_waitset_ctx").unwrap();
2869            let ctx = hdds_rmw_context_create(ctx_name.as_ptr());
2870            assert!(!ctx.is_null());
2871
2872            let topic = CString::new("rmw_waitset_topic").unwrap();
2873            let mut reader = ptr::null_mut();
2874            let create_ret = hdds_rmw_context_create_reader(ctx, topic.as_ptr(), &mut reader);
2875            assert_eq!(create_ret, HddsError::HddsOk);
2876            assert!(!reader.is_null());
2877
2878            let waitset = hdds_rmw_waitset_create(ctx);
2879            assert!(!waitset.is_null());
2880
2881            let guard = hdds_rmw_context_graph_guard_condition(ctx);
2882            hdds_guard_condition_set_trigger(guard, true);
2883
2884            let mut readers = [ptr::null_mut(); 4];
2885            let mut reader_len = 0usize;
2886            let mut guard_hit = false;
2887            let ret = hdds_rmw_waitset_wait(
2888                waitset,
2889                1_000_000,
2890                readers.as_mut_ptr(),
2891                readers.len(),
2892                &mut reader_len,
2893                &mut guard_hit,
2894            );
2895            assert_eq!(ret, HddsError::HddsOk);
2896            assert_eq!(reader_len, 0);
2897            assert!(guard_hit);
2898
2899            let attach_ret = hdds_rmw_waitset_attach_reader(waitset, reader);
2900            assert_eq!(attach_ret, HddsError::HddsOk);
2901
2902            let detach_ret = hdds_rmw_waitset_detach_reader(waitset, reader);
2903            assert_eq!(detach_ret, HddsError::HddsOk);
2904
2905            hdds_guard_condition_release(guard);
2906            hdds_rmw_waitset_destroy(waitset);
2907            let destroy_ret = hdds_rmw_context_destroy_reader(ctx, reader);
2908            assert_eq!(destroy_ret, HddsError::HddsOk);
2909            hdds_rmw_context_destroy(ctx);
2910        }
2911    }
2912
2913    #[test]
2914    fn test_participant_graph_guard_waitset_integration() {
2915        unsafe {
2916            let name = CString::new("graph_guard").unwrap();
2917            let participant = hdds_participant_create(name.as_ptr());
2918            assert!(!participant.is_null());
2919
2920            let guard = hdds_participant_graph_guard_condition(participant);
2921            assert!(!guard.is_null());
2922
2923            let waitset = hdds_waitset_create();
2924            assert!(!waitset.is_null());
2925
2926            assert_eq!(
2927                hdds_waitset_attach_guard_condition(waitset, guard),
2928                HddsError::HddsOk
2929            );
2930
2931            hdds_guard_condition_set_trigger(guard, true);
2932
2933            let mut triggered = [ptr::null(); 4];
2934            let mut len = 0usize;
2935            let ret = hdds_waitset_wait(
2936                waitset,
2937                1_000_000,
2938                triggered.as_mut_ptr(),
2939                triggered.len(),
2940                &mut len,
2941            );
2942            assert_eq!(ret, HddsError::HddsOk);
2943            assert_eq!(len, 1);
2944            assert_eq!(triggered[0], guard.cast());
2945
2946            assert_eq!(
2947                hdds_waitset_detach_condition(waitset, guard.cast()),
2948                HddsError::HddsOk
2949            );
2950            hdds_waitset_destroy(waitset);
2951            hdds_guard_condition_release(guard);
2952            hdds_participant_destroy(participant);
2953        }
2954    }
2955
2956    #[test]
2957    #[cfg(feature = "xtypes")]
2958    fn test_register_type_support_and_hash() {
2959        unsafe {
2960            let members_vec = vec![
2961                rosidl_typesupport_introspection_c__MessageMember {
2962                    name_: b"x\0".as_ptr().cast(),
2963                    type_id_: 1,
2964                    string_upper_bound_: 0,
2965                    members_: ptr::null(),
2966                    is_array_: false,
2967                    array_size_: 0,
2968                    is_upper_bound_: false,
2969                    offset_: 0,
2970                    default_value_: ptr::null(),
2971                    size_function: None,
2972                    get_const_function: None,
2973                    get_function: None,
2974                    fetch_function: None,
2975                    assign_function: None,
2976                    resize_function: None,
2977                },
2978                rosidl_typesupport_introspection_c__MessageMember {
2979                    name_: b"labels\0".as_ptr().cast(),
2980                    type_id_: 16,
2981                    string_upper_bound_: 0,
2982                    members_: ptr::null(),
2983                    is_array_: true,
2984                    array_size_: 0,
2985                    is_upper_bound_: true,
2986                    offset_: 0,
2987                    default_value_: ptr::null(),
2988                    size_function: None,
2989                    get_const_function: None,
2990                    get_function: None,
2991                    fetch_function: None,
2992                    assign_function: None,
2993                    resize_function: None,
2994                },
2995            ];
2996
2997            let members_box = members_vec.into_boxed_slice();
2998            let members_ptr = members_box.as_ptr();
2999            let members_slice = Box::leak(members_box);
3000
3001            let descriptor = Box::leak(Box::new(
3002                rosidl_typesupport_introspection_c__MessageMembers {
3003                    message_namespace_: b"test_pkg__msg\0".as_ptr().cast(),
3004                    message_name_: b"TaggedPoint\0".as_ptr().cast(),
3005                    member_count_: u32::try_from(members_slice.len())
3006                        .expect("member count fits in u32"),
3007                    size_of_: 0,
3008                    members_: members_ptr,
3009                    init_function: None,
3010                    fini_function: None,
3011                },
3012            ));
3013
3014            let type_support = Box::leak(Box::new(rosidl_message_type_support_t {
3015                typesupport_identifier: b"rosidl_typesupport_introspection_c\0".as_ptr().cast(),
3016                data: std::ptr::from_ref(&*descriptor).cast::<c_void>(),
3017                func: None,
3018                get_type_hash_func: Some(stub_hash),
3019                get_type_description_func: None,
3020                get_type_description_sources_func: None,
3021            }));
3022
3023            let name = CString::new("ffi_register").unwrap();
3024            let participant = hdds_participant_create(name.as_ptr());
3025            assert!(!participant.is_null());
3026
3027            let mut handle: *const HddsTypeObject = ptr::null();
3028            let err = hdds_participant_register_type_support(
3029                participant,
3030                0,
3031                type_support,
3032                std::ptr::addr_of_mut!(handle),
3033            );
3034            assert_eq!(err, HddsError::HddsOk);
3035            assert!(!handle.is_null());
3036
3037            let mut version = 0u8;
3038            let mut buf = [0u8; ROS_HASH_SIZE];
3039            let err = hdds_type_object_hash(
3040                handle,
3041                std::ptr::addr_of_mut!(version),
3042                buf.as_mut_ptr(),
3043                buf.len(),
3044            );
3045            assert_eq!(err, HddsError::HddsOk);
3046            assert_eq!(version, 1);
3047            assert_eq!(buf, HASH_BYTES);
3048
3049            hdds_type_object_release(handle);
3050            hdds_participant_destroy(participant);
3051        }
3052    }
3053
3054    #[test]
3055    fn test_participant_create_null_name() {
3056        unsafe {
3057            let participant = hdds_participant_create(ptr::null());
3058            assert!(participant.is_null());
3059        }
3060    }
3061
3062    #[test]
3063    fn test_writer_reader_lifecycle() {
3064        unsafe {
3065            // Create participant
3066            let name = CString::new("test_participant_ffi").unwrap();
3067            let participant = hdds_participant_create(name.as_ptr());
3068            assert!(!participant.is_null());
3069
3070            // Create writer and reader
3071            let topic = CString::new("test_topic_ffi").unwrap();
3072            let writer = hdds_writer_create(participant, topic.as_ptr());
3073            let reader = hdds_reader_create(participant, topic.as_ptr());
3074            assert!(!writer.is_null());
3075            assert!(!reader.is_null());
3076
3077            // Note: We don't test actual data transmission here because
3078            // writer/reader need time to discover each other via SPDP/SEDP.
3079            // For FFI correctness testing, we just verify creation/destruction works.
3080
3081            // Cleanup
3082            hdds_writer_destroy(writer);
3083            hdds_reader_destroy(reader);
3084            hdds_participant_destroy(participant);
3085        }
3086    }
3087
3088    #[test]
3089    fn test_writer_write_null_checks() {
3090        unsafe {
3091            // Test null writer
3092            let test_data = b"test";
3093            let result = hdds_writer_write(
3094                ptr::null_mut(),
3095                test_data.as_ptr().cast::<c_void>(),
3096                test_data.len(),
3097            );
3098            assert_eq!(result, HddsError::HddsInvalidArgument);
3099
3100            // Test null data
3101            let name = CString::new("test_participant").unwrap();
3102            let participant = hdds_participant_create(name.as_ptr());
3103            let topic = CString::new("test_topic").unwrap();
3104            let writer = hdds_writer_create(participant, topic.as_ptr());
3105
3106            let result = hdds_writer_write(writer, ptr::null(), 10);
3107            assert_eq!(result, HddsError::HddsInvalidArgument);
3108
3109            hdds_writer_destroy(writer);
3110            hdds_participant_destroy(participant);
3111        }
3112    }
3113}