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