Skip to main content

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}