1use std::fs::File;
9use std::io;
10
11pub fn platform_read_at(f: &File, buf: &mut [u8], off: u64) -> io::Result<usize> {
14 platform_read_at_impl(f, buf, off)
15}
16
17#[cfg(unix)]
18fn platform_read_at_impl(f: &File, buf: &mut [u8], off: u64) -> io::Result<usize> {
19 use std::os::unix::fs::FileExt;
20 f.read_at(buf, off)
21}
22
23#[cfg(windows)]
24fn platform_read_at_impl(f: &File, buf: &mut [u8], off: u64) -> io::Result<usize> {
25 use std::os::windows::fs::FileExt;
26 f.seek_read(buf, off)
27}
28
29#[cfg(not(any(unix, windows)))]
30fn platform_read_at_impl(f: &File, buf: &mut [u8], off: u64) -> io::Result<usize> {
31 let mut f2 = f.try_clone()?;
32 use std::io::{Read, Seek, SeekFrom};
33 f2.seek(SeekFrom::Start(off))?;
34 f2.read(buf)
35}
36
37pub fn platform_read_exact_at(f: &File, buf: &mut [u8], off: u64) -> io::Result<()> {
39 let mut done = 0usize;
40 while done < buf.len() {
41 let n = platform_read_at(f, &mut buf[done..], off + done as u64)?;
42 if n == 0 {
43 return Err(io::Error::new(
44 io::ErrorKind::UnexpectedEof,
45 "read_exact_at: eof inside buffer",
46 ));
47 }
48 done += n;
49 }
50 Ok(())
51}
52
53pub struct PreadvBackend;
55
56impl IoBackend for PreadvBackend {
57 fn read_exact_at(&self, f: &File, buf: &mut [u8], off: u64) -> io::Result<()> {
58 platform_read_exact_at(f, buf, off)
59 }
60
61 fn read_at(&self, f: &File, buf: &mut [u8], off: u64) -> io::Result<usize> {
62 platform_read_at(f, buf, off)
63 }
64}
65
66pub trait IoBackend: Send + Sync + 'static {
67 fn read_exact_at(&self, f: &File, buf: &mut [u8], off: u64) -> io::Result<()>;
68 fn read_at(&self, f: &File, buf: &mut [u8], off: u64) -> io::Result<usize>;
69
70 fn read_many(&self, f: &File, reqs: &mut [(u64, &mut [u8])]) -> io::Result<()> {
73 for (off, buf) in reqs.iter_mut() {
74 self.read_exact_at(f, buf, *off)?;
75 }
76 Ok(())
77 }
78}
79
80pub fn default_backend() -> Box<dyn IoBackend> {
84 Box::new(PreadvBackend)
85}
86
87#[cfg(target_os = "linux")]
94pub struct UringBackend;
95
96#[cfg(target_os = "linux")]
99pub struct UringBatchBackend;
100
101#[cfg(target_os = "linux")]
102impl IoBackend for UringBatchBackend {
103 fn read_exact_at(&self, f: &File, buf: &mut [u8], off: u64) -> io::Result<()> {
104 platform_read_exact_at(f, buf, off)
105 }
106 fn read_at(&self, f: &File, buf: &mut [u8], off: u64) -> io::Result<usize> {
107 platform_read_at(f, buf, off)
108 }
109 fn read_many(&self, f: &File, reqs: &mut [(u64, &mut [u8])]) -> io::Result<()> {
110 let _ = uring_batch_read(f, reqs)?;
111 Ok(())
112 }
113}
114
115#[cfg(target_os = "linux")]
116fn uring_batch_read(f: &File, reqs: &mut [(u64, &mut [u8])]) -> io::Result<usize> {
117 const DEPTH: u32 = 256;
118 use std::os::fd::AsRawFd;
119 URING.with(|sl| {
120 let mut r = sl.borrow_mut();
121 if r.is_none() {
122 let ring = io_uring::IoUring::new(DEPTH)
123 .map_err(|e| io::Error::other(format!("IoUring::new: {e}")))?;
124 *r = Some(ring);
125 }
126 let ring = r.as_mut().unwrap();
127 let fd = f.as_raw_fd();
128 let mut total = 0usize;
129 for chunk in reqs.chunks_mut(DEPTH as usize) {
130 let expect: Vec<(usize, usize)> = chunk
131 .iter()
132 .enumerate()
133 .map(|(i, (_, b))| (i, b.len()))
134 .collect();
135 for (i, (off, buf)) in chunk.iter_mut().enumerate() {
136 let sqe = io_uring::opcode::Read::new(
137 io_uring::types::Fd(fd),
138 buf.as_mut_ptr(),
139 buf.len() as u32,
140 )
141 .offset(*off);
142 let sqe = sqe.build().user_data(i as u64);
143 unsafe {
144 ring.submission()
145 .push(&sqe)
146 .map_err(|e| io::Error::other(format!("SQ push: {e}")))?;
147 }
148 }
149 let n = chunk.len();
150 ring.submit_and_wait(n)
151 .map_err(|e| io::Error::other(format!("submit_and_wait: {e}")))?;
152 for _ in 0..n {
153 let cqe = ring
154 .completion()
155 .next()
156 .ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "no cqe"))?;
157 let res = cqe.result();
158 let idx = cqe.user_data() as usize;
159 let elen = expect.get(idx).map(|x| x.1).unwrap_or(0);
160 if res < 0 {
161 return Err(io::Error::from_raw_os_error(-res));
162 }
163 if (res as usize) != elen {
164 return Err(io::Error::new(
165 io::ErrorKind::UnexpectedEof,
166 format!("read_many: short read {res} < {elen}"),
167 ));
168 }
169 total += res as usize;
170 }
171 }
172 Ok(total)
173 })
174}
175
176#[cfg(target_os = "linux")]
177thread_local! {
178 static URING: std::cell::RefCell<Option<io_uring::IoUring>> =
179 const { std::cell::RefCell::new(None) };
180}
181
182#[cfg(target_os = "linux")]
183fn submit_uring_read(f: &std::fs::File, buf: &mut [u8], off: u64) -> io::Result<usize> {
184 use std::os::fd::AsRawFd;
185 URING.with(|sl| {
186 let mut r = sl.borrow_mut();
187 if r.is_none() {
188 let ring = io_uring::IoUring::new(256)
189 .map_err(|e| io::Error::other(format!("IoUring::new: {e}")))?;
190 *r = Some(ring);
191 }
192 let ring = r.as_mut().unwrap();
193 let fd = f.as_raw_fd();
194 let sqe = io_uring::opcode::Read::new(
195 io_uring::types::Fd(fd),
196 buf.as_mut_ptr(),
197 buf.len() as u32,
198 )
199 .offset(off);
200 let sqe = sqe.build().user_data(0);
201 unsafe {
202 ring.submission()
203 .push(&sqe)
204 .map_err(|e| io::Error::other(format!("SQ push: {e}")))?;
205 }
206 ring.submit_and_wait(1)
207 .map_err(|e| io::Error::other(format!("submit_and_wait: {e}")))?;
208 let cqe = ring
209 .completion()
210 .next()
211 .ok_or_else(|| io::Error::new(io::ErrorKind::UnexpectedEof, "no cqe"))?;
212 let res = cqe.result();
213 if res < 0 {
214 return Err(io::Error::from_raw_os_error(-res));
215 }
216 Ok(res as usize)
217 })
218}
219
220#[cfg(target_os = "linux")]
221impl IoBackend for UringBackend {
222 fn read_at(&self, f: &File, buf: &mut [u8], off: u64) -> io::Result<usize> {
223 submit_uring_read(f, buf, off)
224 }
225
226 fn read_exact_at(&self, f: &File, buf: &mut [u8], off: u64) -> io::Result<()> {
227 let mut done = 0usize;
228 while done < buf.len() {
229 let n = self.read_at(f, &mut buf[done..], off + done as u64)?;
230 if n == 0 {
231 return Err(io::Error::new(
232 io::ErrorKind::UnexpectedEof,
233 "read_exact_at: eof inside buffer",
234 ));
235 }
236 done += n;
237 }
238 Ok(())
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn preadv_roundtrip() {
248 let dir = std::env::temp_dir().join("engramdb-backend-test");
249 let _ = std::fs::remove_dir_all(&dir);
250 std::fs::create_dir_all(&dir).unwrap();
251 let p = dir.join("t.bin");
252 std::fs::write(&p, (0u8..255).collect::<Vec<u8>>()).unwrap();
253 let f = std::fs::File::open(&p).unwrap();
254 let b = PreadvBackend;
255 let mut buf = [0u8; 4];
256 b.read_exact_at(&f, &mut buf, 10).unwrap();
257 assert_eq!(&buf, &[10, 11, 12, 13]);
258 let mut p2 = [0u8; 2];
259 let n = b.read_at(&f, &mut p2, 253).unwrap();
260 assert_eq!(n, 2);
261 assert_eq!(&p2, &[253, 254]);
262 let _ = std::fs::remove_dir_all(&dir);
263 }
264
265 #[test]
266 fn default_backend_constructs() {
267 let _ = default_backend();
268 }
269
270 #[cfg(target_os = "linux")]
271 #[test]
272 fn uring_roundtrip_and_semantics() {
273 let dir = std::env::temp_dir().join("engramdb-uring-test");
274 let _ = std::fs::remove_dir_all(&dir);
275 std::fs::create_dir_all(&dir).unwrap();
276 let p = dir.join("t.bin");
277 std::fs::write(&p, (0usize..512).map(|i| i as u8).collect::<Vec<u8>>()).unwrap();
278 let f = std::fs::File::open(&p).unwrap();
279 let b = UringBackend;
280 let mut buf = [0u8; 4];
281 b.read_exact_at(&f, &mut buf, 10).unwrap();
282 assert_eq!(&buf, &[10, 11, 12, 13]);
283 let mut p2 = [0u8; 2];
285 let n = b.read_at(&f, &mut p2, 253).unwrap();
286 assert_eq!(n, 2);
287 assert_eq!(&p2, &[253, 254]);
288 let mut p3 = [0u8; 4];
290 let n3 = b.read_at(&f, &mut p3, 512).unwrap();
291 assert_eq!(n3, 0);
292 let r = b.read_exact_at(&f, &mut p3, 511);
294 assert!(r.is_err());
295 let _ = std::fs::remove_dir_all(&dir);
296 }
297}