1use std::path::{Path, PathBuf};
42use std::sync::Arc;
43use std::sync::atomic::Ordering;
44
45use crate::shared_atomic::SharedAtomicU64;
46
47pub const DIRECT_FILE_SLOT_SIZE: usize = 4096;
50
51#[derive(Debug)]
53pub enum DirectFileError {
54 Io(std::io::Error),
55 LayoutMismatch,
56 Empty,
57 Full,
58 PayloadTooLarge,
59}
60
61impl std::fmt::Display for DirectFileError {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 match self {
64 Self::Io(e) => write!(f, "io: {e}"),
65 Self::LayoutMismatch => write!(f, "layout mismatch"),
66 Self::Empty => write!(f, "ring is empty"),
67 Self::Full => write!(f, "ring is full"),
68 Self::PayloadTooLarge => write!(f, "payload too large"),
69 }
70 }
71}
72
73impl std::error::Error for DirectFileError {}
74
75impl From<std::io::Error> for DirectFileError {
76 fn from(e: std::io::Error) -> Self { Self::Io(e) }
77}
78
79struct AlignedBuf {
83 ptr: *mut u8,
84 len: usize,
85}
86
87unsafe impl Send for AlignedBuf {}
88unsafe impl Sync for AlignedBuf {}
89
90impl AlignedBuf {
91 #[cfg(unix)]
92 fn new(len: usize) -> std::io::Result<Self> {
93 assert_eq!(len % DIRECT_FILE_SLOT_SIZE, 0);
94 let mut ptr: *mut libc::c_void = std::ptr::null_mut();
95 let rc = unsafe { libc::posix_memalign(&mut ptr, DIRECT_FILE_SLOT_SIZE, len) };
96 if rc != 0 {
97 return Err(std::io::Error::from_raw_os_error(rc));
98 }
99 unsafe { std::ptr::write_bytes(ptr as *mut u8, 0, len) };
100 Ok(Self { ptr: ptr as *mut u8, len })
101 }
102
103 #[cfg(windows)]
104 fn new(len: usize) -> std::io::Result<Self> {
105 use windows_sys::Win32::System::Memory::{
106 VirtualAlloc, MEM_COMMIT, MEM_RESERVE, PAGE_READWRITE,
107 };
108 assert_eq!(len % DIRECT_FILE_SLOT_SIZE, 0);
109 let ptr = unsafe {
111 VirtualAlloc(std::ptr::null(), len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)
112 };
113 if ptr.is_null() {
114 return Err(std::io::Error::last_os_error());
115 }
116 Ok(Self { ptr: ptr as *mut u8, len })
117 }
118
119 fn as_mut_slice(&mut self) -> &mut [u8] {
120 unsafe { std::slice::from_raw_parts_mut(self.ptr, self.len) }
121 }
122
123 fn as_slice(&self) -> &[u8] {
124 unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
125 }
126}
127
128impl Drop for AlignedBuf {
129 #[cfg(unix)]
130 fn drop(&mut self) {
131 unsafe { libc::free(self.ptr as *mut libc::c_void) };
132 }
133
134 #[cfg(windows)]
135 fn drop(&mut self) {
136 use windows_sys::Win32::System::Memory::{VirtualFree, MEM_RELEASE};
137 unsafe { VirtualFree(self.ptr as *mut core::ffi::c_void, 0, MEM_RELEASE) };
138 }
139}
140
141fn open_unbuffered(path: &Path, create_new: bool) -> std::io::Result<std::fs::File> {
145 let mut opts = std::fs::OpenOptions::new();
146 opts.read(true).write(true);
147 if create_new {
148 opts.create_new(true);
149 }
150 #[cfg(all(unix, not(target_os = "macos")))]
151 {
152 use std::os::unix::fs::OpenOptionsExt;
154 opts.custom_flags(libc::O_DIRECT);
155 }
156 #[cfg(windows)]
157 {
158 use std::os::windows::fs::OpenOptionsExt;
159 use windows_sys::Win32::Storage::FileSystem::{
160 FILE_FLAG_NO_BUFFERING, FILE_FLAG_WRITE_THROUGH,
161 };
162 opts.custom_flags(FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH);
166 }
167 let file = opts.open(path)?;
168 #[cfg(target_os = "macos")]
169 {
170 use std::os::unix::io::AsRawFd;
175 unsafe {
177 libc::fcntl(file.as_raw_fd(), libc::F_NOCACHE, 1);
178 }
179 }
180 Ok(file)
181}
182
183pub struct DirectFileRing {
185 data_file: std::fs::File,
186 head: Arc<SharedAtomicU64>,
187 tail: Arc<SharedAtomicU64>,
188 capacity: usize,
189 base_path: PathBuf,
190}
191
192unsafe impl Send for DirectFileRing {}
193unsafe impl Sync for DirectFileRing {}
194
195impl DirectFileRing {
196 pub fn create(
206 base_path: impl AsRef<Path>,
207 capacity: usize,
208 ) -> Result<Self, DirectFileError> {
209 assert!(capacity.is_power_of_two() && capacity >= 2,
210 "capacity must be pow2 >= 2");
211 let base = base_path.as_ref().to_path_buf();
212 let data_path = with_suffix(&base, ".directfile.data.bin");
213 let head_path = with_suffix(&base, ".directfile.head.bin");
214 let tail_path = with_suffix(&base, ".directfile.tail.bin");
215
216 let expected_size = (capacity * DIRECT_FILE_SLOT_SIZE) as u64;
217 let data_file = match open_unbuffered(&data_path, true) {
218 Ok(f) => {
219 f.set_len(expected_size)?;
220 f
221 }
222 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
223 let f = open_unbuffered(&data_path, false)?;
224 let deadline = std::time::Instant::now() + crate::mmf_attach::INIT_WAIT;
227 loop {
228 let len = f.metadata()?.len();
229 if len == expected_size {
230 break;
231 }
232 if len > expected_size {
233 return Err(DirectFileError::LayoutMismatch);
234 }
235 if std::time::Instant::now() >= deadline {
236 return Err(std::io::Error::new(
237 std::io::ErrorKind::TimedOut,
238 "the ring's creator did not finish initializing it",
239 ).into());
240 }
241 std::thread::yield_now();
242 }
243 f
244 }
245 Err(e) => return Err(e.into()),
246 };
247
248 let head = Arc::new(SharedAtomicU64::create(&head_path, 0)
249 .map_err(|e| std::io::Error::other(format!("{e:?}")))?);
250 let tail = Arc::new(SharedAtomicU64::create(&tail_path, 0)
251 .map_err(|e| std::io::Error::other(format!("{e:?}")))?);
252
253 Ok(Self { data_file, head, tail, capacity, base_path: base })
254 }
255
256 pub fn reset(
260 base_path: impl AsRef<Path>,
261 capacity: usize,
262 ) -> Result<Self, DirectFileError> {
263 assert!(capacity.is_power_of_two() && capacity >= 2,
264 "capacity must be pow2 >= 2");
265 let base = base_path.as_ref().to_path_buf();
266 let data_path = with_suffix(&base, ".directfile.data.bin");
267 let head_path = with_suffix(&base, ".directfile.head.bin");
268 let tail_path = with_suffix(&base, ".directfile.tail.bin");
269
270 let data_file = match open_unbuffered(&data_path, true) {
271 Ok(f) => f,
272 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
273 open_unbuffered(&data_path, false)?
274 }
275 Err(e) => return Err(e.into()),
276 };
277 data_file.set_len(0)?;
278 data_file.set_len((capacity * DIRECT_FILE_SLOT_SIZE) as u64)?;
279
280 let head = Arc::new(SharedAtomicU64::reset(&head_path, 0)
281 .map_err(|e| std::io::Error::other(format!("{e:?}")))?);
282 let tail = Arc::new(SharedAtomicU64::reset(&tail_path, 0)
283 .map_err(|e| std::io::Error::other(format!("{e:?}")))?);
284
285 Ok(Self { data_file, head, tail, capacity, base_path: base })
286 }
287
288 pub fn open(
290 base_path: impl AsRef<Path>,
291 expected_capacity: usize,
292 ) -> Result<Self, DirectFileError> {
293 let base = base_path.as_ref().to_path_buf();
294 let data_path = with_suffix(&base, ".directfile.data.bin");
295 let head_path = with_suffix(&base, ".directfile.head.bin");
296 let tail_path = with_suffix(&base, ".directfile.tail.bin");
297
298 let data_file = open_unbuffered(&data_path, false)?;
299 let actual_size = data_file.metadata()?.len();
300 let expected_size = (expected_capacity * DIRECT_FILE_SLOT_SIZE) as u64;
301 if actual_size != expected_size {
305 return Err(DirectFileError::LayoutMismatch);
306 }
307
308 let head = Arc::new(SharedAtomicU64::open(&head_path)
309 .map_err(|e| std::io::Error::other(format!("{e:?}")))?);
310 let tail = Arc::new(SharedAtomicU64::open(&tail_path)
311 .map_err(|e| std::io::Error::other(format!("{e:?}")))?);
312
313 Ok(Self {
314 data_file, head, tail,
315 capacity: expected_capacity,
316 base_path: base,
317 })
318 }
319
320 pub fn capacity(&self) -> usize { self.capacity }
322
323 pub fn head(&self) -> u64 { self.head.load(Ordering::Acquire) }
325
326 pub fn tail(&self) -> u64 { self.tail.load(Ordering::Acquire) }
328
329 pub fn try_push(&self, payload: &[u8]) -> Result<(), DirectFileError> {
333 if payload.len() > DIRECT_FILE_SLOT_SIZE {
334 return Err(DirectFileError::PayloadTooLarge);
335 }
336 let head = self.head.load(Ordering::Relaxed);
337 let tail = self.tail.load(Ordering::Acquire);
338 if head.wrapping_sub(tail) >= self.capacity as u64 {
339 return Err(DirectFileError::Full);
340 }
341 let slot_offset = ((head as usize) & (self.capacity - 1))
342 * DIRECT_FILE_SLOT_SIZE;
343 let mut buf = AlignedBuf::new(DIRECT_FILE_SLOT_SIZE)?;
344 buf.as_mut_slice()[..payload.len()].copy_from_slice(payload);
345 let n = pwrite_aligned(&self.data_file, buf.as_slice(), slot_offset)?;
346 if n != DIRECT_FILE_SLOT_SIZE {
347 return Err(DirectFileError::Io(std::io::Error::other(
348 format!("partial write: {n} != {DIRECT_FILE_SLOT_SIZE}")
349 )));
350 }
351 self.head.store(head + 1, Ordering::Release);
352 Ok(())
353 }
354
355 pub fn try_pop(&self, out: &mut [u8]) -> Result<usize, DirectFileError> {
359 let tail = self.tail.load(Ordering::Relaxed);
360 let head = self.head.load(Ordering::Acquire);
361 if tail == head {
362 return Err(DirectFileError::Empty);
363 }
364 let slot_offset = ((tail as usize) & (self.capacity - 1))
365 * DIRECT_FILE_SLOT_SIZE;
366 let mut buf = AlignedBuf::new(DIRECT_FILE_SLOT_SIZE)?;
367 let n = pread_aligned(&self.data_file, buf.as_mut_slice(), slot_offset)?;
368 if n != DIRECT_FILE_SLOT_SIZE {
369 return Err(DirectFileError::Io(std::io::Error::other(
370 format!("partial read: {n} != {DIRECT_FILE_SLOT_SIZE}")
371 )));
372 }
373 let copy_len = out.len().min(DIRECT_FILE_SLOT_SIZE);
374 out[..copy_len].copy_from_slice(&buf.as_slice()[..copy_len]);
375 self.tail.store(tail + 1, Ordering::Release);
376 Ok(copy_len)
377 }
378}
379
380impl Drop for DirectFileRing {
381 fn drop(&mut self) {
382 let data_path = with_suffix(&self.base_path, ".directfile.data.bin");
383 let head_path = with_suffix(&self.base_path, ".directfile.head.bin");
384 let tail_path = with_suffix(&self.base_path, ".directfile.tail.bin");
385 std::fs::remove_file(&data_path).ok();
386 std::fs::remove_file(&head_path).ok();
387 std::fs::remove_file(&tail_path).ok();
388 }
389}
390
391fn with_suffix(base: &Path, suffix: &str) -> PathBuf {
392 let mut s = base.as_os_str().to_owned();
393 s.push(suffix);
394 PathBuf::from(s)
395}
396
397#[cfg(unix)]
398fn pwrite_aligned(
399 file: &std::fs::File,
400 buf: &[u8],
401 offset: usize,
402) -> std::io::Result<usize> {
403 use std::os::unix::io::AsRawFd;
404 let n = unsafe {
405 libc::pwrite(
406 file.as_raw_fd(),
407 buf.as_ptr() as *const libc::c_void,
408 buf.len(),
409 offset as libc::off_t,
410 )
411 };
412 if n < 0 { Err(std::io::Error::last_os_error()) } else { Ok(n as usize) }
413}
414
415#[cfg(unix)]
416fn pread_aligned(
417 file: &std::fs::File,
418 buf: &mut [u8],
419 offset: usize,
420) -> std::io::Result<usize> {
421 use std::os::unix::io::AsRawFd;
422 let n = unsafe {
423 libc::pread(
424 file.as_raw_fd(),
425 buf.as_mut_ptr() as *mut libc::c_void,
426 buf.len(),
427 offset as libc::off_t,
428 )
429 };
430 if n < 0 { Err(std::io::Error::last_os_error()) } else { Ok(n as usize) }
431}
432
433#[cfg(windows)]
434fn pwrite_aligned(
435 file: &std::fs::File,
436 buf: &[u8],
437 offset: usize,
438) -> std::io::Result<usize> {
439 use std::os::windows::io::AsRawHandle;
440 use windows_sys::Win32::Storage::FileSystem::WriteFile;
441 use windows_sys::Win32::System::IO::OVERLAPPED;
442 let mut ov: OVERLAPPED = unsafe { std::mem::zeroed() };
443 ov.Anonymous.Anonymous.Offset = (offset as u64 & 0xFFFF_FFFF) as u32;
444 ov.Anonymous.Anonymous.OffsetHigh = ((offset as u64) >> 32) as u32;
445 let mut written: u32 = 0;
446 let ok = unsafe {
447 WriteFile(
448 file.as_raw_handle() as _,
449 buf.as_ptr(),
450 buf.len() as u32,
451 &mut written,
452 &mut ov,
453 )
454 };
455 if ok == 0 { Err(std::io::Error::last_os_error()) } else { Ok(written as usize) }
456}
457
458#[cfg(windows)]
459fn pread_aligned(
460 file: &std::fs::File,
461 buf: &mut [u8],
462 offset: usize,
463) -> std::io::Result<usize> {
464 use std::os::windows::io::AsRawHandle;
465 use windows_sys::Win32::Storage::FileSystem::ReadFile;
466 use windows_sys::Win32::System::IO::OVERLAPPED;
467 let mut ov: OVERLAPPED = unsafe { std::mem::zeroed() };
468 ov.Anonymous.Anonymous.Offset = (offset as u64 & 0xFFFF_FFFF) as u32;
469 ov.Anonymous.Anonymous.OffsetHigh = ((offset as u64) >> 32) as u32;
470 let mut read: u32 = 0;
471 let ok = unsafe {
472 ReadFile(
473 file.as_raw_handle() as _,
474 buf.as_mut_ptr(),
475 buf.len() as u32,
476 &mut read,
477 &mut ov,
478 )
479 };
480 if ok == 0 { Err(std::io::Error::last_os_error()) } else { Ok(read as usize) }
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486
487 fn tmp(name: &str) -> PathBuf {
488 let mut p = std::env::temp_dir();
489 let pid = std::process::id();
490 let nonce = std::time::SystemTime::now()
491 .duration_since(std::time::UNIX_EPOCH)
492 .map(|d| d.as_nanos())
493 .unwrap_or(0);
494 p.push(format!("dfring_{pid}_{nonce}_{name}"));
495 p
496 }
497
498 #[test]
499 fn create_then_push_pop_round_trip() {
500 let path = tmp("rt");
501 let ring = DirectFileRing::create(&path, 4).expect("create");
502 let payload = b"hello unbuffered world";
503 ring.try_push(payload).expect("push");
504 let mut out = [0u8; DIRECT_FILE_SLOT_SIZE];
505 let n = ring.try_pop(&mut out).expect("pop");
506 assert_eq!(n, DIRECT_FILE_SLOT_SIZE);
507 assert_eq!(&out[..payload.len()], payload);
508 }
509
510 #[test]
513 fn second_create_attaches_and_keeps_slots() {
514 let path = tmp("attach");
515 let ring = DirectFileRing::create(&path, 4).expect("create");
516 let payload = b"slot survives attach";
517 ring.try_push(payload).expect("push");
518
519 let ring2 = DirectFileRing::create(&path, 4).expect("second create");
520 let mut out = [0u8; DIRECT_FILE_SLOT_SIZE];
521 let n = ring2.try_pop(&mut out).expect("pop after attach");
522 assert_eq!(n, DIRECT_FILE_SLOT_SIZE);
523 assert_eq!(&out[..payload.len()], payload, "attach lost a queued slot");
524 assert!(matches!(
525 DirectFileRing::create(&path, 2),
526 Err(DirectFileError::LayoutMismatch),
527 ));
528
529 drop(ring);
530 drop(ring2);
531 let fresh = DirectFileRing::reset(&path, 4).expect("reset");
532 assert!(matches!(
533 fresh.try_pop(&mut out),
534 Err(DirectFileError::Empty),
535 ), "reset kept a queued slot");
536 }
537
538 #[test]
539 fn fills_to_capacity_then_full() {
540 let path = tmp("fills");
541 let ring = DirectFileRing::create(&path, 4).expect("create");
542 for i in 0u8..4 {
543 ring.try_push(&[i; 16]).expect("push within cap");
544 }
545 assert!(matches!(
546 ring.try_push(&[0u8; 16]),
547 Err(DirectFileError::Full)
548 ));
549 }
550
551 #[test]
552 fn payload_too_large_rejected() {
553 let path = tmp("oversize");
554 let ring = DirectFileRing::create(&path, 4).expect("create");
555 let big = vec![0u8; DIRECT_FILE_SLOT_SIZE + 1];
556 assert!(matches!(
557 ring.try_push(&big),
558 Err(DirectFileError::PayloadTooLarge)
559 ));
560 }
561
562 #[test]
565 fn many_items_round_trip_in_order() {
566 let path = tmp("many");
567 let ring = DirectFileRing::create(&path, 8).expect("create");
568 let mut buf = [0u8; DIRECT_FILE_SLOT_SIZE];
569 for i in 0u64..500 {
570 ring.try_push(&i.to_le_bytes()).expect("push");
571 ring.try_pop(&mut buf).expect("pop");
572 let v = u64::from_le_bytes(buf[..8].try_into().unwrap());
573 assert_eq!(v, i, "in-order round trip at {i}");
574 }
575 }
576}