zerodds-c-api 1.0.0-rc.3

ZeroDDS C-FFI: extern "C" runtime hub for C++/C#/TypeScript bindings + Apex.AI plugin + ROS-2 RMW
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 ZeroDDS Contributors

//! Built-in Topic Data + Discovered Endpoints C-FFI (Spec §2.2.5).
//!
//! Provides the 4 normative builtin topics:
//! - `DCPSParticipant` (§2.2.5.1) → `ZeroDdsParticipantBuiltinTopicData`
//! - `DCPSTopic` (§2.2.5.2) → `ZeroDdsTopicBuiltinTopicData`
//! - `DCPSPublication` (§2.2.5.3) → `ZeroDdsPublicationBuiltinTopicData`
//! - `DCPSSubscription` (§2.2.5.4) → `ZeroDdsSubscriptionBuiltinTopicData`

use alloc::ffi::CString;
use alloc::vec::Vec;
use core::ffi::{c_char, c_int};
use core::ptr;
use core::slice;

use zerodds_dcps::instance_handle::InstanceHandle;

use crate::ZeroDdsStatus;
use crate::entities::ZeroDdsDomainParticipant;

// ---------------------------------------------------------------------------
// Structs
// ---------------------------------------------------------------------------

/// `DCPSParticipant` sample (Spec §2.2.5.1).
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct ZeroDdsParticipantBuiltinTopicData {
    /// 16-byte GUID.
    pub guid: [u8; 16],
    /// Pointer to UserData bytes (static over the `dp` lifetime in the RC1
    /// snapshot — the caller copies if it outlives the data).
    pub user_data: *const u8,
    /// UserData length.
    pub user_data_len: usize,
}

/// `DCPSTopic` sample (Spec §2.2.5.2).
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct ZeroDdsTopicBuiltinTopicData {
    /// Synthetic topic key (16 byte).
    pub key: [u8; 16],
    /// Topic name as a C string (heap-allocated; the caller must
    /// call `zerodds_string_free`).
    pub name: *mut c_char,
    /// Type name (heap-allocated; the caller must call `zerodds_string_free`).
    pub type_name: *mut c_char,
    /// Durability kind (0=Volatile, 1=TransientLocal, 2=Transient, 3=Persistent).
    pub durability_kind: u32,
    /// Reliability kind (1=BestEffort, 2=Reliable).
    pub reliability_kind: u32,
}

/// `DCPSPublication` sample (Spec §2.2.5.3).
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct ZeroDdsPublicationBuiltinTopicData {
    /// Endpoint GUID (writer).
    pub key: [u8; 16],
    /// Owning participant GUID.
    pub participant_key: [u8; 16],
    /// Topic name (heap-allocated; via `zerodds_string_free`).
    pub topic_name: *mut c_char,
    /// Type name (heap-allocated).
    pub type_name: *mut c_char,
    /// Durability.
    pub durability_kind: u32,
    /// Reliability.
    pub reliability_kind: u32,
    /// Ownership (0=Shared, 1=Exclusive).
    pub ownership_kind: u32,
    /// Ownership strength.
    pub ownership_strength: i32,
    /// Liveliness lease in seconds.
    pub liveliness_lease_seconds: i32,
    /// Deadline period in seconds.
    pub deadline_seconds: i32,
    /// Lifespan duration in seconds.
    pub lifespan_seconds: i32,
}

/// `DCPSSubscription` sample (Spec §2.2.5.4).
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct ZeroDdsSubscriptionBuiltinTopicData {
    /// Endpoint GUID (reader).
    pub key: [u8; 16],
    /// Owning participant GUID.
    pub participant_key: [u8; 16],
    /// Topic name (heap-allocated).
    pub topic_name: *mut c_char,
    /// Type name (heap-allocated).
    pub type_name: *mut c_char,
    /// Durability.
    pub durability_kind: u32,
    /// Reliability.
    pub reliability_kind: u32,
    /// Ownership.
    pub ownership_kind: u32,
    /// Liveliness lease in seconds.
    pub liveliness_lease_seconds: i32,
    /// Deadline period in seconds.
    pub deadline_seconds: i32,
}

// ---------------------------------------------------------------------------
// DP → Discovered Topics + Data
// ---------------------------------------------------------------------------

/// Returns the InstanceHandles of all discovered topics.
///
/// # Safety
/// `p`, `out`, `out_count` valid.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn zerodds_dp_get_discovered_topics(
    p: *mut ZeroDdsDomainParticipant,
    out: *mut u64,
    out_count: *mut usize,
    cap: usize,
) -> c_int {
    if p.is_null() || out.is_null() || out_count.is_null() {
        return ZeroDdsStatus::BadParameter as c_int;
    }
    // SAFETY: see fn # Safety doc — p+out+out_count NULL-checked above; out[0..cap]
    // must be writeable (caller pledge).
    unsafe {
        let pp = &*p;
        let handles = pp.dp.get_discovered_topics();
        let n = handles.len().min(cap);
        let dst = slice::from_raw_parts_mut(out, n);
        for (i, h) in handles.iter().take(n).enumerate() {
            dst[i] = h.as_raw();
        }
        *out_count = n;
    }
    ZeroDdsStatus::Ok as c_int
}

/// Returns the `ParticipantBuiltinTopicData` for an InstanceHandle.
///
/// # Safety
/// `p`, `out` valid.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn zerodds_dp_get_discovered_participant_data(
    p: *mut ZeroDdsDomainParticipant,
    handle: u64,
    out: *mut ZeroDdsParticipantBuiltinTopicData,
) -> c_int {
    if p.is_null() || out.is_null() {
        return ZeroDdsStatus::BadParameter as c_int;
    }
    // SAFETY: see fn # Safety doc — p+out NULL-checked above; user_data is lifted into
    // a slice via Box::leak, the caller returns it via builtin_userdata_free.
    unsafe {
        let pp = &*p;
        let data = match pp
            .dp
            .get_discovered_participant_data(InstanceHandle::from_raw(handle))
        {
            Ok(d) => d,
            Err(_) => return ZeroDdsStatus::BadParameter as c_int,
        };
        let mut guid = [0u8; 16];
        guid.copy_from_slice(&data.key.to_bytes());
        let bytes = data.user_data.into_boxed_slice();
        let len = bytes.len();
        let p_data = if len == 0 {
            ptr::null()
        } else {
            Box::leak(bytes).as_ptr()
        };
        *out = ZeroDdsParticipantBuiltinTopicData {
            guid,
            user_data: p_data,
            user_data_len: len,
        };
    }
    ZeroDdsStatus::Ok as c_int
}

/// Returns a previously allocated UserData slice.
///
/// # Safety
/// `p[0..len]` must come from `dp_get_discovered_participant_data`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn zerodds_builtin_userdata_free(p: *const u8, len: usize) {
    if p.is_null() || len == 0 {
        return;
    }
    // SAFETY: see fn # Safety doc — p+len from dp_get_discovered_participant_data
    // Box::leak roundtrip.
    let _ = unsafe { Box::from_raw(slice::from_raw_parts_mut(p as *mut u8, len)) };
}

/// Returns the `TopicBuiltinTopicData` for an InstanceHandle.
///
/// # Safety
/// `p`, `out` valid.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn zerodds_dp_get_discovered_topic_data(
    p: *mut ZeroDdsDomainParticipant,
    handle: u64,
    out: *mut ZeroDdsTopicBuiltinTopicData,
) -> c_int {
    if p.is_null() || out.is_null() {
        return ZeroDdsStatus::BadParameter as c_int;
    }
    // SAFETY: see fn # Safety doc — p+out NULL-checked above; name/type_name are
    // passed as CString::into_raw, the caller via zerodds_string_free.
    unsafe {
        let pp = &*p;
        let data = match pp
            .dp
            .get_discovered_topic_data(InstanceHandle::from_raw(handle))
        {
            Ok(d) => d,
            Err(_) => return ZeroDdsStatus::BadParameter as c_int,
        };
        let mut key = [0u8; 16];
        key.copy_from_slice(&data.key.to_bytes());
        let name_c = CString::new(data.name.as_bytes())
            .unwrap_or_default()
            .into_raw();
        let type_c = CString::new(data.type_name.as_bytes())
            .unwrap_or_default()
            .into_raw();
        *out = ZeroDdsTopicBuiltinTopicData {
            key,
            name: name_c,
            type_name: type_c,
            durability_kind: data.durability as u32,
            reliability_kind: data.reliability as u32,
        };
    }
    ZeroDdsStatus::Ok as c_int
}

/// Number of discovered publications (Spec §2.2.2.2.1.13).
///
/// # Safety
/// `p` valid.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn zerodds_dp_discovered_publications_count(
    p: *mut ZeroDdsDomainParticipant,
) -> usize {
    if p.is_null() {
        return 0;
    }
    // SAFETY: see fn # Safety doc — p NULL-checked above.
    unsafe { (*p).dp.discovered_publications_count() }
}

/// Number of discovered subscriptions.
///
/// # Safety
/// `p` valid.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn zerodds_dp_discovered_subscriptions_count(
    p: *mut ZeroDdsDomainParticipant,
) -> usize {
    if p.is_null() {
        return 0;
    }
    // SAFETY: see fn # Safety doc — p NULL-checked above.
    unsafe { (*p).dp.discovered_subscriptions_count() }
}

// suppress unused-import warning
#[allow(dead_code)]
fn _suppress(_: Vec<u8>) {}

#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod tests {
    use super::*;
    use crate::factory_ffi::{
        zerodds_dpf_create_participant, zerodds_dpf_delete_participant, zerodds_dpf_get_instance,
    };
    use crate::participant_ffi::zerodds_dp_delete_contained_entities;

    fn mk(domain: u32) -> *mut ZeroDdsDomainParticipant {
        let f = zerodds_dpf_get_instance();
        // SAFETY: f static.
        unsafe { zerodds_dpf_create_participant(f, domain, ptr::null()) }
    }

    fn cleanup(p: *mut ZeroDdsDomainParticipant) {
        let f = zerodds_dpf_get_instance();
        // SAFETY: p from mk; f static.
        unsafe {
            zerodds_dp_delete_contained_entities(p);
            zerodds_dpf_delete_participant(f, p);
        }
    }

    #[test]
    fn discovered_topics_empty_initially() {
        let p = mk(61);
        let mut buf = [0u64; 16];
        let mut count = 0usize;
        // SAFETY: p from mk; buf+count stack-local.
        let rc = unsafe { zerodds_dp_get_discovered_topics(p, buf.as_mut_ptr(), &mut count, 16) };
        assert_eq!(rc, ZeroDdsStatus::Ok as c_int);
        assert_eq!(count, 0);
        cleanup(p);
    }

    #[test]
    fn discovered_publications_count_starts_zero() {
        let p = mk(62);
        // SAFETY: p from mk.
        unsafe {
            assert_eq!(zerodds_dp_discovered_publications_count(p), 0);
            assert_eq!(zerodds_dp_discovered_subscriptions_count(p), 0);
        }
        cleanup(p);
    }

    #[test]
    fn discovered_participant_data_unknown_handle_returns_error() {
        let p = mk(63);
        let mut data = ZeroDdsParticipantBuiltinTopicData {
            guid: [0u8; 16],
            user_data: ptr::null(),
            user_data_len: 0,
        };
        // SAFETY: p from mk; data stack-local.
        let rc = unsafe { zerodds_dp_get_discovered_participant_data(p, 0xDEAD, &mut data) };
        assert_eq!(rc, ZeroDdsStatus::BadParameter as c_int);
        cleanup(p);
    }
}