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}