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)]
38pub struct Options {
39 pub(crate) queue_depth: NonZeroU32,
40 pub(crate) max_batch_bytes: NonZeroUsize,
41 pub(crate) flags: Flags,
42}
43
44impl Default for Options {
45 fn default() -> Self {
46 Self {
47 queue_depth: NonZeroU32::new(256).unwrap(),
48 max_batch_bytes: NonZeroUsize::new(2 * 1024 * 1024).unwrap(),
49 flags: Flags::new(),
50 }
51 }
52}
53
54impl Options {
55 /// Execute buffered positional writes with ordered syscalls, avoiding
56 /// worker scheduling and the kernel-facing scratch copy. Disabled by
57 /// default; reads and fsyncs still use the ring unless separately configured.
58 pub fn syscall_writes(mut self, enabled: bool) -> Self {
59 self.flags.set_syscall_writes(enabled);
60 self
61 }
62 /// Require two kernel-page-cache probe hits per cohort of 16 small reads
63 /// (<=64 KiB) before selecting ordinary positional reads. A miss switches
64 /// the rest of the call to bounded ring batching. A mixed/evicted cohort may
65 /// block for at most 14 unprobed reads before checking again. Large reads
66 /// always use the ring; partial or zero probe replies never imply EOF.
67 /// Disabled by default; unsupported filesystems retain ordinary ring reads.
68 pub fn cached_reads(mut self, enabled: bool) -> Self {
69 self.flags.set_cached_reads(enabled);
70 self
71 }
72 /// Maximum submissions in a wave (1..=4096). Oversized values fail at
73 /// connection rather than allocating an arbitrarily large kernel ring.
74 pub fn queue_depth(mut self, depth: NonZeroU32) -> Self {
75 self.queue_depth = depth;
76 self
77 }
78 /// Maximum aggregate transfer bytes per wave, also bounding scratch storage,
79 /// independent of caller-facing
80 /// read allocation limits. Shared read limits still default to 16 MiB.
81 /// Defaults to 2 MiB to limit copy working sets while preserving many-file
82 /// concurrency; tune upward for storage that benefits from larger windows.
83 pub fn max_batch_bytes(mut self, bytes: NonZeroUsize) -> Self {
84 self.max_batch_bytes = bytes;
85 self
86 }
87}
88
89pub use engine::{Stats, Telemetry};
90
91/// Construct a rooted VFSI client. Roots follow the local backend's semantics
92/// (including creating a missing root); configure [`vfsi_core::api::ResourceLimits`]
93/// on the returned client to change application allocation/traversal budgets.
94pub fn connect(
95 root: impl AsRef<Path>,
96 options: Options,
97) -> VfResult<vfsi_sync::FsClient<vfsi_local::LocalBackend>> {
98 connect_with_telemetry(root, options).map(|(fs, _)| fs)
99}
100
101/// Like [`connect`], retaining low-cost counters for actual submissions,
102/// completions, waves, ring entries, arena growths and peak scratch bytes.
103/// Ring counters do not include syscalls. Opt-in cache probes and descriptor
104/// syscall requests have separate counters; namespace/dependency fallbacks
105/// remain excluded.
106pub fn connect_with_telemetry(
107 root: impl AsRef<Path>,
108 options: Options,
109) -> VfResult<(vfsi_sync::FsClient<vfsi_local::LocalBackend>, Telemetry)> {
110 let (engine, telemetry) = engine::Engine::new(options).map_err(|e| {
111 VfError::client(0, e.raw_os_error().unwrap_or(libc::EIO) as u32)
112 .with_context("io_uring_setup", root.as_ref())
113 })?;
114 let backend = vfsi_local::LocalBackend::new(root.as_ref().to_path_buf(), engine)?;
115 Ok((vfsi_sync::FsClient::new(backend), telemetry))
116}