Skip to main content

ipc_ring48/
lib.rs

1//! `ipc-ring48` is a bounded 48-byte POSIX shared-memory SPSC queue.
2//!
3//! The queue has one producer, one consumer, fixed 48-byte slots, and a
4//! lock-free hot path. `push` returns [`PushError::Full`] when the queue is
5//! full. `pop` returns `None` when the queue is empty.
6
7use libc::{
8    MAP_FAILED, MAP_SHARED, O_CREAT, O_EXCL, O_RDWR, PROT_READ, PROT_WRITE, c_void, close, fstat,
9    ftruncate, mmap, munmap, shm_open, shm_unlink,
10};
11use std::ffi::CString;
12use std::fmt;
13use std::io;
14use std::mem::{size_of, zeroed};
15use std::ptr::{self, addr_of};
16use std::sync::atomic::{AtomicU64, Ordering};
17
18/// POSIX shared-memory object name used by `ipc-ring48`.
19pub const SHM_NAME: &str = "/ipc_ring48_queue";
20
21/// Fixed payload size carried by every queue slot.
22pub const PAYLOAD_SIZE: usize = 48;
23
24const MAGIC: u64 = 0x4950_4352_494E_4734; // "IPCRING4"
25const VERSION: u64 = 1;
26const FLAGS: u64 = 0;
27
28#[repr(C, align(64))]
29pub struct SharedHeader {
30    pub magic: u64,
31    pub version: u64,
32    pub region_size: u64,
33    pub payload_size: u64,
34    pub capacity: u64,
35    pub flags: u64,
36    pub producer_pid: u64,
37    pub consumer_pid: u64,
38}
39
40#[repr(C, align(64))]
41pub struct CounterLine {
42    pub value: AtomicU64,
43    pub reserved: [u8; 56],
44}
45
46#[repr(C, align(64))]
47pub struct Slot48 {
48    pub payload: [u8; PAYLOAD_SIZE],
49    pub reserved: [u8; 16],
50}
51
52const HEADER_SIZE: usize = size_of::<SharedHeader>();
53const COUNTER_SIZE: usize = size_of::<CounterLine>();
54const SLOT_SIZE: usize = size_of::<Slot48>();
55const BASE_SIZE: usize = HEADER_SIZE + COUNTER_SIZE + COUNTER_SIZE;
56
57/// Error returned by [`Producer::push`] when the queue is full.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum PushError {
60    Full,
61}
62
63impl fmt::Display for PushError {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        match self {
66            PushError::Full => write!(f, "queue full"),
67        }
68    }
69}
70
71impl std::error::Error for PushError {}
72
73/// Snapshot of queue state.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub struct QueueStats {
76    pub capacity: u64,
77    pub len: u64,
78    pub head: u64,
79    pub tail: u64,
80    pub region_size: u64,
81}
82
83struct Mapping {
84    ptr: *mut u8,
85    len: usize,
86    fd: i32,
87}
88
89impl Mapping {
90    fn create_or_open(capacity: usize) -> io::Result<Self> {
91        validate_capacity(capacity)?;
92
93        let expected_region_size = region_size_for_capacity(capacity)?;
94        let name = CString::new(SHM_NAME).unwrap();
95
96        let mut created = false;
97
98        let fd = unsafe { shm_open(name.as_ptr(), O_CREAT | O_EXCL | O_RDWR, 0o600) };
99
100        let fd = if fd >= 0 {
101            created = true;
102            fd
103        } else {
104            let err = io::Error::last_os_error();
105
106            if err.raw_os_error() == Some(libc::EEXIST) {
107                let fd = unsafe { shm_open(name.as_ptr(), O_RDWR, 0o600) };
108
109                if fd < 0 {
110                    return Err(io::Error::last_os_error());
111                }
112
113                fd
114            } else {
115                return Err(err);
116            }
117        };
118
119        let region_size = if created {
120            let rc = unsafe { ftruncate(fd, expected_region_size as libc::off_t) };
121
122            if rc != 0 {
123                let err = io::Error::last_os_error();
124
125                unsafe {
126                    close(fd);
127                }
128
129                return Err(err);
130            }
131
132            expected_region_size
133        } else {
134            file_size(fd)?
135        };
136
137        let raw = unsafe {
138            mmap(
139                ptr::null_mut(),
140                region_size,
141                PROT_READ | PROT_WRITE,
142                MAP_SHARED,
143                fd,
144                0,
145            )
146        };
147
148        if raw == MAP_FAILED {
149            let err = io::Error::last_os_error();
150
151            unsafe {
152                close(fd);
153            }
154
155            return Err(err);
156        }
157
158        let mapping = Self {
159            ptr: raw as *mut u8,
160            len: region_size,
161            fd,
162        };
163
164        if created {
165            mapping.initialise(capacity)?;
166        } else {
167            mapping.validate()?;
168
169            if mapping.capacity() != capacity as u64 {
170                return Err(io::Error::new(
171                    io::ErrorKind::InvalidInput,
172                    "existing queue capacity does not match requested capacity",
173                ));
174            }
175        }
176
177        Ok(mapping)
178    }
179
180    fn open_existing() -> io::Result<Self> {
181        let name = CString::new(SHM_NAME).unwrap();
182
183        let fd = unsafe { shm_open(name.as_ptr(), O_RDWR, 0o600) };
184
185        if fd < 0 {
186            return Err(io::Error::last_os_error());
187        }
188
189        let region_size = file_size(fd)?;
190
191        let raw = unsafe {
192            mmap(
193                ptr::null_mut(),
194                region_size,
195                PROT_READ | PROT_WRITE,
196                MAP_SHARED,
197                fd,
198                0,
199            )
200        };
201
202        if raw == MAP_FAILED {
203            let err = io::Error::last_os_error();
204
205            unsafe {
206                close(fd);
207            }
208
209            return Err(err);
210        }
211
212        let mapping = Self {
213            ptr: raw as *mut u8,
214            len: region_size,
215            fd,
216        };
217
218        mapping.validate()?;
219
220        Ok(mapping)
221    }
222
223    fn initialise(&self, capacity: usize) -> io::Result<()> {
224        validate_capacity(capacity)?;
225
226        unsafe {
227            ptr::write_bytes(self.ptr, 0, self.len);
228
229            let header = self.header_mut();
230
231            (*header).magic = MAGIC;
232            (*header).version = VERSION;
233            (*header).region_size = self.len as u64;
234            (*header).payload_size = PAYLOAD_SIZE as u64;
235            (*header).capacity = capacity as u64;
236            (*header).flags = FLAGS;
237            (*header).producer_pid = libc::getpid() as u64;
238            (*header).consumer_pid = 0;
239
240            (*self.head()).value = AtomicU64::new(0);
241            (*self.tail()).value = AtomicU64::new(0);
242        }
243
244        Ok(())
245    }
246
247    fn validate(&self) -> io::Result<()> {
248        unsafe {
249            let header = self.header();
250
251            let magic = ptr::read_volatile(addr_of!((*header).magic));
252            let version = ptr::read_volatile(addr_of!((*header).version));
253            let region_size = ptr::read_volatile(addr_of!((*header).region_size));
254            let payload_size = ptr::read_volatile(addr_of!((*header).payload_size));
255            let capacity = ptr::read_volatile(addr_of!((*header).capacity));
256
257            if magic != MAGIC {
258                return Err(io::Error::new(
259                    io::ErrorKind::InvalidData,
260                    "shared memory magic mismatch",
261                ));
262            }
263
264            if version != VERSION {
265                return Err(io::Error::new(
266                    io::ErrorKind::InvalidData,
267                    "shared memory version mismatch",
268                ));
269            }
270
271            if region_size != self.len as u64 {
272                return Err(io::Error::new(
273                    io::ErrorKind::InvalidData,
274                    "shared memory region size mismatch",
275                ));
276            }
277
278            if payload_size != PAYLOAD_SIZE as u64 {
279                return Err(io::Error::new(
280                    io::ErrorKind::InvalidData,
281                    "shared memory payload size mismatch",
282                ));
283            }
284
285            let capacity_usize = usize::try_from(capacity).map_err(|_| {
286                io::Error::new(io::ErrorKind::InvalidData, "capacity does not fit usize")
287            })?;
288
289            validate_capacity(capacity_usize)?;
290
291            let expected_region_size = region_size_for_capacity(capacity_usize)?;
292
293            if expected_region_size != self.len {
294                return Err(io::Error::new(
295                    io::ErrorKind::InvalidData,
296                    "capacity does not match region size",
297                ));
298            }
299        }
300
301        Ok(())
302    }
303
304    fn push(&self, value: [u8; PAYLOAD_SIZE]) -> Result<(), PushError> {
305        unsafe {
306            let head = &(*self.head()).value;
307            let tail = &(*self.tail()).value;
308
309            let current_head = head.load(Ordering::Relaxed);
310            let current_tail = tail.load(Ordering::Acquire);
311            let capacity = self.capacity();
312
313            if current_head.wrapping_sub(current_tail) == capacity {
314                return Err(PushError::Full);
315            }
316
317            let index = current_head & (capacity - 1);
318            let slot = self.slot(index);
319
320            ptr::copy_nonoverlapping(value.as_ptr(), (*slot).payload.as_mut_ptr(), PAYLOAD_SIZE);
321
322            head.store(current_head.wrapping_add(1), Ordering::Release);
323
324            Ok(())
325        }
326    }
327
328    fn pop(&self) -> Option<[u8; PAYLOAD_SIZE]> {
329        unsafe {
330            let head = &(*self.head()).value;
331            let tail = &(*self.tail()).value;
332
333            let current_tail = tail.load(Ordering::Relaxed);
334            let current_head = head.load(Ordering::Acquire);
335
336            if current_tail == current_head {
337                return None;
338            }
339
340            let capacity = self.capacity();
341            let index = current_tail & (capacity - 1);
342            let slot = self.slot(index);
343
344            let mut out = [0u8; PAYLOAD_SIZE];
345
346            ptr::copy_nonoverlapping((*slot).payload.as_ptr(), out.as_mut_ptr(), PAYLOAD_SIZE);
347
348            tail.store(current_tail.wrapping_add(1), Ordering::Release);
349
350            Some(out)
351        }
352    }
353
354    fn stats(&self) -> QueueStats {
355        unsafe {
356            let head = (*self.head()).value.load(Ordering::Acquire);
357            let tail = (*self.tail()).value.load(Ordering::Acquire);
358            let capacity = self.capacity();
359
360            QueueStats {
361                capacity,
362                len: head.wrapping_sub(tail),
363                head,
364                tail,
365                region_size: self.len as u64,
366            }
367        }
368    }
369
370    fn set_producer_pid(&self) {
371        unsafe {
372            (*self.header_mut()).producer_pid = libc::getpid() as u64;
373        }
374    }
375
376    fn set_consumer_pid(&self) {
377        unsafe {
378            (*self.header_mut()).consumer_pid = libc::getpid() as u64;
379        }
380    }
381
382    fn capacity(&self) -> u64 {
383        unsafe { ptr::read_volatile(addr_of!((*self.header()).capacity)) }
384    }
385
386    unsafe fn header(&self) -> *const SharedHeader {
387        self.ptr as *const SharedHeader
388    }
389
390    unsafe fn header_mut(&self) -> *mut SharedHeader {
391        self.ptr as *mut SharedHeader
392    }
393
394    unsafe fn head(&self) -> *mut CounterLine {
395        unsafe { self.ptr.add(HEADER_SIZE) as *mut CounterLine }
396    }
397
398    unsafe fn tail(&self) -> *mut CounterLine {
399        unsafe { self.ptr.add(HEADER_SIZE + COUNTER_SIZE) as *mut CounterLine }
400    }
401
402    unsafe fn slot(&self, index: u64) -> *mut Slot48 {
403        unsafe { self.ptr.add(BASE_SIZE + index as usize * SLOT_SIZE) as *mut Slot48 }
404    }
405}
406
407impl Drop for Mapping {
408    fn drop(&mut self) {
409        unsafe {
410            munmap(self.ptr as *mut c_void, self.len);
411            close(self.fd);
412        }
413    }
414}
415
416/// Queue producer.
417///
418/// A process holding `Producer` owns pushes into the queue.
419pub struct Producer {
420    mapping: Mapping,
421}
422
423impl Producer {
424    /// Create the queue if missing, or open the existing queue when capacity matches.
425    ///
426    /// Capacity must be a non-zero power of two.
427    pub fn create_or_open(capacity: usize) -> io::Result<Self> {
428        let mapping = Mapping::create_or_open(capacity)?;
429        mapping.set_producer_pid();
430
431        Ok(Self { mapping })
432    }
433
434    /// Open an existing queue as producer.
435    pub fn open() -> io::Result<Self> {
436        let mapping = Mapping::open_existing()?;
437        mapping.set_producer_pid();
438
439        Ok(Self { mapping })
440    }
441
442    /// Push one 48-byte value.
443    ///
444    /// Returns [`PushError::Full`] when the bounded queue has no free slot.
445    pub fn push(&self, value: [u8; PAYLOAD_SIZE]) -> Result<(), PushError> {
446        self.mapping.push(value)
447    }
448
449    /// Queue capacity in slots.
450    pub fn capacity(&self) -> u64 {
451        self.mapping.stats().capacity
452    }
453
454    /// Current queue length in slots.
455    pub fn len(&self) -> u64 {
456        self.mapping.stats().len
457    }
458
459    /// Returns `true` when the queue is empty.
460    pub fn is_empty(&self) -> bool {
461        self.len() == 0
462    }
463
464    /// Current queue statistics.
465    pub fn stats(&self) -> QueueStats {
466        self.mapping.stats()
467    }
468}
469
470/// Queue consumer.
471///
472/// A process holding `Consumer` owns pops from the queue.
473pub struct Consumer {
474    mapping: Mapping,
475}
476
477impl Consumer {
478    /// Open an existing queue as consumer.
479    pub fn open() -> io::Result<Self> {
480        let mapping = Mapping::open_existing()?;
481        mapping.set_consumer_pid();
482
483        Ok(Self { mapping })
484    }
485
486    /// Pop one 48-byte value.
487    ///
488    /// Returns `None` when the queue is empty.
489    pub fn pop(&self) -> Option<[u8; PAYLOAD_SIZE]> {
490        self.mapping.pop()
491    }
492
493    /// Queue capacity in slots.
494    pub fn capacity(&self) -> u64 {
495        self.mapping.stats().capacity
496    }
497
498    /// Current queue length in slots.
499    pub fn len(&self) -> u64 {
500        self.mapping.stats().len
501    }
502
503    /// Returns `true` when the queue is empty.
504    pub fn is_empty(&self) -> bool {
505        self.len() == 0
506    }
507
508    /// Current queue statistics.
509    pub fn stats(&self) -> QueueStats {
510        self.mapping.stats()
511    }
512}
513
514/// Open the queue and return statistics.
515pub fn stats() -> io::Result<QueueStats> {
516    Ok(Mapping::open_existing()?.stats())
517}
518
519/// Remove the POSIX shared-memory object.
520///
521/// Removing an already absent object is treated as success.
522pub fn unlink() -> io::Result<()> {
523    let name = CString::new(SHM_NAME).unwrap();
524
525    let rc = unsafe { shm_unlink(name.as_ptr()) };
526
527    if rc != 0 {
528        let err = io::Error::last_os_error();
529
530        if err.raw_os_error() == Some(libc::ENOENT) {
531            return Ok(());
532        }
533
534        return Err(err);
535    }
536
537    Ok(())
538}
539
540fn validate_capacity(capacity: usize) -> io::Result<()> {
541    if capacity == 0 {
542        return Err(io::Error::new(
543            io::ErrorKind::InvalidInput,
544            "capacity must be greater than zero",
545        ));
546    }
547
548    if !capacity.is_power_of_two() {
549        return Err(io::Error::new(
550            io::ErrorKind::InvalidInput,
551            "capacity must be a power of two",
552        ));
553    }
554
555    Ok(())
556}
557
558fn region_size_for_capacity(capacity: usize) -> io::Result<usize> {
559    let slots_size = capacity.checked_mul(SLOT_SIZE).ok_or_else(|| {
560        io::Error::new(
561            io::ErrorKind::InvalidInput,
562            "capacity overflows region size",
563        )
564    })?;
565
566    BASE_SIZE
567        .checked_add(slots_size)
568        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "region size overflow"))
569}
570
571fn file_size(fd: i32) -> io::Result<usize> {
572    let mut stat_buf: libc::stat = unsafe { zeroed() };
573
574    let rc = unsafe { fstat(fd, &mut stat_buf) };
575
576    if rc != 0 {
577        return Err(io::Error::last_os_error());
578    }
579
580    if stat_buf.st_size <= 0 {
581        return Err(io::Error::new(
582            io::ErrorKind::InvalidData,
583            "shared memory object has invalid size",
584        ));
585    }
586
587    usize::try_from(stat_buf.st_size).map_err(|_| {
588        io::Error::new(
589            io::ErrorKind::InvalidData,
590            "shared memory object size does not fit usize",
591        )
592    })
593}
594
595#[cfg(test)]
596mod tests {
597    use super::*;
598
599    #[test]
600    fn capacity_must_be_non_zero() {
601        let err = validate_capacity(0).expect_err("zero capacity is invalid");
602        assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
603    }
604
605    #[test]
606    fn capacity_must_be_power_of_two() {
607        let err = validate_capacity(3).expect_err("non-power-of-two capacity is invalid");
608        assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
609    }
610
611    #[test]
612    fn power_of_two_capacity_is_valid() {
613        validate_capacity(1024).expect("power-of-two capacity is valid");
614    }
615
616    #[test]
617    fn region_size_calculation_matches_layout() {
618        let capacity = 1024;
619        let expected = size_of::<SharedHeader>()
620            + size_of::<CounterLine>()
621            + size_of::<CounterLine>()
622            + capacity * size_of::<Slot48>();
623
624        assert_eq!(region_size_for_capacity(capacity).unwrap(), expected);
625    }
626
627    #[test]
628    fn slot_layout_is_one_cacheline() {
629        assert_eq!(size_of::<Slot48>(), 64);
630        assert_eq!(PAYLOAD_SIZE, 48);
631    }
632}