1use std::fs::{File, OpenOptions};
46use std::marker::PhantomData;
47use std::mem::size_of;
48use std::path::Path;
49use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
50
51use memmap2::{MmapMut, MmapOptions};
52
53pub const STACK_MAGIC: u32 = 0x4150_5354;
54pub const STACK_NIL: u32 = u32::MAX;
55
56#[repr(C, align(64))]
57pub struct StackHeader {
58 pub magic: u32,
59 pub capacity: u32,
60 pub slot_size: u32,
61 _pad1: u32,
62 pub head: AtomicU64, pub free_head: AtomicU64, pub bump_next: AtomicU32,
65 _pad2: [u8; 28],
66}
67
68const _: () = {
69 assert!(size_of::<StackHeader>() == 64);
70};
71
72pub fn stack_file_size(capacity: usize, slot_size: usize) -> usize {
73 size_of::<StackHeader>()
74 + capacity * size_of::<AtomicU32>()
75 + capacity * slot_size
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum StackError {
80 Full,
81 LayoutMismatch,
82 IoError(std::io::ErrorKind),
83}
84
85impl From<std::io::Error> for StackError {
86 fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
87}
88
89#[inline]
90fn pack(counter: u32, index: u32) -> u64 {
91 ((counter as u64) << 32) | (index as u64)
92}
93#[inline]
94fn unpack(v: u64) -> (u32, u32) {
95 ((v >> 32) as u32, v as u32)
96}
97
98pub struct SharedTreiberStack<T: Copy + 'static> {
99 _file: File,
100 mmap: MmapMut,
101 capacity: usize,
102 next_offset: usize,
103 slots_offset: usize,
104 _phantom: PhantomData<T>,
105 header_sidecar: subetha_core::HandshakeHeader,
106 ring_sidecar: Box<subetha_core::ObservationRing>,
107}
108
109unsafe impl<T: Copy + Send + 'static> Send for SharedTreiberStack<T> {}
110unsafe impl<T: Copy + Sync + 'static> Sync for SharedTreiberStack<T> {}
111
112impl<T: Copy + Send + Sync + 'static> subetha_sidecar::AdaptiveInstance for SharedTreiberStack<T> {
113 fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
114 fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
115 fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
116 Box::new(subetha_sidecar::NoMigrationPolicy)
117 }
118}
119
120impl<T: Copy + 'static> SharedTreiberStack<T> {
121 pub fn create(
122 path: impl AsRef<Path>, capacity: usize,
123 ) -> Result<Self, StackError> {
124 assert!(capacity >= 1);
125 assert!(capacity < STACK_NIL as usize, "capacity must be < u32::MAX");
126 let slot_size = size_of::<T>();
127 let total = stack_file_size(capacity, slot_size);
128 let file = OpenOptions::new()
129 .read(true).write(true).create(true).truncate(true)
130 .open(path.as_ref())?;
131 file.set_len(total as u64)?;
132 let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
133 let hdr = mmap.as_mut_ptr() as *mut StackHeader;
134 unsafe {
135 std::ptr::write_bytes(hdr as *mut u8, 0, size_of::<StackHeader>());
136 (*hdr).magic = STACK_MAGIC;
137 (*hdr).capacity = capacity as u32;
138 (*hdr).slot_size = slot_size as u32;
139 (*hdr).head.store(pack(0, STACK_NIL), Ordering::Release);
140 (*hdr).free_head.store(pack(0, STACK_NIL), Ordering::Release);
141 (*hdr).bump_next.store(0, Ordering::Release);
142 }
143 let next_offset = size_of::<StackHeader>();
144 let slots_offset = next_offset + capacity * size_of::<AtomicU32>();
145 Ok(Self {
146 _file: file, mmap, capacity, next_offset, slots_offset,
147 _phantom: PhantomData,
148 header_sidecar: subetha_core::HandshakeHeader::new(),
149 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
150 })
151 }
152
153 pub fn open(
154 path: impl AsRef<Path>, expected_capacity: usize,
155 ) -> Result<Self, StackError> {
156 let slot_size = size_of::<T>();
157 let total = stack_file_size(expected_capacity, slot_size);
158 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
159 if file.metadata()?.len() < total as u64 {
160 return Err(StackError::LayoutMismatch);
161 }
162 let mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
163 let hdr = unsafe { &*(mmap.as_ptr() as *const StackHeader) };
164 if hdr.magic != STACK_MAGIC
165 || hdr.capacity != expected_capacity as u32
166 || hdr.slot_size != slot_size as u32
167 {
168 return Err(StackError::LayoutMismatch);
169 }
170 let next_offset = size_of::<StackHeader>();
171 let slots_offset = next_offset + expected_capacity * size_of::<AtomicU32>();
172 Ok(Self {
173 _file: file, mmap, capacity: expected_capacity,
174 next_offset, slots_offset,
175 _phantom: PhantomData,
176 header_sidecar: subetha_core::HandshakeHeader::new(),
177 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
178 })
179 }
180
181 #[inline]
182 pub fn capacity(&self) -> usize { self.capacity }
183
184 fn header(&self) -> &StackHeader {
185 unsafe { &*(self.mmap.as_ptr() as *const StackHeader) }
186 }
187
188 fn next_link(&self, idx: usize) -> &AtomicU32 {
189 let base = unsafe { self.mmap.as_ptr().add(self.next_offset) };
190 unsafe { &*(base.add(idx * size_of::<AtomicU32>()) as *const AtomicU32) }
191 }
192
193 fn slot_ptr(&self, idx: usize) -> *mut T {
194 let base = unsafe { self.mmap.as_ptr().add(self.slots_offset) };
195 unsafe { base.add(idx * size_of::<T>()) as *mut T }
196 }
197
198 fn acquire_slot(&self) -> Result<u32, StackError> {
201 loop {
203 let head = self.header().free_head.load(Ordering::Acquire);
204 let (counter, idx) = unpack(head);
205 if idx == STACK_NIL { break; }
206 let next_idx = self.next_link(idx as usize).load(Ordering::Acquire);
207 let new_head = pack(counter.wrapping_add(1), next_idx);
208 if self.header().free_head.compare_exchange(
209 head, new_head, Ordering::AcqRel, Ordering::Acquire,
210 ).is_ok() {
211 return Ok(idx);
212 }
213 }
214 let idx = self.header().bump_next.fetch_add(1, Ordering::AcqRel);
216 if (idx as usize) >= self.capacity {
217 self.header().bump_next.fetch_sub(1, Ordering::AcqRel);
218 return Err(StackError::Full);
219 }
220 Ok(idx)
221 }
222
223 fn release_slot(&self, idx: u32) {
225 loop {
226 let head = self.header().free_head.load(Ordering::Acquire);
227 let (counter, old_top) = unpack(head);
228 self.next_link(idx as usize).store(old_top, Ordering::Release);
229 let new_head = pack(counter.wrapping_add(1), idx);
230 if self.header().free_head.compare_exchange(
231 head, new_head, Ordering::AcqRel, Ordering::Acquire,
232 ).is_ok() {
233 return;
234 }
235 }
236 }
237
238 pub fn push(&self, value: T) -> Result<(), StackError> {
240 let idx = match self.acquire_slot() {
241 Ok(i) => i,
242 Err(e) => {
243 self.ring_sidecar
244 .push_op(crate::sidecar_ops::ordered::OP_INSERT, 1); return Err(e);
246 }
247 };
248 unsafe { std::ptr::write(self.slot_ptr(idx as usize), value); }
249 loop {
252 let head = self.header().head.load(Ordering::Acquire);
253 let (counter, old_top) = unpack(head);
254 self.next_link(idx as usize).store(old_top, Ordering::Release);
255 let new_head = pack(counter.wrapping_add(1), idx);
256 if self.header().head.compare_exchange(
257 head, new_head, Ordering::AcqRel, Ordering::Acquire,
258 ).is_ok() {
259 self.ring_sidecar
260 .push_op(crate::sidecar_ops::ordered::OP_INSERT, 0);
261 return Ok(());
262 }
263 }
264 }
265
266 pub fn pop(&self) -> Option<T> {
268 loop {
269 let head = self.header().head.load(Ordering::Acquire);
270 let (counter, top) = unpack(head);
271 if top == STACK_NIL {
272 self.ring_sidecar
273 .push_op(crate::sidecar_ops::ordered::OP_POP, 2); return None;
275 }
276 let next_top = self.next_link(top as usize).load(Ordering::Acquire);
277 let new_head = pack(counter.wrapping_add(1), next_top);
278 if self.header().head.compare_exchange(
279 head, new_head, Ordering::AcqRel, Ordering::Acquire,
280 ).is_ok() {
281 let value = unsafe { std::ptr::read(self.slot_ptr(top as usize)) };
282 self.release_slot(top);
283 self.ring_sidecar
284 .push_op(crate::sidecar_ops::ordered::OP_POP, 0);
285 return Some(value);
286 }
287 }
288 }
289
290 pub fn peek(&self) -> Option<T> {
292 let head = self.header().head.load(Ordering::Acquire);
293 let (_, top) = unpack(head);
294 if top == STACK_NIL {
295 self.ring_sidecar
296 .push_op(crate::sidecar_ops::ordered::OP_GET, 2); return None;
298 }
299 let v = unsafe { std::ptr::read(self.slot_ptr(top as usize)) };
300 self.ring_sidecar
301 .push_op(crate::sidecar_ops::ordered::OP_GET, 0);
302 Some(v)
303 }
304
305 pub fn is_empty(&self) -> bool {
307 let head = self.header().head.load(Ordering::Acquire);
308 unpack(head).1 == STACK_NIL
309 }
310
311 pub fn approx_len(&self) -> usize {
314 let head = self.header().head.load(Ordering::Acquire);
315 let (_, mut idx) = unpack(head);
316 let mut count = 0usize;
317 let mut visited = 0;
318 while idx != STACK_NIL && visited < self.capacity {
319 count += 1;
320 visited += 1;
321 idx = self.next_link(idx as usize).load(Ordering::Acquire);
322 }
323 count
324 }
325
326 pub fn flush(&self) -> Result<(), StackError> {
327 self.mmap.flush()?;
328 Ok(())
329 }
330 pub fn flush_async(&self) -> Result<(), StackError> {
331 self.mmap.flush_async()?;
332 Ok(())
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339 use std::sync::Arc;
340 use std::thread;
341
342 fn tmp(name: &str) -> std::path::PathBuf {
343 let mut p = std::env::temp_dir();
344 let pid = std::process::id();
345 p.push(format!("subetha-stack-{name}-{pid}.bin"));
346 p
347 }
348
349 #[test]
350 fn create_initial_state_is_empty() {
351 let p = tmp("init");
352 let s: SharedTreiberStack<u64> = SharedTreiberStack::create(&p, 16).unwrap();
353 assert!(s.is_empty());
354 assert_eq!(s.pop(), None);
355 assert_eq!(s.peek(), None);
356 std::fs::remove_file(&p).ok();
357 }
358
359 #[test]
360 fn push_pop_lifo_order() {
361 let p = tmp("lifo");
362 let s: SharedTreiberStack<u32> = SharedTreiberStack::create(&p, 16).unwrap();
363 s.push(10).unwrap();
364 s.push(20).unwrap();
365 s.push(30).unwrap();
366 assert_eq!(s.pop(), Some(30));
367 assert_eq!(s.pop(), Some(20));
368 assert_eq!(s.pop(), Some(10));
369 assert_eq!(s.pop(), None);
370 std::fs::remove_file(&p).ok();
371 }
372
373 #[test]
374 fn peek_does_not_remove() {
375 let p = tmp("peek");
376 let s: SharedTreiberStack<u32> = SharedTreiberStack::create(&p, 8).unwrap();
377 s.push(42).unwrap();
378 assert_eq!(s.peek(), Some(42));
379 assert_eq!(s.peek(), Some(42));
380 assert_eq!(s.pop(), Some(42));
381 assert_eq!(s.peek(), None);
382 std::fs::remove_file(&p).ok();
383 }
384
385 #[test]
386 fn full_capacity_returns_error() {
387 let p = tmp("full");
388 let s: SharedTreiberStack<u32> = SharedTreiberStack::create(&p, 4).unwrap();
389 for i in 0..4 { s.push(i).unwrap(); }
390 assert_eq!(s.push(99).err(), Some(StackError::Full));
391 s.pop();
393 s.push(99).unwrap();
394 std::fs::remove_file(&p).ok();
395 }
396
397 #[test]
398 fn free_list_reuse_after_pop() {
399 let p = tmp("reuse");
400 let s: SharedTreiberStack<u32> = SharedTreiberStack::create(&p, 4).unwrap();
401 for i in 0..4 { s.push(i).unwrap(); }
402 for _ in 0..4 { s.pop(); }
403 for i in 100..104 { s.push(i).unwrap(); }
405 assert_eq!(s.pop(), Some(103));
406 assert_eq!(s.pop(), Some(102));
407 std::fs::remove_file(&p).ok();
408 }
409
410 #[test]
411 fn approx_len_tracks_size() {
412 let p = tmp("len");
413 let s: SharedTreiberStack<u32> = SharedTreiberStack::create(&p, 16).unwrap();
414 assert_eq!(s.approx_len(), 0);
415 s.push(1).unwrap();
416 s.push(2).unwrap();
417 s.push(3).unwrap();
418 assert_eq!(s.approx_len(), 3);
419 s.pop();
420 assert_eq!(s.approx_len(), 2);
421 std::fs::remove_file(&p).ok();
422 }
423
424 #[test]
425 fn cross_handle_visibility() {
426 let p = tmp("cross-handle");
427 let w: SharedTreiberStack<u32> = SharedTreiberStack::create(&p, 8).unwrap();
428 let r: SharedTreiberStack<u32> = SharedTreiberStack::open(&p, 8).unwrap();
429 w.push(42).unwrap();
430 w.push(7).unwrap();
431 assert_eq!(r.peek(), Some(7));
432 assert_eq!(r.pop(), Some(7));
433 assert_eq!(w.pop(), Some(42));
434 assert!(r.is_empty());
435 std::fs::remove_file(&p).ok();
436 }
437
438 #[test]
439 fn struct_payload_round_trip() {
440 #[derive(Clone, Copy, Debug, PartialEq)]
441 #[repr(C)]
442 struct Frame { pc: u64, sp: u64 }
443 let p = tmp("struct");
444 let s: SharedTreiberStack<Frame> = SharedTreiberStack::create(&p, 8).unwrap();
445 s.push(Frame { pc: 0x1000, sp: 0xFF00 }).unwrap();
446 s.push(Frame { pc: 0x2000, sp: 0xFE00 }).unwrap();
447 assert_eq!(s.pop(), Some(Frame { pc: 0x2000, sp: 0xFE00 }));
448 assert_eq!(s.pop(), Some(Frame { pc: 0x1000, sp: 0xFF00 }));
449 std::fs::remove_file(&p).ok();
450 }
451
452 #[test]
453 fn concurrent_pushers_all_succeed() {
454 let p = tmp("concurrent-push");
455 let s: Arc<SharedTreiberStack<u32>>
456 = Arc::new(SharedTreiberStack::create(&p, 1024).unwrap());
457 let n_threads = 4;
458 let per_thread = 100;
459 let mut handles = vec![];
460 for t in 0..n_threads as u32 {
461 let s = s.clone();
462 handles.push(thread::spawn(move || {
463 for i in 0..per_thread as u32 {
464 s.push(t * 1000 + i).unwrap();
465 }
466 }));
467 }
468 for h in handles { h.join().unwrap(); }
469 assert_eq!(s.approx_len(), n_threads * per_thread);
470 let mut all = Vec::new();
472 while let Some(v) = s.pop() { all.push(v); }
473 all.sort();
474 let mut expected: Vec<u32> = (0..n_threads as u32)
476 .flat_map(|t| (0..per_thread as u32).map(move |i| t * 1000 + i))
477 .collect();
478 expected.sort();
479 assert_eq!(all, expected);
480 std::fs::remove_file(&p).ok();
481 }
482
483 #[test]
484 fn concurrent_push_pop_no_corruption() {
485 let p = tmp("concurrent-pp");
488 let s: Arc<SharedTreiberStack<u32>>
489 = Arc::new(SharedTreiberStack::create(&p, 1024).unwrap());
490 for i in 0..500u32 { s.push(i).unwrap(); }
492 let mut handles = vec![];
494 for _ in 0..4 {
495 let s = s.clone();
496 handles.push(thread::spawn(move || {
497 let mut got = Vec::new();
498 while let Some(v) = s.pop() { got.push(v); }
499 got
500 }));
501 }
502 let mut total: Vec<u32> = handles.into_iter()
503 .flat_map(|h| h.join().unwrap()).collect();
504 total.sort();
505 let expected: Vec<u32> = (0..500u32).collect();
506 assert_eq!(total, expected, "no items should be lost or duplicated");
507 std::fs::remove_file(&p).ok();
508 }
509
510 #[test]
511 fn disk_persistence_survives_reopen() {
512 let p = tmp("disk");
513 {
514 let s: SharedTreiberStack<u32> = SharedTreiberStack::create(&p, 8).unwrap();
515 s.push(1).unwrap();
516 s.push(2).unwrap();
517 s.push(3).unwrap();
518 s.flush().unwrap();
519 }
520 let s2: SharedTreiberStack<u32> = SharedTreiberStack::open(&p, 8).unwrap();
521 assert_eq!(s2.pop(), Some(3));
522 assert_eq!(s2.pop(), Some(2));
523 assert_eq!(s2.pop(), Some(1));
524 std::fs::remove_file(&p).ok();
525 }
526}