vfsi_uring/lib.rs
1//! Linux io_uring execution of independent descriptor reads, writes and fsyncs.
2//!
3//! [`connect`] returns a client implementing [`vfsi_core::Vfsi`] and
4//! [`vfsi_core::VfsiExt`]. The local backend retains namespace resolution,
5//! descriptor ownership, bounded directory paging and ordered dependent I/O.
6//! Open/close, path writes, append, overlapping writes, and namespace operations
7//! use ordinary syscalls. This is synchronous batching, not an async runtime,
8//! direct I/O or zero-copy API. Completion order never changes result order.
9//! Opt-in cache-read and syscall-write paths avoid ring overhead for warm
10//! buffered I/O. Their counters are separate from actual SQEs and CQEs.
11//! Owned read cohorts fitting the configured byte window use their final result
12//! allocations directly. Borrowed destinations and larger cohorts retain
13//! bounded scratch storage; neither path is kernel zero-copy.
14//!
15//! Ring setup failures (including sandbox/seccomp restrictions) are returned;
16//! there is no silent fallback to a different backend.
17#![cfg(target_os = "linux")]
18
19mod engine;
20
21use std::{
22 num::{NonZeroU32, NonZeroUsize},
23 path::Path,
24};
25use vfsi_core::{VfError, VfResult};
26
27#[bitfields::bitfield(u8)]
28struct Flags {
29 cached_reads: bool,
30 syscall_writes: bool,
31 #[bits(6)]
32 _reserved: u8,
33}
34
35/// Bounds both in-flight SQEs and the reusable owned scratch arena. No SQPOLL
36/// thread is started. A large operation is split into bounded contiguous chunks.
37#[derive(Clone, Copy, Debug)]
38#[must_use = "configuration and requests have no effect until used"]
39pub struct Options {
40 pub(crate) queue_depth: NonZeroU32,
41 pub(crate) max_batch_bytes: NonZeroUsize,
42 pub(crate) flags: Flags,
43}
44
45impl Default for Options {
46 fn default() -> Self {
47 Self {
48 queue_depth: NonZeroU32::new(256).unwrap(),
49 max_batch_bytes: NonZeroUsize::new(2 * 1024 * 1024).unwrap(),
50 flags: Flags::new(),
51 }
52 }
53}
54
55impl Options {
56 /// Execute buffered positional writes with ordered syscalls, avoiding
57 /// worker scheduling and the kernel-facing scratch copy. Disabled by
58 /// default; reads and fsyncs still use the ring unless separately configured.
59 pub fn with_syscall_writes(mut self, enabled: bool) -> Self {
60 self.flags.set_syscall_writes(enabled);
61 self
62 }
63 /// Require two kernel-page-cache probe hits per cohort of 16 small reads
64 /// (<=64 KiB) before selecting ordinary positional reads. A miss switches
65 /// the rest of the call to bounded ring batching. A mixed/evicted cohort may
66 /// block for at most 14 unprobed reads before checking again. Large reads
67 /// always use the ring; partial or zero probe replies never imply EOF.
68 /// Disabled by default; unsupported filesystems retain ordinary ring reads.
69 pub fn with_cached_reads(mut self, enabled: bool) -> Self {
70 self.flags.set_cached_reads(enabled);
71 self
72 }
73 /// Maximum submissions in a wave (1..=4096). Oversized values fail at
74 /// connection rather than allocating an arbitrarily large kernel ring.
75 pub fn with_queue_depth(mut self, depth: NonZeroU32) -> Self {
76 self.queue_depth = depth;
77 self
78 }
79 /// Maximum aggregate transfer bytes per wave, also bounding scratch storage,
80 /// independent of caller-facing
81 /// read allocation limits. Shared read limits still default to 16 MiB.
82 /// Defaults to 2 MiB to limit copy working sets while preserving many-file
83 /// concurrency; tune upward for storage that benefits from larger windows.
84 pub fn with_max_batch_bytes(mut self, bytes: NonZeroUsize) -> Self {
85 self.max_batch_bytes = bytes;
86 self
87 }
88}
89
90pub use engine::{Stats, Telemetry};
91
92/// Construct a VFSI client rooted at an existing directory.
93/// Configure [`vfsi_core::api::ResourceLimits`]
94/// on the returned client to change application allocation/traversal budgets.
95pub fn connect(
96 root: impl AsRef<Path>,
97 options: Options,
98) -> VfResult<vfsi_sync::FsClient<vfsi_local::LocalBackend>> {
99 connect_with_telemetry(root, options).map(|(fs, _)| fs)
100}
101
102/// Like [`connect`], retaining low-cost counters for actual submissions,
103/// completions, waves, ring entries, arena growths and peak scratch bytes.
104/// Ring counters do not include syscalls. Opt-in cache probes and descriptor
105/// syscall requests have separate counters; namespace/dependency fallbacks
106/// remain excluded.
107pub fn connect_with_telemetry(
108 root: impl AsRef<Path>,
109 options: Options,
110) -> VfResult<(vfsi_sync::FsClient<vfsi_local::LocalBackend>, Telemetry)> {
111 let (engine, telemetry) = engine::Engine::new(options).map_err(|e| {
112 VfError::client(0, e.raw_os_error().unwrap_or(libc::EIO) as u32)
113 .with_context("io_uring_setup", root.as_ref())
114 })?;
115 let backend = vfsi_local::LocalBackend::new(root.as_ref().to_path_buf(), engine)?;
116 Ok((vfsi_sync::FsClient::new(backend), telemetry))
117}