Skip to main content

zerodds_flatdata/
backend.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! `SlotBackend` trait — abstracts the slot storage backend.
4//!
5//! ADR-0003: three production backends — in-memory (default for
6//! tests + single-process), POSIX shm/mmap (cross-process same-host
7//! via the `posix-mmap` feature), Iceoryx2 (optional bridge via the
8//! `iceoryx2-bridge` feature). All three implement the same
9//! trait, so that `FlatWriter`/`FlatReader` are generic over the
10//! backend.
11
12use alloc::vec::Vec;
13
14use crate::allocator::{SlotError, SlotHandle};
15use crate::slot::{ReaderMask, SlotHeader};
16
17/// Backend trait for the SHM slot allocator.
18///
19/// Implementers:
20/// - [`crate::InMemorySlotAllocator`] — in-memory backend, default
21///   for single-process tests and as the reference implementation.
22/// - `PosixSlotAllocator` — POSIX `shm_open` + `mmap`,
23///   cross-process same-host (feature `posix-mmap`).
24/// - `Iceoryx2SlotAdapter` — Iceoryx2 bridge (optional feature
25///   `iceoryx2-bridge`).
26pub trait SlotBackend: Send + Sync {
27    /// Reserves a free slot. The caller then writes via
28    /// `commit_slot` and thereby publishes the sample.
29    ///
30    /// # Errors
31    /// `NoFreeSlot` when all slots are loaned or unfinished.
32    fn reserve_slot(&self, active_readers_mask: ReaderMask) -> Result<SlotHandle, SlotError>;
33
34    /// Writes the sample bytes into the slot and sets SlotHeader
35    /// `{ sn, sample_size, reader_mask=0 }`. Returns the SN.
36    ///
37    /// # Errors
38    /// `OutOfBounds`, `SampleTooLarge`, or lock poison.
39    fn commit_slot(&self, handle: SlotHandle, bytes: &[u8]) -> Result<u32, SlotError>;
40
41    /// Discards a loan without committing. The slot becomes free again.
42    ///
43    /// # Errors
44    /// `OutOfBounds` or lock poison.
45    fn discard_slot(&self, handle: SlotHandle) -> Result<(), SlotError>;
46
47    /// True zero-copy loan: returns a writable pointer + capacity into a
48    /// *currently reserved* slot's data region, so the caller can fill the
49    /// sample **in place** (no staging copy) and then call
50    /// [`Self::commit_in_place`]. The pointer is valid until that
51    /// `commit_in_place` or a `discard_slot` for the same handle; the slot
52    /// must have been obtained from [`Self::reserve_slot`] and not yet
53    /// committed/discarded.
54    ///
55    /// # Errors
56    /// `OutOfBounds` if the handle is not a live loan, lock poison, or
57    /// `InPlaceUnsupported` for backends without in-place support (default).
58    fn slot_data_ptr(&self, handle: SlotHandle) -> Result<(*mut u8, usize), SlotError> {
59        let _ = handle;
60        Err(SlotError::InPlaceUnsupported)
61    }
62
63    /// Finalizes a slot whose `len` data bytes were already written in place
64    /// via [`Self::slot_data_ptr`] — sets SlotHeader `{ sn, sample_size=len,
65    /// reader_mask=0 }` and releases the loan, with **no** data copy. Returns
66    /// the SN. The byte-for-byte result is identical to `commit_slot(handle,
67    /// &buf[..len])`, only without the staging copy.
68    ///
69    /// # Errors
70    /// `SampleTooLarge`, `OutOfBounds`, lock poison, or `InPlaceUnsupported`
71    /// (default).
72    fn commit_in_place(&self, handle: SlotHandle, len: usize) -> Result<u32, SlotError> {
73        let _ = (handle, len);
74        Err(SlotError::InPlaceUnsupported)
75    }
76
77    /// Reads slot header + data bytes (copied).
78    ///
79    /// # Errors
80    /// `OutOfBounds` or lock poison.
81    fn read_slot(&self, handle: SlotHandle) -> Result<(SlotHeader, Vec<u8>), SlotError>;
82
83    /// Zero-copy read: returns a **read-only** pointer + sample length into a
84    /// *committed* slot's data region — the reader consumes in place without a
85    /// copy (counterpart to [`Self::read_slot`], which copies). The pointer is
86    /// valid until the slot is recycled (i.e. until every active reader has
87    /// `mark_read` it and the writer reserves it again); a same-host reader
88    /// reads it, then calls [`Self::mark_read`]. Default: `InPlaceUnsupported`.
89    ///
90    /// # Errors
91    /// `OutOfBounds`, lock poison, or `InPlaceUnsupported`.
92    fn slot_read_ptr(&self, handle: SlotHandle) -> Result<(*const u8, usize), SlotError> {
93        let _ = handle;
94        Err(SlotError::InPlaceUnsupported)
95    }
96
97    /// Reader-side scan: returns the handle of the next committed slot
98    /// (`sample_size > 0`) that `reader_index` has not yet `mark_read`. Used
99    /// with [`Self::slot_read_ptr`] for an untyped zero-copy take, then
100    /// [`Self::mark_read`] to release it. Returns `Ok(None)` when nothing new
101    /// is pending. Default: `Ok(None)`.
102    ///
103    /// # Errors
104    /// Lock poison.
105    fn next_unread_slot(&self, reader_index: u8) -> Result<Option<SlotHandle>, SlotError> {
106        let _ = reader_index;
107        Ok(None)
108    }
109
110    /// Sets the `reader_index` bit in the slot's `reader_mask`
111    /// (reader has read).
112    ///
113    /// # Errors
114    /// `OutOfBounds` or lock poison.
115    fn mark_read(&self, handle: SlotHandle, reader_index: u8) -> Result<(), SlotError>;
116
117    /// Sets the `reader_index` bit retroactively on all slots
118    /// (SPDP lease-expiry).
119    ///
120    /// # Errors
121    /// Lock poison.
122    fn mark_reader_disconnected(&self, reader_index: u8) -> Result<(), SlotError>;
123
124    /// Number of configured slots.
125    ///
126    /// # Errors
127    /// Lock poison.
128    fn slot_count(&self) -> Result<usize, SlotError>;
129
130    /// Total slot size (header + data + padding); for discovery.
131    fn slot_total_size(&self) -> usize;
132
133    /// Data area per slot (without header, without padding).
134    fn slot_capacity(&self) -> usize;
135
136    /// Spec §6.1: TYPE_HASH of the topic type, if known to the
137    /// backend. `None` = the backend tracks no hash; the caller must
138    /// verify another way (e.g. via discovery). Default: None.
139    fn type_hash(&self) -> Option<[u8; 16]> {
140        None
141    }
142
143    /// Spec §4.2 event-driven notify — current change generation. Bumped on
144    /// every `commit_slot` (new sample → wake readers) and slot-free
145    /// (`mark_read`/`discard_slot`/… → wake writers). Capture this before
146    /// checking a condition and pass it to [`Self::wait_for_change`] for a
147    /// lost-wakeup-free wait. Default `0` for backends without notify support.
148    fn notify_generation(&self) -> u64 {
149        0
150    }
151
152    /// Spec §4.2 — blocks until the change generation differs from `last` or
153    /// `timeout` elapses (event-driven, NO busy-poll). Default: returns
154    /// immediately (a backend without notify support degrades to caller-driven
155    /// polling). `InMemorySlotAllocator` uses a `Condvar`; `PosixSlotAllocator`
156    /// uses a cross-process futex on a shared-memory word (Linux).
157    fn wait_for_change(&self, last: u64, timeout: core::time::Duration) {
158        let _ = (last, timeout);
159    }
160}