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: bool) -> std::io::Result<std::fs::File> {
143 let mut opts = std::fs::OpenOptions::new();
144 opts.read(true).write(true);
145 if create {
146 opts.create(true).truncate(true);
147 }
148 #[cfg(all(unix, not(target_os = "macos")))]
149 {
150 use std::os::unix::fs::OpenOptionsExt;
152 opts.custom_flags(libc::O_DIRECT);
153 }
154 #[cfg(windows)]
155 {
156 use std::os::windows::fs::OpenOptionsExt;
157 use windows_sys::Win32::Storage::FileSystem::{
158 FILE_FLAG_NO_BUFFERING, FILE_FLAG_WRITE_THROUGH,
159 };
160 opts.custom_flags(FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH);
164 }
165 let file = opts.open(path)?;
166 #[cfg(target_os = "macos")]
167 {
168 use std::os::unix::io::AsRawFd;
173 unsafe {
175 libc::fcntl(file.as_raw_fd(), libc::F_NOCACHE, 1);
176 }
177 }
178 Ok(file)
179}
180
181pub struct DirectFileRing {
183 data_file: std::fs::File,
184 head: Arc<SharedAtomicU64>,
185 tail: Arc<SharedAtomicU64>,
186 capacity: usize,
187 base_path: PathBuf,
188}
189
190unsafe impl Send for DirectFileRing {}
191unsafe impl Sync for DirectFileRing {}
192
193impl DirectFileRing {
194 pub fn create(
199 base_path: impl AsRef<Path>,
200 capacity: usize,
201 ) -> Result<Self, DirectFileError> {
202 assert!(capacity.is_power_of_two() && capacity >= 2,
203 "capacity must be pow2 >= 2");
204 let base = base_path.as_ref().to_path_buf();
205 let data_path = with_suffix(&base, ".directfile.data.bin");
206 let head_path = with_suffix(&base, ".directfile.head.bin");
207 let tail_path = with_suffix(&base, ".directfile.tail.bin");
208
209 let data_file = open_unbuffered(&data_path, true)?;
210 data_file.set_len((capacity * DIRECT_FILE_SLOT_SIZE) as u64)?;
211
212 let head = Arc::new(SharedAtomicU64::create(&head_path, 0)
213 .map_err(|e| std::io::Error::other(format!("{e:?}")))?);
214 let tail = Arc::new(SharedAtomicU64::create(&tail_path, 0)
215 .map_err(|e| std::io::Error::other(format!("{e:?}")))?);
216
217 Ok(Self { data_file, head, tail, capacity, base_path: base })
218 }
219
220 pub fn open(
222 base_path: impl AsRef<Path>,
223 expected_capacity: usize,
224 ) -> Result<Self, DirectFileError> {
225 let base = base_path.as_ref().to_path_buf();
226 let data_path = with_suffix(&base, ".directfile.data.bin");
227 let head_path = with_suffix(&base, ".directfile.head.bin");
228 let tail_path = with_suffix(&base, ".directfile.tail.bin");
229
230 let data_file = open_unbuffered(&data_path, false)?;
231 let actual_size = data_file.metadata()?.len();
232 let expected_size = (expected_capacity * DIRECT_FILE_SLOT_SIZE) as u64;
233 if actual_size < expected_size {
234 return Err(DirectFileError::LayoutMismatch);
235 }
236
237 let head = Arc::new(SharedAtomicU64::open(&head_path)
238 .map_err(|e| std::io::Error::other(format!("{e:?}")))?);
239 let tail = Arc::new(SharedAtomicU64::open(&tail_path)
240 .map_err(|e| std::io::Error::other(format!("{e:?}")))?);
241
242 Ok(Self {
243 data_file, head, tail,
244 capacity: expected_capacity,
245 base_path: base,
246 })
247 }
248
249 pub fn capacity(&self) -> usize { self.capacity }
251
252 pub fn head(&self) -> u64 { self.head.load(Ordering::Acquire) }
254
255 pub fn tail(&self) -> u64 { self.tail.load(Ordering::Acquire) }
257
258 pub fn try_push(&self, payload: &[u8]) -> Result<(), DirectFileError> {
262 if payload.len() > DIRECT_FILE_SLOT_SIZE {
263 return Err(DirectFileError::PayloadTooLarge);
264 }
265 let head = self.head.load(Ordering::Relaxed);
266 let tail = self.tail.load(Ordering::Acquire);
267 if head.wrapping_sub(tail) >= self.capacity as u64 {
268 return Err(DirectFileError::Full);
269 }
270 let slot_offset = ((head as usize) & (self.capacity - 1))
271 * DIRECT_FILE_SLOT_SIZE;
272 let mut buf = AlignedBuf::new(DIRECT_FILE_SLOT_SIZE)?;
273 buf.as_mut_slice()[..payload.len()].copy_from_slice(payload);
274 let n = pwrite_aligned(&self.data_file, buf.as_slice(), slot_offset)?;
275 if n != DIRECT_FILE_SLOT_SIZE {
276 return Err(DirectFileError::Io(std::io::Error::other(
277 format!("partial write: {n} != {DIRECT_FILE_SLOT_SIZE}")
278 )));
279 }
280 self.head.store(head + 1, Ordering::Release);
281 Ok(())
282 }
283
284 pub fn try_pop(&self, out: &mut [u8]) -> Result<usize, DirectFileError> {
288 let tail = self.tail.load(Ordering::Relaxed);
289 let head = self.head.load(Ordering::Acquire);
290 if tail == head {
291 return Err(DirectFileError::Empty);
292 }
293 let slot_offset = ((tail as usize) & (self.capacity - 1))
294 * DIRECT_FILE_SLOT_SIZE;
295 let mut buf = AlignedBuf::new(DIRECT_FILE_SLOT_SIZE)?;
296 let n = pread_aligned(&self.data_file, buf.as_mut_slice(), slot_offset)?;
297 if n != DIRECT_FILE_SLOT_SIZE {
298 return Err(DirectFileError::Io(std::io::Error::other(
299 format!("partial read: {n} != {DIRECT_FILE_SLOT_SIZE}")
300 )));
301 }
302 let copy_len = out.len().min(DIRECT_FILE_SLOT_SIZE);
303 out[..copy_len].copy_from_slice(&buf.as_slice()[..copy_len]);
304 self.tail.store(tail + 1, Ordering::Release);
305 Ok(copy_len)
306 }
307}
308
309impl Drop for DirectFileRing {
310 fn drop(&mut self) {
311 let data_path = with_suffix(&self.base_path, ".directfile.data.bin");
312 let head_path = with_suffix(&self.base_path, ".directfile.head.bin");
313 let tail_path = with_suffix(&self.base_path, ".directfile.tail.bin");
314 std::fs::remove_file(&data_path).ok();
315 std::fs::remove_file(&head_path).ok();
316 std::fs::remove_file(&tail_path).ok();
317 }
318}
319
320fn with_suffix(base: &Path, suffix: &str) -> PathBuf {
321 let mut s = base.as_os_str().to_owned();
322 s.push(suffix);
323 PathBuf::from(s)
324}
325
326#[cfg(unix)]
327fn pwrite_aligned(
328 file: &std::fs::File,
329 buf: &[u8],
330 offset: usize,
331) -> std::io::Result<usize> {
332 use std::os::unix::io::AsRawFd;
333 let n = unsafe {
334 libc::pwrite(
335 file.as_raw_fd(),
336 buf.as_ptr() as *const libc::c_void,
337 buf.len(),
338 offset as libc::off_t,
339 )
340 };
341 if n < 0 { Err(std::io::Error::last_os_error()) } else { Ok(n as usize) }
342}
343
344#[cfg(unix)]
345fn pread_aligned(
346 file: &std::fs::File,
347 buf: &mut [u8],
348 offset: usize,
349) -> std::io::Result<usize> {
350 use std::os::unix::io::AsRawFd;
351 let n = unsafe {
352 libc::pread(
353 file.as_raw_fd(),
354 buf.as_mut_ptr() as *mut libc::c_void,
355 buf.len(),
356 offset as libc::off_t,
357 )
358 };
359 if n < 0 { Err(std::io::Error::last_os_error()) } else { Ok(n as usize) }
360}
361
362#[cfg(windows)]
363fn pwrite_aligned(
364 file: &std::fs::File,
365 buf: &[u8],
366 offset: usize,
367) -> std::io::Result<usize> {
368 use std::os::windows::io::AsRawHandle;
369 use windows_sys::Win32::Storage::FileSystem::WriteFile;
370 use windows_sys::Win32::System::IO::OVERLAPPED;
371 let mut ov: OVERLAPPED = unsafe { std::mem::zeroed() };
372 ov.Anonymous.Anonymous.Offset = (offset as u64 & 0xFFFF_FFFF) as u32;
373 ov.Anonymous.Anonymous.OffsetHigh = ((offset as u64) >> 32) as u32;
374 let mut written: u32 = 0;
375 let ok = unsafe {
376 WriteFile(
377 file.as_raw_handle() as _,
378 buf.as_ptr(),
379 buf.len() as u32,
380 &mut written,
381 &mut ov,
382 )
383 };
384 if ok == 0 { Err(std::io::Error::last_os_error()) } else { Ok(written as usize) }
385}
386
387#[cfg(windows)]
388fn pread_aligned(
389 file: &std::fs::File,
390 buf: &mut [u8],
391 offset: usize,
392) -> std::io::Result<usize> {
393 use std::os::windows::io::AsRawHandle;
394 use windows_sys::Win32::Storage::FileSystem::ReadFile;
395 use windows_sys::Win32::System::IO::OVERLAPPED;
396 let mut ov: OVERLAPPED = unsafe { std::mem::zeroed() };
397 ov.Anonymous.Anonymous.Offset = (offset as u64 & 0xFFFF_FFFF) as u32;
398 ov.Anonymous.Anonymous.OffsetHigh = ((offset as u64) >> 32) as u32;
399 let mut read: u32 = 0;
400 let ok = unsafe {
401 ReadFile(
402 file.as_raw_handle() as _,
403 buf.as_mut_ptr(),
404 buf.len() as u32,
405 &mut read,
406 &mut ov,
407 )
408 };
409 if ok == 0 { Err(std::io::Error::last_os_error()) } else { Ok(read as usize) }
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 fn tmp(name: &str) -> PathBuf {
417 let mut p = std::env::temp_dir();
418 let pid = std::process::id();
419 let nonce = std::time::SystemTime::now()
420 .duration_since(std::time::UNIX_EPOCH)
421 .map(|d| d.as_nanos())
422 .unwrap_or(0);
423 p.push(format!("dfring_{pid}_{nonce}_{name}"));
424 p
425 }
426
427 #[test]
428 fn create_then_push_pop_round_trip() {
429 let path = tmp("rt");
430 let ring = DirectFileRing::create(&path, 4).expect("create");
431 let payload = b"hello unbuffered world";
432 ring.try_push(payload).expect("push");
433 let mut out = [0u8; DIRECT_FILE_SLOT_SIZE];
434 let n = ring.try_pop(&mut out).expect("pop");
435 assert_eq!(n, DIRECT_FILE_SLOT_SIZE);
436 assert_eq!(&out[..payload.len()], payload);
437 }
438
439 #[test]
440 fn fills_to_capacity_then_full() {
441 let path = tmp("fills");
442 let ring = DirectFileRing::create(&path, 4).expect("create");
443 for i in 0u8..4 {
444 ring.try_push(&[i; 16]).expect("push within cap");
445 }
446 assert!(matches!(
447 ring.try_push(&[0u8; 16]),
448 Err(DirectFileError::Full)
449 ));
450 }
451
452 #[test]
453 fn payload_too_large_rejected() {
454 let path = tmp("oversize");
455 let ring = DirectFileRing::create(&path, 4).expect("create");
456 let big = vec![0u8; DIRECT_FILE_SLOT_SIZE + 1];
457 assert!(matches!(
458 ring.try_push(&big),
459 Err(DirectFileError::PayloadTooLarge)
460 ));
461 }
462
463 #[test]
466 fn many_items_round_trip_in_order() {
467 let path = tmp("many");
468 let ring = DirectFileRing::create(&path, 8).expect("create");
469 let mut buf = [0u8; DIRECT_FILE_SLOT_SIZE];
470 for i in 0u64..500 {
471 ring.try_push(&i.to_le_bytes()).expect("push");
472 ring.try_pop(&mut buf).expect("pop");
473 let v = u64::from_le_bytes(buf[..8].try_into().unwrap());
474 assert_eq!(v, i, "in-order round trip at {i}");
475 }
476 }
477}