1use std::collections::HashMap;
10use std::fmt;
11use std::str::FromStr;
12use std::sync::atomic::{AtomicU8, AtomicU16, AtomicU32, Ordering};
13use std::sync::{Arc, LazyLock, Mutex, Weak};
14
15use orbit_core::Fleet;
16#[cfg(unix)]
17use orbit_core::shm::{ShmRegion, ring_segment_name};
18
19use crate::{Error, Result, SLOT_EMPTY, SLOT_OCCUPIED, lock_unpoisoned};
20
21pub const CELL_TEXT_KIND: u8 = 227;
23pub const CELL_TEXT_CAPACITY: usize = orbit_core::compile::usize_from_env(
27 option_env!("ORBIT_CELL_TEXT_CAPACITY"),
28 1_024,
29);
30pub const CELL_TEXT_MAX: usize = orbit_core::compile::usize_from_env(
35 option_env!("ORBIT_CELL_TEXT_MAX"),
36 240,
37);
38
39#[cfg(unix)]
40const TEXT_MAGIC: u32 = 0x43_54_58_54; #[cfg(unix)]
42const TEXT_VERSION: u16 = 2;
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
46pub struct TextId {
47 index: u32,
48 generation: u32,
49}
50
51impl TextId {
52 pub const fn index(self) -> u32 {
53 self.index
54 }
55
56 pub const fn generation(self) -> u32 {
57 self.generation
58 }
59
60 pub const fn to_bits(self) -> u64 {
61 ((self.index as u64) << 32) | self.generation as u64
62 }
63
64 pub const fn from_bits(bits: u64) -> Self {
65 Self { index: (bits >> 32) as u32, generation: bits as u32 }
66 }
67}
68
69impl fmt::Display for TextId {
70 fn fmt(
71 &self,
72 f: &mut fmt::Formatter<'_>,
73 ) -> fmt::Result {
74 write!(f, "text:{}:{}", self.index, self.generation)
75 }
76}
77
78impl FromStr for TextId {
79 type Err = Error;
80
81 fn from_str(text: &str) -> Result<Self> {
82 let malformed = || Error::Malformed(text.to_owned());
83 let rest = text.strip_prefix("text:").ok_or_else(malformed)?;
84 let (index, generation) = rest.split_once(':').ok_or_else(malformed)?;
85 Ok(Self {
86 index: index.parse().map_err(|_| malformed())?,
87 generation: generation.parse().map_err(|_| malformed())?,
88 })
89 }
90}
91
92#[derive(Clone)]
94pub struct Text {
95 table: TextBackend,
96 id: TextId,
97}
98
99impl Text {
100 pub fn id(&self) -> TextId {
101 self.id
102 }
103
104 pub fn load(&self) -> Result<String> {
106 let slot = self.slot()?;
107 let mut bytes = vec![0_u8; CELL_TEXT_MAX];
108 loop {
109 let before = slot.version.load(Ordering::Acquire);
110 if before & 1 == 1 {
111 std::hint::spin_loop();
112 continue;
113 }
114 let len = usize::from(slot.len.load(Ordering::Relaxed))
115 .min(CELL_TEXT_MAX);
116 for (target, source) in
117 bytes[..len].iter_mut().zip(&slot.bytes[..len])
118 {
119 *target = source.load(Ordering::Relaxed);
120 }
121 std::sync::atomic::fence(Ordering::Acquire);
122 if slot.version.load(Ordering::Relaxed) == before {
123 bytes.truncate(len);
124 return String::from_utf8(bytes)
125 .map_err(|_| Error::Corrupt(self.id.to_string()));
126 }
127 }
128 }
129
130 pub fn len(&self) -> Result<usize> {
131 Ok(usize::from(self.slot()?.len.load(Ordering::Acquire)))
132 }
133
134 pub fn is_empty(&self) -> Result<bool> {
135 Ok(self.len()? == 0)
136 }
137
138 pub fn store(
140 &self,
141 text: &str,
142 ) -> Result<()> {
143 if text.len() > CELL_TEXT_MAX {
144 return Err(Error::TooLong { len: text.len(), max: CELL_TEXT_MAX });
145 }
146 let slot = self.slot()?;
147 let _write = slot.writer();
148 slot.write(0, text.as_bytes());
149 Ok(())
150 }
151
152 pub fn append(
155 &self,
156 text: &str,
157 ) -> Result<usize> {
158 let slot = self.slot()?;
159 let _write = slot.writer();
160 let current = usize::from(slot.len.load(Ordering::Relaxed));
161 let total = current + text.len();
162 if total > CELL_TEXT_MAX {
163 return Err(Error::TooLong { len: total, max: CELL_TEXT_MAX });
164 }
165 slot.write(current, text.as_bytes());
166 Ok(total)
167 }
168
169 pub fn release(self) -> Result<()> {
171 self.table.release(self.id)
172 }
173
174 pub fn version(&self) -> Result<u32> {
177 let slot = self.slot()?;
178 loop {
179 let version = slot.version.load(Ordering::Acquire);
180 if version & 1 == 0 {
181 return Ok(version);
182 }
183 std::hint::spin_loop();
184 }
185 }
186
187 pub fn wait_changed(
191 &self,
192 since: u32,
193 ) -> Result<u32> {
194 loop {
195 let slot = self.slot()?;
196 let now = slot.version.load(Ordering::SeqCst);
197 if now != since {
198 if now & 1 == 1 {
199 std::hint::spin_loop();
200 continue;
201 }
202 return Ok(now);
203 }
204 slot.waiters.fetch_add(1, Ordering::SeqCst);
205 let outcome = if slot.version.load(Ordering::SeqCst) == since {
206 crate::wait_on(&slot.version, since)
207 } else {
208 Ok(())
209 };
210 slot.waiters.fetch_sub(1, Ordering::SeqCst);
211 outcome?;
212 }
213 }
214
215 pub fn wait_changed_until(
220 &self,
221 since: u32,
222 interrupt: &core::sync::atomic::AtomicBool,
223 ) -> Result<Option<u32>> {
224 loop {
225 if interrupt.load(Ordering::SeqCst) {
226 return Ok(None);
227 }
228
229 let slot = self.slot()?;
230 let now = slot.version.load(Ordering::SeqCst);
231 if now != since {
232 if now & 1 == 1 {
233 std::hint::spin_loop();
234 continue;
235 }
236 return Ok(Some(now));
237 }
238
239 slot.waiters.fetch_add(1, Ordering::SeqCst);
240 let outcome = if slot.version.load(Ordering::SeqCst) == since
241 && !interrupt.load(Ordering::SeqCst)
242 {
243 crate::wait_on(&slot.version, since)
244 } else {
245 Ok(())
246 };
247 slot.waiters.fetch_sub(1, Ordering::SeqCst);
248 outcome?;
249 }
250 }
251
252 pub fn wake_waiters(&self) -> Result<()> {
255 crate::wake_on(&self.slot()?.version);
256 Ok(())
257 }
258
259 fn slot(&self) -> Result<&TextSlot> {
260 self.table.slot(self.id)
261 }
262}
263
264#[derive(Clone)]
265pub(crate) enum TextBackend {
266 InMemory(Arc<MemoryTextTable>),
267 #[cfg(unix)]
268 Shm(Arc<ShmTextTable>),
269}
270
271impl TextBackend {
272 pub(crate) fn new(fleet: &Arc<Fleet>) -> Result<Self> {
273 Ok(if fleet.is_shm() {
274 #[cfg(unix)]
275 {
276 Self::Shm(Arc::new(ShmTextTable::open_or_create(
277 &ring_segment_name(fleet.name(), CELL_TEXT_KIND),
278 )?))
279 }
280 #[cfg(not(unix))]
281 unreachable!("non-Unix fleets cannot use POSIX SHM")
282 } else {
283 Self::InMemory(memory_table(fleet))
284 })
285 }
286
287 pub(crate) fn allocate(
288 &self,
289 text: &str,
290 ) -> Result<Text> {
291 if text.len() > CELL_TEXT_MAX {
292 return Err(Error::TooLong { len: text.len(), max: CELL_TEXT_MAX });
293 }
294 let id = self.with_structure(|slots, hint| {
295 let capacity = slots.len();
296 let start = hint.load(Ordering::Relaxed) as usize;
297 for offset in 0..capacity {
298 let index = (start + offset) & (capacity - 1);
299 let slot = &slots[index];
300 if slot.state.load(Ordering::Acquire) == SLOT_EMPTY {
301 let generation = slot.install(text.as_bytes());
302 hint.store(
303 ((index + 1) & (capacity - 1)) as u32,
304 Ordering::Relaxed,
305 );
306 return Ok(TextId { index: index as u32, generation });
307 }
308 }
309 Err(Error::Full { capacity })
310 })?;
311 Ok(Text { table: self.clone(), id })
312 }
313
314 pub(crate) fn open(
315 &self,
316 id: TextId,
317 ) -> Result<Text> {
318 self.slot(id)?;
319 Ok(Text { table: self.clone(), id })
320 }
321
322 pub(crate) fn release(
323 &self,
324 id: TextId,
325 ) -> Result<()> {
326 self.with_structure(|slots, _| {
327 let slot = slots
328 .get(id.index as usize)
329 .filter(|slot| slot.is(id))
330 .ok_or(Error::StaleText(id))?;
331 slot.state.store(SLOT_EMPTY, Ordering::Release);
332 slot.version.fetch_add(2, Ordering::SeqCst);
334 slot.wake_waiters();
335 Ok(())
336 })
337 }
338
339 pub(crate) fn is_live(
340 &self,
341 id: TextId,
342 ) -> bool {
343 self.slot(id).is_ok()
344 }
345
346 pub(crate) fn reset_all(&self) -> Result<()> {
347 self.with_structure(|slots, hint| {
348 for slot in slots {
349 slot.state.store(SLOT_EMPTY, Ordering::Release);
350 slot.version.fetch_add(2, Ordering::SeqCst);
351 slot.wake_waiters();
352 }
353 hint.store(0, Ordering::Relaxed);
354 Ok(())
355 })
356 }
357
358 #[cfg(unix)]
359 pub(crate) fn unlink(&self) -> Result<()> {
360 match self {
361 Self::InMemory(_) => self.reset_all(),
362 Self::Shm(table) => table.region.unlink().map_err(Error::Io),
363 }
364 }
365
366 fn slots(&self) -> &[TextSlot] {
367 match self {
368 Self::InMemory(table) => &table.slots,
369 #[cfg(unix)]
370 Self::Shm(table) => table.slots(),
371 }
372 }
373
374 fn slot(
375 &self,
376 id: TextId,
377 ) -> Result<&TextSlot> {
378 self.slots()
379 .get(id.index as usize)
380 .filter(|slot| slot.is(id))
381 .ok_or(Error::StaleText(id))
382 }
383
384 fn with_structure<T>(
385 &self,
386 operation: impl FnOnce(&[TextSlot], &AtomicU32) -> Result<T>,
387 ) -> Result<T> {
388 match self {
389 Self::InMemory(table) => {
390 let _local = lock_unpoisoned(&table.structural_lock);
391 operation(&table.slots, &table.hint)
392 }
393 #[cfg(unix)]
394 Self::Shm(table) => table.with_structure(operation),
395 }
396 }
397}
398
399#[repr(C, align(64))]
400struct TextStateHeader {
401 #[cfg(unix)]
402 magic: u32,
403 #[cfg(not(unix))]
404 _magic: u32,
405 #[cfg(unix)]
406 version: u16,
407 #[cfg(not(unix))]
408 _version: u16,
409 header_size: u16,
410 capacity: u32,
411 slot_size: u32,
412 hint: AtomicU32,
413 _reserved: [u8; 44],
414}
415
416impl TextStateHeader {
417 fn new() -> Self {
418 Self {
419 #[cfg(unix)]
420 magic: TEXT_MAGIC,
421 #[cfg(not(unix))]
422 _magic: 0,
423 #[cfg(unix)]
424 version: TEXT_VERSION,
425 #[cfg(not(unix))]
426 _version: 0,
427 header_size: size_of::<Self>() as u16,
428 capacity: CELL_TEXT_CAPACITY as u32,
429 slot_size: size_of::<TextSlot>() as u32,
430 hint: AtomicU32::new(0),
431 _reserved: [0; 44],
432 }
433 }
434}
435
436#[repr(C, align(64))]
439struct TextSlot {
440 state: AtomicU8,
441 writer: AtomicU8,
442 len: AtomicU16,
443 generation: AtomicU32,
444 version: AtomicU32,
447 waiters: AtomicU32,
449 bytes: [AtomicU8; CELL_TEXT_MAX],
450}
451
452impl TextSlot {
453 fn empty() -> Self {
454 Self {
455 state: AtomicU8::new(SLOT_EMPTY),
456 writer: AtomicU8::new(0),
457 len: AtomicU16::new(0),
458 generation: AtomicU32::new(0),
459 version: AtomicU32::new(0),
460 waiters: AtomicU32::new(0),
461 bytes: std::array::from_fn(|_| AtomicU8::new(0)),
462 }
463 }
464
465 fn is(
466 &self,
467 id: TextId,
468 ) -> bool {
469 self.state.load(Ordering::Acquire) == SLOT_OCCUPIED
470 && self.generation.load(Ordering::Relaxed) == id.generation
471 }
472
473 fn install(
475 &self,
476 text: &[u8],
477 ) -> u32 {
478 let generation =
479 self.generation.load(Ordering::Relaxed).wrapping_add(1).max(1);
480 for (index, byte) in self.bytes.iter().enumerate() {
481 byte.store(
482 text.get(index).copied().unwrap_or(0),
483 Ordering::Relaxed,
484 );
485 }
486 self.len.store(text.len() as u16, Ordering::Relaxed);
487 self.version.store(0, Ordering::Relaxed);
488 self.waiters.store(0, Ordering::Relaxed);
489 self.writer.store(0, Ordering::Relaxed);
490 self.generation.store(generation, Ordering::Relaxed);
491 self.state.store(SLOT_OCCUPIED, Ordering::Release);
492 generation
493 }
494
495 fn writer(&self) -> WriterGuard<'_> {
497 while self
498 .writer
499 .compare_exchange_weak(0, 1, Ordering::Acquire, Ordering::Relaxed)
500 .is_err()
501 {
502 std::hint::spin_loop();
503 }
504 WriterGuard(self)
505 }
506
507 fn write(
510 &self,
511 offset: usize,
512 bytes: &[u8],
513 ) {
514 debug_assert!(offset + bytes.len() <= CELL_TEXT_MAX);
515 self.version.fetch_add(1, Ordering::AcqRel);
516 for (target, source) in
517 self.bytes[offset..offset + bytes.len()].iter().zip(bytes)
518 {
519 target.store(*source, Ordering::Relaxed);
520 }
521 self.len.store((offset + bytes.len()) as u16, Ordering::Relaxed);
522 self.version.fetch_add(1, Ordering::SeqCst);
523 self.wake_waiters();
524 }
525
526 fn wake_waiters(&self) {
528 if self.waiters.load(Ordering::SeqCst) > 0 {
529 crate::wake_on(&self.version);
530 }
531 }
532}
533
534struct WriterGuard<'a>(&'a TextSlot);
535
536impl Drop for WriterGuard<'_> {
537 fn drop(&mut self) {
538 self.0.writer.store(0, Ordering::Release);
539 }
540}
541
542pub(crate) struct MemoryTextTable {
543 _fleet: Arc<Fleet>,
546 slots: Vec<TextSlot>,
547 hint: AtomicU32,
548 structural_lock: Mutex<()>,
549}
550
551impl MemoryTextTable {
552 fn new(fleet: Arc<Fleet>) -> Self {
553 Self {
554 _fleet: fleet,
555 slots: (0..CELL_TEXT_CAPACITY).map(|_| TextSlot::empty()).collect(),
556 hint: AtomicU32::new(0),
557 structural_lock: Mutex::new(()),
558 }
559 }
560}
561
562type MemoryRegistry = HashMap<usize, Weak<MemoryTextTable>>;
563
564static MEMORY_TABLES: LazyLock<Mutex<MemoryRegistry>> =
565 LazyLock::new(|| Mutex::new(HashMap::new()));
566
567fn memory_table(fleet: &Arc<Fleet>) -> Arc<MemoryTextTable> {
568 let fleet_identity = Arc::as_ptr(fleet) as usize;
569 let mut tables = lock_unpoisoned(&MEMORY_TABLES);
570 tables.retain(|_, table| table.strong_count() > 0);
571 if let Some(table) = tables.get(&fleet_identity).and_then(Weak::upgrade) {
572 return table;
573 }
574 let table = Arc::new(MemoryTextTable::new(Arc::clone(fleet)));
575 tables.insert(fleet_identity, Arc::downgrade(&table));
576 table
577}
578
579#[cfg(unix)]
580pub(crate) struct ShmTextTable {
581 region: ShmRegion,
582 structural_lock: Mutex<()>,
583}
584
585#[cfg(unix)]
586impl ShmTextTable {
587 fn open_or_create(name: &str) -> Result<Self> {
588 use std::{io, ptr};
589
590 let (region, _initialization_lock) =
591 ShmRegion::open_or_create_locked(name, shm_segment_size())?;
592 if region.created() {
593 unsafe {
594 ptr::write(
595 region.as_ptr().cast::<TextStateHeader>(),
596 TextStateHeader::new(),
597 );
598 let slots = region.as_ptr().add(size_of::<TextStateHeader>());
599 ptr::write_bytes(
600 slots,
601 0,
602 CELL_TEXT_CAPACITY * size_of::<TextSlot>(),
603 );
604 }
605 } else {
606 let header = unsafe { &*region.as_ptr().cast::<TextStateHeader>() };
607 if header.magic != TEXT_MAGIC {
608 return Err(Error::Io(io::Error::new(
609 io::ErrorKind::InvalidData,
610 format!(
611 "SHM segment {name} has wrong magic 0x{:08X} (expected 0x{TEXT_MAGIC:08X})",
612 header.magic
613 ),
614 )));
615 }
616 if header.version != TEXT_VERSION
617 || usize::from(header.header_size)
618 != size_of::<TextStateHeader>()
619 || header.capacity as usize != CELL_TEXT_CAPACITY
620 || header.slot_size as usize != size_of::<TextSlot>()
621 {
622 return Err(Error::Io(io::Error::new(
623 io::ErrorKind::InvalidData,
624 format!(
625 "SHM segment {name} has an incompatible text-cell layout"
626 ),
627 )));
628 }
629 }
630 Ok(Self { region, structural_lock: Mutex::new(()) })
631 }
632
633 fn header(&self) -> &TextStateHeader {
634 unsafe { &*self.region.as_ptr().cast::<TextStateHeader>() }
635 }
636
637 fn slots(&self) -> &[TextSlot] {
638 unsafe {
639 std::slice::from_raw_parts(
640 self.region
641 .as_ptr()
642 .add(size_of::<TextStateHeader>())
643 .cast::<TextSlot>(),
644 CELL_TEXT_CAPACITY,
645 )
646 }
647 }
648
649 fn with_structure<T>(
650 &self,
651 operation: impl FnOnce(&[TextSlot], &AtomicU32) -> Result<T>,
652 ) -> Result<T> {
653 let _local = lock_unpoisoned(&self.structural_lock);
654 let _process = self.region.lock_exclusive()?;
655 operation(self.slots(), &self.header().hint)
656 }
657}
658
659#[cfg(unix)]
660fn shm_segment_size() -> usize {
661 size_of::<TextStateHeader>() + CELL_TEXT_CAPACITY * size_of::<TextSlot>()
662}
663
664const _: () = assert!(CELL_TEXT_CAPACITY.is_power_of_two());
665const _: () = assert!(CELL_TEXT_MAX > 0 && CELL_TEXT_MAX <= u16::MAX as usize);
666const _: () = assert!(size_of::<TextStateHeader>() == 64);
667const _: () =
669 assert!(size_of::<TextSlot>() == (16 + CELL_TEXT_MAX).next_multiple_of(64));