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 { index: (bits >> 32) as u32, generation: bits as u32 }
62 }
63}
64
65impl fmt::Display for TextId {
66 fn fmt(
67 &self,
68 f: &mut fmt::Formatter<'_>
69 ) -> 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(
132 &self,
133 text: &str
134 ) -> Result<()> {
135 if text.len() > CELL_TEXT_MAX {
136 return Err(Error::TooLong { len: text.len(), max: CELL_TEXT_MAX });
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(
147 &self,
148 text: &str
149 ) -> Result<usize> {
150 let slot = self.slot()?;
151 let _write = slot.writer();
152 let current = usize::from(slot.len.load(Ordering::Relaxed));
153 let total = current + text.len();
154 if total > CELL_TEXT_MAX {
155 return Err(Error::TooLong { len: total, max: CELL_TEXT_MAX });
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(
183 &self,
184 since: u32
185 ) -> Result<u32> {
186 loop {
187 let slot = self.slot()?;
188 let now = slot.version.load(Ordering::SeqCst);
189 if now != since {
190 if now & 1 == 1 {
191 std::hint::spin_loop();
192 continue;
193 }
194 return Ok(now);
195 }
196 slot.waiters.fetch_add(1, Ordering::SeqCst);
197 let outcome = if slot.version.load(Ordering::SeqCst) == since {
198 crate::wait_on(&slot.version, since)
199 } else {
200 Ok(())
201 };
202 slot.waiters.fetch_sub(1, Ordering::SeqCst);
203 outcome?;
204 }
205 }
206
207 pub fn wait_changed_until(
212 &self,
213 since: u32,
214 interrupt: &core::sync::atomic::AtomicBool
215 ) -> Result<Option<u32>> {
216 loop {
217 if interrupt.load(Ordering::SeqCst) {
218 return Ok(None);
219 }
220
221 let slot = self.slot()?;
222 let now = slot.version.load(Ordering::SeqCst);
223 if now != since {
224 if now & 1 == 1 {
225 std::hint::spin_loop();
226 continue;
227 }
228 return Ok(Some(now));
229 }
230
231 slot.waiters.fetch_add(1, Ordering::SeqCst);
232 let outcome = if slot.version.load(Ordering::SeqCst) == since
233 && !interrupt.load(Ordering::SeqCst)
234 {
235 crate::wait_on(&slot.version, since)
236 } else {
237 Ok(())
238 };
239 slot.waiters.fetch_sub(1, Ordering::SeqCst);
240 outcome?;
241 }
242 }
243
244 pub fn wake_waiters(&self) -> Result<()> {
247 crate::wake_on(&self.slot()?.version);
248 Ok(())
249 }
250
251 fn slot(&self) -> Result<&TextSlot> {
252 self.table.slot(self.id)
253 }
254}
255
256#[derive(Clone)]
257pub(crate) enum TextBackend {
258 InMemory(Arc<MemoryTextTable>),
259 #[cfg(unix)]
260 Shm(Arc<ShmTextTable>)
261}
262
263impl TextBackend {
264 pub(crate) fn new(fleet: &Arc<Fleet>) -> Result<Self> {
265 Ok(if fleet.is_shm() {
266 #[cfg(unix)]
267 {
268 Self::Shm(Arc::new(ShmTextTable::open_or_create(&ring_segment_name(
269 fleet.name(),
270 CELL_TEXT_KIND
271 ))?))
272 }
273 #[cfg(not(unix))]
274 unreachable!("non-Unix fleets cannot use POSIX SHM")
275 } else {
276 Self::InMemory(memory_table(fleet))
277 })
278 }
279
280 pub(crate) fn allocate(
281 &self,
282 text: &str
283 ) -> Result<Text> {
284 if text.len() > CELL_TEXT_MAX {
285 return Err(Error::TooLong { len: text.len(), max: CELL_TEXT_MAX });
286 }
287 let id = self.with_structure(|slots, hint| {
288 let capacity = slots.len();
289 let start = hint.load(Ordering::Relaxed) as usize;
290 for offset in 0..capacity {
291 let index = (start + offset) & (capacity - 1);
292 let slot = &slots[index];
293 if slot.state.load(Ordering::Acquire) == SLOT_EMPTY {
294 let generation = slot.install(text.as_bytes());
295 hint.store(((index + 1) & (capacity - 1)) as u32, Ordering::Relaxed);
296 return Ok(TextId { index: index as u32, generation });
297 }
298 }
299 Err(Error::Full { capacity })
300 })?;
301 Ok(Text { table: self.clone(), id })
302 }
303
304 pub(crate) fn open(
305 &self,
306 id: TextId
307 ) -> Result<Text> {
308 self.slot(id)?;
309 Ok(Text { table: self.clone(), id })
310 }
311
312 pub(crate) fn release(
313 &self,
314 id: TextId
315 ) -> 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(
330 &self,
331 id: TextId
332 ) -> bool {
333 self.slot(id).is_ok()
334 }
335
336 pub(crate) fn reset_all(&self) -> Result<()> {
337 self.with_structure(|slots, hint| {
338 for slot in slots {
339 slot.state.store(SLOT_EMPTY, Ordering::Release);
340 slot.version.fetch_add(2, Ordering::SeqCst);
341 slot.wake_waiters();
342 }
343 hint.store(0, Ordering::Relaxed);
344 Ok(())
345 })
346 }
347
348 #[cfg(unix)]
349 pub(crate) fn unlink(&self) -> Result<()> {
350 match self {
351 Self::InMemory(_) => self.reset_all(),
352 Self::Shm(table) => table.region.unlink().map_err(Error::Io)
353 }
354 }
355
356 fn slots(&self) -> &[TextSlot] {
357 match self {
358 Self::InMemory(table) => &table.slots,
359 #[cfg(unix)]
360 Self::Shm(table) => table.slots()
361 }
362 }
363
364 fn slot(
365 &self,
366 id: TextId
367 ) -> Result<&TextSlot> {
368 self.slots().get(id.index as usize).filter(|slot| slot.is(id)).ok_or(Error::StaleText(id))
369 }
370
371 fn with_structure<T>(
372 &self,
373 operation: impl FnOnce(&[TextSlot], &AtomicU32) -> Result<T>
374 ) -> Result<T> {
375 match self {
376 Self::InMemory(table) => {
377 let _local = lock_unpoisoned(&table.structural_lock);
378 operation(&table.slots, &table.hint)
379 }
380 #[cfg(unix)]
381 Self::Shm(table) => table.with_structure(operation)
382 }
383 }
384}
385
386#[repr(C, align(64))]
387struct TextStateHeader {
388 #[cfg(unix)]
389 magic: u32,
390 #[cfg(not(unix))]
391 _magic: u32,
392 #[cfg(unix)]
393 version: u16,
394 #[cfg(not(unix))]
395 _version: u16,
396 header_size: u16,
397 capacity: u32,
398 slot_size: u32,
399 hint: AtomicU32,
400 _reserved: [u8; 44]
401}
402
403impl TextStateHeader {
404 fn new() -> Self {
405 Self {
406 #[cfg(unix)]
407 magic: TEXT_MAGIC,
408 #[cfg(not(unix))]
409 _magic: 0,
410 #[cfg(unix)]
411 version: TEXT_VERSION,
412 #[cfg(not(unix))]
413 _version: 0,
414 header_size: size_of::<Self>() as u16,
415 capacity: CELL_TEXT_CAPACITY as u32,
416 slot_size: size_of::<TextSlot>() as u32,
417 hint: AtomicU32::new(0),
418 _reserved: [0; 44]
419 }
420 }
421}
422
423#[repr(C, align(64))]
426struct TextSlot {
427 state: AtomicU8,
428 writer: AtomicU8,
429 len: AtomicU16,
430 generation: AtomicU32,
431 version: AtomicU32,
434 waiters: AtomicU32,
436 bytes: [AtomicU8; CELL_TEXT_MAX]
437}
438
439impl TextSlot {
440 fn empty() -> Self {
441 Self {
442 state: AtomicU8::new(SLOT_EMPTY),
443 writer: AtomicU8::new(0),
444 len: AtomicU16::new(0),
445 generation: AtomicU32::new(0),
446 version: AtomicU32::new(0),
447 waiters: AtomicU32::new(0),
448 bytes: std::array::from_fn(|_| AtomicU8::new(0))
449 }
450 }
451
452 fn is(
453 &self,
454 id: TextId
455 ) -> bool {
456 self.state.load(Ordering::Acquire) == SLOT_OCCUPIED
457 && self.generation.load(Ordering::Relaxed) == id.generation
458 }
459
460 fn install(
462 &self,
463 text: &[u8]
464 ) -> u32 {
465 let generation = self.generation.load(Ordering::Relaxed).wrapping_add(1).max(1);
466 for (index, byte) in self.bytes.iter().enumerate() {
467 byte.store(text.get(index).copied().unwrap_or(0), Ordering::Relaxed);
468 }
469 self.len.store(text.len() as u16, Ordering::Relaxed);
470 self.version.store(0, Ordering::Relaxed);
471 self.waiters.store(0, Ordering::Relaxed);
472 self.writer.store(0, Ordering::Relaxed);
473 self.generation.store(generation, Ordering::Relaxed);
474 self.state.store(SLOT_OCCUPIED, Ordering::Release);
475 generation
476 }
477
478 fn writer(&self) -> WriterGuard<'_> {
480 while self.writer.compare_exchange_weak(0, 1, Ordering::Acquire, Ordering::Relaxed).is_err()
481 {
482 std::hint::spin_loop();
483 }
484 WriterGuard(self)
485 }
486
487 fn write(
490 &self,
491 offset: usize,
492 bytes: &[u8]
493 ) {
494 debug_assert!(offset + bytes.len() <= CELL_TEXT_MAX);
495 self.version.fetch_add(1, Ordering::AcqRel);
496 for (target, source) in self.bytes[offset..offset + bytes.len()].iter().zip(bytes) {
497 target.store(*source, Ordering::Relaxed);
498 }
499 self.len.store((offset + bytes.len()) as u16, Ordering::Relaxed);
500 self.version.fetch_add(1, Ordering::SeqCst);
501 self.wake_waiters();
502 }
503
504 fn wake_waiters(&self) {
506 if self.waiters.load(Ordering::SeqCst) > 0 {
507 crate::wake_on(&self.version);
508 }
509 }
510}
511
512struct WriterGuard<'a>(&'a TextSlot);
513
514impl Drop for WriterGuard<'_> {
515 fn drop(&mut self) {
516 self.0.writer.store(0, Ordering::Release);
517 }
518}
519
520pub(crate) struct MemoryTextTable {
521 _fleet: Arc<Fleet>,
524 slots: Vec<TextSlot>,
525 hint: AtomicU32,
526 structural_lock: Mutex<()>
527}
528
529impl MemoryTextTable {
530 fn new(fleet: Arc<Fleet>) -> Self {
531 Self {
532 _fleet: fleet,
533 slots: (0..CELL_TEXT_CAPACITY).map(|_| TextSlot::empty()).collect(),
534 hint: AtomicU32::new(0),
535 structural_lock: Mutex::new(())
536 }
537 }
538}
539
540type MemoryRegistry = HashMap<usize, Weak<MemoryTextTable>>;
541
542static MEMORY_TABLES: LazyLock<Mutex<MemoryRegistry>> =
543 LazyLock::new(|| Mutex::new(HashMap::new()));
544
545fn memory_table(fleet: &Arc<Fleet>) -> Arc<MemoryTextTable> {
546 let fleet_identity = Arc::as_ptr(fleet) as usize;
547 let mut tables = lock_unpoisoned(&MEMORY_TABLES);
548 tables.retain(|_, table| table.strong_count() > 0);
549 if let Some(table) = tables.get(&fleet_identity).and_then(Weak::upgrade) {
550 return table;
551 }
552 let table = Arc::new(MemoryTextTable::new(Arc::clone(fleet)));
553 tables.insert(fleet_identity, Arc::downgrade(&table));
554 table
555}
556
557#[cfg(unix)]
558pub(crate) struct ShmTextTable {
559 region: ShmRegion,
560 structural_lock: Mutex<()>
561}
562
563#[cfg(unix)]
564impl ShmTextTable {
565 fn open_or_create(name: &str) -> Result<Self> {
566 use std::{io, ptr};
567
568 let (region, _initialization_lock) =
569 ShmRegion::open_or_create_locked(name, shm_segment_size())?;
570 if region.created() {
571 unsafe {
572 ptr::write(region.as_ptr().cast::<TextStateHeader>(), TextStateHeader::new());
573 let slots = region.as_ptr().add(size_of::<TextStateHeader>());
574 ptr::write_bytes(slots, 0, CELL_TEXT_CAPACITY * size_of::<TextSlot>());
575 }
576 } else {
577 let header = unsafe { &*region.as_ptr().cast::<TextStateHeader>() };
578 if header.magic != TEXT_MAGIC {
579 return Err(Error::Io(io::Error::new(
580 io::ErrorKind::InvalidData,
581 format!(
582 "SHM segment {name} has wrong magic 0x{:08X} (expected 0x{TEXT_MAGIC:08X})",
583 header.magic
584 )
585 )));
586 }
587 if header.version != TEXT_VERSION
588 || usize::from(header.header_size) != size_of::<TextStateHeader>()
589 || header.capacity as usize != CELL_TEXT_CAPACITY
590 || header.slot_size as usize != size_of::<TextSlot>()
591 {
592 return Err(Error::Io(io::Error::new(
593 io::ErrorKind::InvalidData,
594 format!("SHM segment {name} has an incompatible text-cell layout")
595 )));
596 }
597 }
598 Ok(Self { region, structural_lock: Mutex::new(()) })
599 }
600
601 fn header(&self) -> &TextStateHeader {
602 unsafe { &*self.region.as_ptr().cast::<TextStateHeader>() }
603 }
604
605 fn slots(&self) -> &[TextSlot] {
606 unsafe {
607 std::slice::from_raw_parts(
608 self.region.as_ptr().add(size_of::<TextStateHeader>()).cast::<TextSlot>(),
609 CELL_TEXT_CAPACITY
610 )
611 }
612 }
613
614 fn with_structure<T>(
615 &self,
616 operation: impl FnOnce(&[TextSlot], &AtomicU32) -> Result<T>
617 ) -> Result<T> {
618 let _local = lock_unpoisoned(&self.structural_lock);
619 let _process = self.region.lock_exclusive()?;
620 operation(self.slots(), &self.header().hint)
621 }
622}
623
624#[cfg(unix)]
625fn shm_segment_size() -> usize {
626 size_of::<TextStateHeader>() + CELL_TEXT_CAPACITY * size_of::<TextSlot>()
627}
628
629const _: () = assert!(CELL_TEXT_CAPACITY.is_power_of_two());
630const _: () = assert!(CELL_TEXT_MAX > 0 && CELL_TEXT_MAX <= u16::MAX as usize);
631const _: () = assert!(size_of::<TextStateHeader>() == 64);
632const _: () = assert!(size_of::<TextSlot>() == (16 + CELL_TEXT_MAX).next_multiple_of(64));