1use std::collections::HashMap;
16use std::fmt;
17use std::marker::PhantomData;
18use std::str::FromStr;
19use std::sync::atomic::{AtomicU8, AtomicU32, AtomicU64, Ordering};
20use std::sync::{Arc, LazyLock, Mutex, MutexGuard, Weak};
21
22use orbit_core::Fleet;
23#[cfg(unix)]
24use orbit_core::shm::{ShmRegion, ring_segment_name};
25
26mod text;
27
28pub use text::{CELL_TEXT_CAPACITY, CELL_TEXT_KIND, CELL_TEXT_MAX, Text, TextId};
29
30pub const CELL_STATE_KIND: u8 = 233;
32pub const CELL_CAPACITY: usize =
38 orbit_core::compile::usize_from_env(option_env!("ORBIT_CELL_CAPACITY"), 4_096);
39
40pub(crate) const SLOT_EMPTY: u8 = 0;
41pub(crate) const SLOT_OCCUPIED: u8 = 1;
42
43#[cfg(unix)]
44const STATE_MAGIC: u32 = 0x43_45_4C_4C; #[cfg(unix)]
46const STATE_VERSION: u16 = 1;
47
48pub type Result<T, E = Error> = std::result::Result<T, E>;
49
50#[derive(Debug)]
51pub enum Error {
52 Stale(CellId),
55 StaleText(TextId),
57 TooLong {
59 len: usize,
60 max: usize,
61 },
62 Corrupt(String),
65 TypeMismatch {
67 id: CellId,
68 expected: &'static str,
69 found: &'static str,
70 },
71 Overflow,
73 Full {
75 capacity: usize,
76 },
77 Malformed(String),
79 Io(std::io::Error),
80}
81
82impl fmt::Display for Error {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 match self {
85 Self::Stale(id) => write!(f, "cell {id} has been released"),
86 Self::StaleText(id) => write!(f, "text cell {id} has been released"),
87 Self::TooLong { len, max } => {
88 write!(f, "text does not fit the cell: len={len} max={max}")
89 }
90 Self::Corrupt(id) => write!(f, "text cell {id} holds bytes that are not UTF-8"),
91 Self::TypeMismatch {
92 id,
93 expected,
94 found,
95 } => write!(f, "cell {id} holds {found}, not {expected}"),
96 Self::Overflow => f.write_str("cell value is outside the integer range"),
97 Self::Full { capacity } => write!(f, "cell table is full: capacity={capacity}"),
98 Self::Malformed(text) => write!(f, "not a cell id: {text:?}"),
99 Self::Io(error) => write!(f, "Orbit cell io error: {error}"),
100 }
101 }
102}
103
104impl std::error::Error for Error {
105 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
106 match self {
107 Self::Io(error) => Some(error),
108 _ => None,
109 }
110 }
111}
112
113impl From<std::io::Error> for Error {
114 fn from(value: std::io::Error) -> Self {
115 Self::Io(value)
116 }
117}
118
119#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
124pub struct CellId {
125 index: u32,
126 generation: u32,
127}
128
129impl CellId {
130 pub const fn index(self) -> u32 {
131 self.index
132 }
133
134 pub const fn generation(self) -> u32 {
135 self.generation
136 }
137
138 pub const fn to_bits(self) -> u64 {
140 ((self.index as u64) << 32) | self.generation as u64
141 }
142
143 pub const fn from_bits(bits: u64) -> Self {
144 Self {
145 index: (bits >> 32) as u32,
146 generation: bits as u32,
147 }
148 }
149}
150
151impl fmt::Display for CellId {
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153 write!(f, "cell:{}:{}", self.index, self.generation)
154 }
155}
156
157impl FromStr for CellId {
158 type Err = Error;
159
160 fn from_str(text: &str) -> Result<Self> {
161 let malformed = || Error::Malformed(text.to_owned());
162 let rest = text.strip_prefix("cell:").ok_or_else(malformed)?;
163 let (index, generation) = rest.split_once(':').ok_or_else(malformed)?;
164 Ok(Self {
165 index: index.parse().map_err(|_| malformed())?,
166 generation: generation.parse().map_err(|_| malformed())?,
167 })
168 }
169}
170
171pub trait CellType: Copy + Send + Sync + 'static {
174 const TAG: u8;
175 const NAME: &'static str;
176
177 fn to_bits(self) -> u64;
178 fn from_bits(bits: u64) -> Self;
179}
180
181impl CellType for i64 {
182 const TAG: u8 = 1;
183 const NAME: &'static str = "int";
184
185 fn to_bits(self) -> u64 {
186 self as u64
187 }
188
189 fn from_bits(bits: u64) -> Self {
190 bits as i64
191 }
192}
193
194impl CellType for u64 {
195 const TAG: u8 = 2;
196 const NAME: &'static str = "unsigned";
197
198 fn to_bits(self) -> u64 {
199 self
200 }
201
202 fn from_bits(bits: u64) -> Self {
203 bits
204 }
205}
206
207impl CellType for f64 {
208 const TAG: u8 = 3;
209 const NAME: &'static str = "float";
210
211 fn to_bits(self) -> u64 {
212 self.to_bits()
213 }
214
215 fn from_bits(bits: u64) -> Self {
216 f64::from_bits(bits)
217 }
218}
219
220impl CellType for bool {
221 const TAG: u8 = 4;
222 const NAME: &'static str = "bool";
223
224 fn to_bits(self) -> u64 {
225 u64::from(self)
226 }
227
228 fn from_bits(bits: u64) -> Self {
229 bits != 0
230 }
231}
232
233fn type_name(tag: u8) -> &'static str {
234 match tag {
235 1 => i64::NAME,
236 2 => u64::NAME,
237 3 => f64::NAME,
238 4 => bool::NAME,
239 _ => "unknown",
240 }
241}
242
243#[derive(Clone)]
245pub struct Cells {
246 backend: CellBackend,
247 text: text::TextBackend,
248}
249
250#[derive(Clone)]
251enum CellBackend {
252 InMemory(Arc<MemoryCellTable>),
253 #[cfg(unix)]
254 Shm(Arc<ShmCellTable>),
255}
256
257impl Cells {
258 pub fn new(fleet: Arc<Fleet>) -> Result<Self> {
259 let backend = if fleet.is_shm() {
260 #[cfg(unix)]
261 {
262 CellBackend::Shm(Arc::new(ShmCellTable::open_or_create(&ring_segment_name(
263 fleet.name(),
264 CELL_STATE_KIND,
265 ))?))
266 }
267 #[cfg(not(unix))]
268 unreachable!("non-Unix fleets cannot use POSIX SHM")
269 } else {
270 CellBackend::InMemory(memory_table(&fleet))
271 };
272 let text = text::TextBackend::new(&fleet)?;
273 Ok(Self { backend, text })
274 }
275
276 pub fn allocate_text(&self, initial: &str) -> Result<Text> {
278 self.text.allocate(initial)
279 }
280
281 pub fn open_text(&self, id: TextId) -> Result<Text> {
283 self.text.open(id)
284 }
285
286 pub fn release_text(&self, id: TextId) -> Result<()> {
287 self.text.release(id)
288 }
289
290 pub fn is_text_live(&self, id: TextId) -> bool {
291 self.text.is_live(id)
292 }
293
294 pub fn allocate<T: CellType>(&self, initial: T) -> Result<Orbital<T>> {
297 let id = self.with_structure(|slots, hint| {
298 let capacity = slots.len();
299 let start = hint.load(Ordering::Relaxed) as usize;
300 for offset in 0..capacity {
301 let index = (start + offset) & (capacity - 1);
302 let slot = &slots[index];
303 if slot.state.load(Ordering::Acquire) == SLOT_EMPTY {
304 let generation = slot.install(T::TAG, initial.to_bits());
305 hint.store(((index + 1) & (capacity - 1)) as u32, Ordering::Relaxed);
306 return Ok(CellId {
307 index: index as u32,
308 generation,
309 });
310 }
311 }
312 Err(Error::Full { capacity })
313 })?;
314 Ok(Orbital {
315 cells: self.clone(),
316 id,
317 _type: PhantomData,
318 })
319 }
320
321 pub fn open<T: CellType>(&self, id: CellId) -> Result<Orbital<T>> {
323 let slot = self.slot(id)?;
324 let found = slot.tag.load(Ordering::Relaxed);
325 if found != T::TAG {
326 return Err(Error::TypeMismatch {
327 id,
328 expected: T::NAME,
329 found: type_name(found),
330 });
331 }
332 Ok(Orbital {
333 cells: self.clone(),
334 id,
335 _type: PhantomData,
336 })
337 }
338
339 pub fn release(&self, id: CellId) -> Result<()> {
341 self.with_structure(|slots, _| {
342 let slot = slots
343 .get(id.index as usize)
344 .filter(|slot| slot.is(id))
345 .ok_or(Error::Stale(id))?;
346 slot.state.store(SLOT_EMPTY, Ordering::Release);
347 Ok(())
348 })
349 }
350
351 pub fn is_live(&self, id: CellId) -> bool {
353 self.slot(id).is_ok()
354 }
355
356 pub fn reset_all(&self) -> Result<()> {
358 self.with_structure(|slots, hint| {
359 for slot in slots {
360 slot.state.store(SLOT_EMPTY, Ordering::Release);
361 }
362 hint.store(0, Ordering::Relaxed);
363 Ok(())
364 })?;
365 self.text.reset_all()
366 }
367
368 #[cfg(unix)]
371 pub fn unlink(&self) -> Result<()> {
372 match &self.backend {
373 CellBackend::InMemory(_) => self.reset_all()?,
374 CellBackend::Shm(table) => table.region.unlink().map_err(Error::Io)?,
375 }
376 self.text.unlink()
377 }
378
379 fn slots(&self) -> &[CellSlot] {
380 match &self.backend {
381 CellBackend::InMemory(table) => &table.slots,
382 #[cfg(unix)]
383 CellBackend::Shm(table) => table.slots(),
384 }
385 }
386
387 fn slot(&self, id: CellId) -> Result<&CellSlot> {
388 self.slots()
389 .get(id.index as usize)
390 .filter(|slot| slot.is(id))
391 .ok_or(Error::Stale(id))
392 }
393
394 fn with_structure<T>(
395 &self,
396 operation: impl FnOnce(&[CellSlot], &AtomicU32) -> Result<T>,
397 ) -> Result<T> {
398 match &self.backend {
399 CellBackend::InMemory(table) => {
400 let _local = lock_unpoisoned(&table.structural_lock);
401 operation(&table.slots, &table.hint)
402 }
403 #[cfg(unix)]
404 CellBackend::Shm(table) => table.with_structure(operation),
405 }
406 }
407}
408
409#[derive(Clone)]
411pub struct Orbital<T: CellType> {
412 cells: Cells,
413 id: CellId,
414 _type: PhantomData<T>,
415}
416
417impl<T: CellType> Orbital<T> {
418 pub fn id(&self) -> CellId {
419 self.id
420 }
421
422 pub fn load(&self) -> Result<T> {
423 Ok(T::from_bits(self.slot()?.value.load(Ordering::Acquire)))
424 }
425
426 pub fn store(&self, value: T) -> Result<()> {
427 self.slot()?.value.store(value.to_bits(), Ordering::Release);
428 Ok(())
429 }
430
431 pub fn swap(&self, value: T) -> Result<T> {
433 Ok(T::from_bits(
434 self.slot()?.value.swap(value.to_bits(), Ordering::AcqRel),
435 ))
436 }
437
438 pub fn compare_exchange(&self, current: T, new: T) -> Result<std::result::Result<T, T>> {
441 Ok(self
442 .slot()?
443 .value
444 .compare_exchange(
445 current.to_bits(),
446 new.to_bits(),
447 Ordering::AcqRel,
448 Ordering::Acquire,
449 )
450 .map(T::from_bits)
451 .map_err(T::from_bits))
452 }
453
454 pub fn release(self) -> Result<()> {
456 self.cells.release(self.id)
457 }
458
459 fn slot(&self) -> Result<&CellSlot> {
460 self.cells.slot(self.id)
461 }
462}
463
464impl Orbital<i64> {
465 pub fn fetch_add(&self, by: i64) -> Result<i64> {
467 let slot = self.slot()?;
468 slot.value
469 .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
470 (bits as i64).checked_add(by).map(|value| value as u64)
471 })
472 .map(|previous| (previous as i64) + by)
473 .map_err(|_| Error::Overflow)
474 }
475
476 pub fn fetch_sub(&self, by: i64) -> Result<i64> {
477 self.fetch_add(by.checked_neg().ok_or(Error::Overflow)?)
478 }
479}
480
481impl Orbital<u64> {
482 pub fn fetch_add(&self, by: u64) -> Result<u64> {
483 let slot = self.slot()?;
484 slot.value
485 .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
486 bits.checked_add(by)
487 })
488 .map(|previous| previous + by)
489 .map_err(|_| Error::Overflow)
490 }
491
492 pub fn fetch_sub(&self, by: u64) -> Result<u64> {
493 let slot = self.slot()?;
494 slot.value
495 .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
496 bits.checked_sub(by)
497 })
498 .map(|previous| previous - by)
499 .map_err(|_| Error::Overflow)
500 }
501}
502
503impl Orbital<f64> {
504 pub fn fetch_add(&self, by: f64) -> Result<f64> {
507 let slot = self.slot()?;
508 let previous = slot
509 .value
510 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
511 Some((f64::from_bits(bits) + by).to_bits())
512 })
513 .unwrap_or_else(|bits| bits);
514 Ok(f64::from_bits(previous) + by)
515 }
516}
517
518impl Orbital<bool> {
519 pub fn set(&self) -> Result<bool> {
521 self.swap(true)
522 }
523
524 pub fn clear(&self) -> Result<bool> {
526 self.swap(false)
527 }
528}
529
530#[repr(C, align(64))]
531struct CellStateHeader {
532 #[cfg(unix)]
533 magic: u32,
534 #[cfg(not(unix))]
535 _magic: u32,
536 #[cfg(unix)]
537 version: u16,
538 #[cfg(not(unix))]
539 _version: u16,
540 header_size: u16,
541 capacity: u32,
542 slot_size: u32,
543 hint: AtomicU32,
545 _reserved: [u8; 44],
546}
547
548impl CellStateHeader {
549 fn new() -> Self {
550 Self {
551 #[cfg(unix)]
552 magic: STATE_MAGIC,
553 #[cfg(not(unix))]
554 _magic: 0,
555 #[cfg(unix)]
556 version: STATE_VERSION,
557 #[cfg(not(unix))]
558 _version: 0,
559 header_size: size_of::<Self>() as u16,
560 capacity: CELL_CAPACITY as u32,
561 slot_size: size_of::<CellSlot>() as u32,
562 hint: AtomicU32::new(0),
563 _reserved: [0; 44],
564 }
565 }
566}
567
568#[repr(C, align(64))]
572struct CellSlot {
573 state: AtomicU8,
574 tag: AtomicU8,
575 _reserved: [u8; 2],
576 generation: AtomicU32,
577 value: AtomicU64,
578 _padding: [u8; 48],
579}
580
581impl CellSlot {
582 fn empty() -> Self {
583 Self {
584 state: AtomicU8::new(SLOT_EMPTY),
585 tag: AtomicU8::new(0),
586 _reserved: [0; 2],
587 generation: AtomicU32::new(0),
588 value: AtomicU64::new(0),
589 _padding: [0; 48],
590 }
591 }
592
593 fn is(&self, id: CellId) -> bool {
594 self.state.load(Ordering::Acquire) == SLOT_OCCUPIED
596 && self.generation.load(Ordering::Relaxed) == id.generation
597 }
598
599 fn install(&self, tag: u8, value: u64) -> u32 {
602 let generation = self
603 .generation
604 .load(Ordering::Relaxed)
605 .wrapping_add(1)
606 .max(1);
607 self.tag.store(tag, Ordering::Relaxed);
608 self.value.store(value, Ordering::Relaxed);
609 self.generation.store(generation, Ordering::Relaxed);
610 self.state.store(SLOT_OCCUPIED, Ordering::Release);
611 generation
612 }
613}
614
615pub(crate) fn lock_unpoisoned<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
616 mutex.lock().unwrap_or_else(|error| error.into_inner())
617}
618
619struct MemoryCellTable {
620 slots: Vec<CellSlot>,
621 hint: AtomicU32,
622 structural_lock: Mutex<()>,
623}
624
625impl MemoryCellTable {
626 fn new() -> Self {
627 Self {
628 slots: (0..CELL_CAPACITY).map(|_| CellSlot::empty()).collect(),
629 hint: AtomicU32::new(0),
630 structural_lock: Mutex::new(()),
631 }
632 }
633}
634
635type MemoryRegistry = HashMap<usize, Weak<MemoryCellTable>>;
636
637static MEMORY_TABLES: LazyLock<Mutex<MemoryRegistry>> =
638 LazyLock::new(|| Mutex::new(HashMap::new()));
639
640fn memory_table(fleet: &Arc<Fleet>) -> Arc<MemoryCellTable> {
641 let fleet_identity = Arc::as_ptr(fleet) as usize;
642 let mut tables = lock_unpoisoned(&MEMORY_TABLES);
643 tables.retain(|_, table| table.strong_count() > 0);
644 if let Some(table) = tables.get(&fleet_identity).and_then(Weak::upgrade) {
645 return table;
646 }
647 let table = Arc::new(MemoryCellTable::new());
648 tables.insert(fleet_identity, Arc::downgrade(&table));
649 table
650}
651
652#[cfg(unix)]
653struct ShmCellTable {
654 region: ShmRegion,
655 structural_lock: Mutex<()>,
656}
657
658#[cfg(unix)]
659impl ShmCellTable {
660 fn open_or_create(name: &str) -> Result<Self> {
661 use std::{io, ptr};
662
663 let (region, _initialization_lock) =
664 ShmRegion::open_or_create_locked(name, shm_segment_size())?;
665 if region.created() {
666 unsafe {
667 ptr::write(
668 region.as_ptr().cast::<CellStateHeader>(),
669 CellStateHeader::new(),
670 );
671 let slots = region.as_ptr().add(size_of::<CellStateHeader>());
672 ptr::write_bytes(slots, 0, CELL_CAPACITY * size_of::<CellSlot>());
673 }
674 } else {
675 let header = unsafe { &*region.as_ptr().cast::<CellStateHeader>() };
676 if header.magic != STATE_MAGIC {
677 return Err(Error::Io(io::Error::new(
678 io::ErrorKind::InvalidData,
679 format!(
680 "SHM segment {name} has wrong magic 0x{:08X} (expected 0x{STATE_MAGIC:08X})",
681 header.magic
682 ),
683 )));
684 }
685 if header.version != STATE_VERSION
686 || usize::from(header.header_size) != size_of::<CellStateHeader>()
687 || header.capacity as usize != CELL_CAPACITY
688 || header.slot_size as usize != size_of::<CellSlot>()
689 {
690 return Err(Error::Io(io::Error::new(
691 io::ErrorKind::InvalidData,
692 format!("SHM segment {name} has an incompatible cell-state layout"),
693 )));
694 }
695 }
696 Ok(Self {
697 region,
698 structural_lock: Mutex::new(()),
699 })
700 }
701
702 fn header(&self) -> &CellStateHeader {
703 unsafe { &*self.region.as_ptr().cast::<CellStateHeader>() }
704 }
705
706 fn slots(&self) -> &[CellSlot] {
707 unsafe {
708 std::slice::from_raw_parts(
709 self.region
710 .as_ptr()
711 .add(size_of::<CellStateHeader>())
712 .cast::<CellSlot>(),
713 CELL_CAPACITY,
714 )
715 }
716 }
717
718 fn with_structure<T>(
719 &self,
720 operation: impl FnOnce(&[CellSlot], &AtomicU32) -> Result<T>,
721 ) -> Result<T> {
722 let _local = lock_unpoisoned(&self.structural_lock);
723 let _process = self.region.lock_exclusive()?;
724 operation(self.slots(), &self.header().hint)
725 }
726}
727
728#[cfg(unix)]
729fn shm_segment_size() -> usize {
730 size_of::<CellStateHeader>() + CELL_CAPACITY * size_of::<CellSlot>()
731}
732
733const _: () = assert!(CELL_CAPACITY.is_power_of_two());
734const _: () = assert!(CELL_CAPACITY <= u32::MAX as usize);
735const _: () = assert!(size_of::<CellStateHeader>() == 64);
736const _: () = assert!(size_of::<CellSlot>() == 64);