Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions tokio/src/fs/mocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,15 @@ impl From<MockFile> for OwnedFd {
}
}

#[cfg(all(test, unix))]
impl From<OwnedFd> for MockFile {
#[inline]
fn from(file: OwnedFd) -> MockFile {
use std::os::fd::IntoRawFd;
unsafe { MockFile::from_raw_fd(IntoRawFd::into_raw_fd(file)) }
}
}

tokio_thread_local! {
static QUEUE: RefCell<VecDeque<Box<dyn FnOnce() + Send>>> = RefCell::new(VecDeque::new())
}
Expand Down
13 changes: 8 additions & 5 deletions tokio/src/fs/open_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,10 @@ impl OpenOptions {
/// [`Other`]: std::io::ErrorKind::Other
/// [`PermissionDenied`]: std::io::ErrorKind::PermissionDenied
pub async fn open(&self, path: impl AsRef<Path>) -> io::Result<File> {
self.open_inner(path.as_ref()).await
}

async fn open_inner(&self, path: &Path) -> io::Result<File> {
Comment thread
asder8215 marked this conversation as resolved.
match &self.inner {
Kind::Std(opts) => Self::std_open(opts, path).await,
#[cfg(all(
Expand All @@ -535,7 +539,7 @@ impl OpenOptions {
.check_and_init(io_uring::opcode::OpenAt::CODE)
.await?
{
Op::open(path.as_ref(), opts)?.await
Op::open(path, opts)?.await
} else {
let opts = opts.clone().into();
Self::std_open(&opts, path).await
Expand All @@ -544,12 +548,11 @@ impl OpenOptions {
}
}

async fn std_open(opts: &StdOpenOptions, path: impl AsRef<Path>) -> io::Result<File> {
let path = path.as_ref().to_owned();
async fn std_open(opts: &StdOpenOptions, path: &Path) -> io::Result<File> {
let path = path.to_owned();
let opts = opts.clone();

let std = asyncify(move || opts.open(path)).await?;
Ok(File::from_std(std))
Ok(asyncify(move || opts.open(path)).await?.into())
}

#[cfg(windows)]
Expand Down
20 changes: 17 additions & 3 deletions tokio/src/fs/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,23 @@ use std::{io, path::Path};
/// }
/// ```
pub async fn read(path: impl AsRef<Path>) -> io::Result<Vec<u8>> {
let path = path.as_ref().to_owned();
let path = path.as_ref();

#[cfg(all(
tokio_unstable,
feature = "io-uring",
feature = "rt",
feature = "fs",
target_os = "linux"
// libc::statx is only supported on these platforms
// FIXME: Add musl target env when our minimum supported
// rust version is 1.93. To clarify, statx support is
// introduced to musl in 1.25 as mentioned officially here:
// https://musl.libc.org/releases.html.
// However, rustup target_env building for *-linux-musl
// uses 1.25 musl on all *-linux-musl platforms starting
// in 1.93 stable rust version.
// https://blog.rust-lang.org/2025/12/05/Updating-musl-1.2.5/
any(target_env = "gnu", target_os = "android")
))]
{
use crate::fs::read_uring;
Expand All @@ -72,9 +81,14 @@ pub async fn read(path: impl AsRef<Path>) -> io::Result<Vec<u8>> {
.check_and_init(io_uring::opcode::Read::CODE)
.await?
{
return read_uring(&path).await;
return read_uring(path).await;
}
}

read_spawn_blocking(path).await
}

async fn read_spawn_blocking(path: &Path) -> io::Result<Vec<u8>> {
let path = path.to_owned();
asyncify(move || std::fs::read(path)).await
}
19 changes: 18 additions & 1 deletion tokio/src/fs/read_uring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,26 @@ const MAX_READ_SIZE: usize = 64 * 1024 * 1024;
pub(crate) async fn read_uring(path: &Path) -> io::Result<Vec<u8>> {
let file = OpenOptions::new().read(true).open(path).await?;

// TODO: use io uring in the future to obtain metadata
#[cfg(not(any(target_env = "gnu", target_os = "android")))]
let size_hint: Option<usize> = file.metadata().await.map(|m| m.len() as usize).ok();

#[cfg(
// libc::statx is only supported on these platforms
// FIXME: Add musl target env when our minimum supported
// rust version is 1.93. To clarify, statx support is
// introduced to musl in 1.25 as mentioned officially here:
// https://musl.libc.org/releases.html.
// However, rustup target_env building for *-linux-musl
// uses 1.25 musl on all *-linux-musl platforms starting
// in 1.93 stable rust version.
// https://blog.rust-lang.org/2025/12/05/Updating-musl-1.2.5/
any(target_env = "gnu", target_os = "android")
)]
let size_hint = Op::file_metadata(&file)?
Comment thread
Daksh14 marked this conversation as resolved.
.await
.map(|m| m.len() as usize)
.ok();

let fd: OwnedFd = file
.try_into_std()
.expect("unexpected in-flight operation detected")
Expand Down
63 changes: 62 additions & 1 deletion tokio/src/fs/try_exists.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,67 @@ use std::path::Path;
/// # }
/// ```
pub async fn try_exists(path: impl AsRef<Path>) -> io::Result<bool> {
let path = path.as_ref().to_owned();
let path = path.as_ref();

#[cfg(all(
tokio_unstable,
feature = "io-uring",
feature = "rt",
feature = "fs",
// libc::statx is only supported on these platforms
// FIXME: Add musl target env when our minimum supported
// rust version is 1.93. To clarify, statx support is
// introduced to musl in 1.25 as mentioned officially here:
// https://musl.libc.org/releases.html.
// However, rustup target_env building for *-linux-musl
// uses 1.25 musl on all *-linux-musl platforms starting
// in 1.93 stable rust version.
// https://blog.rust-lang.org/2025/12/05/Updating-musl-1.2.5/
any(target_env = "gnu", target_os = "android")
))]
{
let handle = crate::runtime::Handle::current();
let driver_handle = handle.inner.driver().io();
if driver_handle
.check_and_init(io_uring::opcode::Statx::CODE)
.await?
{
return try_exists_uring(path).await;
}
}

try_exists_spawn_blocking(path).await
}

cfg_io_uring! {
Comment thread
asder8215 marked this conversation as resolved.
#[inline]
#[cfg(
// libc::statx is only supported on these platforms
// FIXME: Add musl target env when our minimum supported
// rust version is 1.93. To clarify, statx support is
// introduced to musl in 1.25 as mentioned officially here:
// https://musl.libc.org/releases.html.
// However, rustup target_env building for *-linux-musl
// uses 1.25 musl on all *-linux-musl platforms starting
// in 1.93 stable rust version.
// https://blog.rust-lang.org/2025/12/05/Updating-musl-1.2.5/
any(target_env = "gnu", target_os = "android")
)]
async fn try_exists_uring(path: &Path) -> io::Result<bool> {
use crate::runtime::driver::op::Op;

match Op::metadata(path)?.await {
Ok(_) => Ok(true),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
Err(error) => Err(error),
}
}
}

async fn try_exists_spawn_blocking(path: &Path) -> io::Result<bool> {
let path = path.to_owned();
// FIXME: When MSRV is 1.81, change this to
// std::fs::exists() to be consistent with
// all other tokio::fs operations
asyncify(move || path.try_exists()).await
}
1 change: 1 addition & 0 deletions tokio/src/io/uring/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub(crate) mod open;
pub(crate) mod read;
pub(crate) mod statx;
pub(crate) mod utils;
pub(crate) mod write;
161 changes: 161 additions & 0 deletions tokio/src/io/uring/statx.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#![cfg(all(
tokio_unstable,
feature = "io-uring",
feature = "rt",
feature = "fs",
// libc::statx is only supported on these platforms
// FIXME: Add musl target env when our minimum supported
// rust version is 1.93. To clarify, statx support is
// introduced to musl in 1.25 as mentioned officially here:
// https://musl.libc.org/releases.html.
// However, rustup target_env building for *-linux-musl
// uses 1.25 musl on all *-linux-musl platforms starting
// in 1.93 stable rust version.
// https://blog.rust-lang.org/2025/12/05/Updating-musl-1.2.5/
any(target_env = "gnu", target_os = "android")
))]

use crate::fs::File;
use crate::io::uring::utils::cstr;
use crate::runtime::driver::op::{CancelData, Cancellable, Completable, CqeResult, Op};
use io_uring::{opcode, types};
use libc::statx;
use std::ffi::{CStr, CString};
use std::fmt::{Debug, Formatter};
use std::io;
use std::mem::MaybeUninit;
use std::os::fd::AsRawFd;
use std::path::Path;

pub(crate) struct Metadata(statx);

impl Metadata {
/// Returns the size of the file, in bytes, this metadata is for.
pub(crate) fn len(&self) -> u64 {
self.0.stx_size
}
}

impl Debug for Metadata {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut debug = f.debug_struct("Metadata");
debug.field("len", &self.len());
debug.finish_non_exhaustive()
}
}

#[derive(Debug)]
pub(crate) struct Statx {
Comment thread
asder8215 marked this conversation as resolved.
/// This field will be read by the kernel during the operation, so we
/// need to ensure it is valid for the entire duration of the operation.
_path: CString,
buffer: Box<MaybeUninit<statx>>,
}

impl Completable for Statx {
type Output = io::Result<Metadata>;

fn complete(self, cqe: CqeResult) -> Self::Output {
// SAFETY: On success, we always receive 0, which should guarantee
// that the information about a file is stored inside the
// statx buffer. On failure, we'll receive an Error value.
// Refer to man page description and return value:
// https://man7.org/linux/man-pages/man2/statx.2.html
cqe.result
.map(|_| Metadata(unsafe { *self.buffer.as_ptr() }))
}

fn complete_with_error(self, error: io::Error) -> Self::Output {
Err(error)
}
}

impl Cancellable for Statx {
fn cancel(self) -> CancelData {
CancelData::Statx(self)
}
}

impl Op<Statx> {
/// Submit a request to retrieve a file's status.
#[inline]
fn statx(path: &Path, flags: i32) -> io::Result<Op<Statx>> {
let path = cstr(path)?;
let mut buffer = Box::new(MaybeUninit::<statx>::uninit());

let statx_op = opcode::Statx::new(
types::Fd(libc::AT_FDCWD),
path.as_ptr(),
buffer.as_mut_ptr().cast(),
)
.flags(flags)
.mask(libc::STATX_BASIC_STATS | libc::STATX_BTIME)
.build();

// SAFETY: Parameters are valid for the entire duration of the operation
Ok(unsafe {
Op::new(
statx_op,
Statx {
_path: path,
buffer,
},
)
})
}

/// Retrieves the metadata information of the given path, following symlinks
/// if the path provided points to a symlink location.
#[inline]
pub(crate) fn metadata(path: &Path) -> io::Result<Op<Statx>> {
Op::statx(path, libc::AT_STATX_SYNC_AS_STAT)
}

/// Retrieves the metadata information of the given file
pub(crate) fn file_metadata(file: &File) -> io::Result<Op<Statx>> {
let mut buffer = Box::new(MaybeUninit::<statx>::uninit());
let empty_path: &'static CStr = c"";

// io-uring was introduced in linux 5.1
// pass in an empty path instead of null to target the file descriptor
// status as specified by man:
// https://man7.org/linux/man-pages/man2/statx.2.html
let statx_op = opcode::Statx::new(
types::Fd(file.as_raw_fd()),
Comment thread
asder8215 marked this conversation as resolved.
// it should be fine to pass in `empty_path` whose lifetime
// does not exceed the `file_metadata()` function as a ptr here
// because we want to stat the dirfd not this pathname
empty_path.as_ptr(),
buffer.as_mut_ptr().cast(),
)
.flags(libc::AT_STATX_SYNC_AS_STAT | libc::AT_EMPTY_PATH)
.mask(libc::STATX_BASIC_STATS | libc::STATX_BTIME)
.build();

// SAFETY: Parameters are valid for the entire duration of the operation
Ok(unsafe {
Op::new(
statx_op,
Statx {
_path: empty_path.into(),
buffer,
},
)
})
}

// TODO: Once `Metadata::from_statx` is stabilized, we can use use this function
// to enable io-uring support on `tokio::fs::symlink_metadata`.
// See this PR for more detail: https://github.com/tokio-rs/tokio/pull/8080
// See `Metadata::from_statx` tracking issue to see progress:
// https://github.com/rust-lang/rust/issues/156268
/// Retrieves the metadata information of the given path without following symlinks.
#[inline]
#[allow(dead_code)]
Comment thread
asder8215 marked this conversation as resolved.
pub(crate) fn symlink_metadata(path: &Path) -> io::Result<Op<Statx>> {
Op::statx(
path,
libc::AT_STATX_SYNC_AS_STAT | libc::AT_SYMLINK_NOFOLLOW,
)
}
}
26 changes: 26 additions & 0 deletions tokio/src/runtime/driver/op.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ use crate::io::uring::write::Write;

use crate::runtime::Handle;

#[cfg(
// libc::statx is only supported on these platforms
// FIXME: Add musl target env when our minimum supported
// rust version is 1.93. To clarify, statx support is
// introduced to musl in 1.25 as mentioned officially here:
// https://musl.libc.org/releases.html.
// However, rustup target_env building for *-linux-musl
// uses 1.25 musl on all *-linux-musl platforms starting
// in 1.93 stable rust version.
// https://blog.rust-lang.org/2025/12/05/Updating-musl-1.2.5/
any(target_env = "gnu", target_os = "android")
)]
use crate::io::uring::statx::Statx;
use io_uring::cqueue;
use io_uring::squeue::Entry;
use std::future::Future;
Expand All @@ -24,6 +37,19 @@ pub(crate) enum CancelData {
Write(Write),
ReadVec(Read<Vec<u8>, OwnedFd>),
ReadBuf(Read<Buf, ArcFd>),
#[cfg(
// libc::statx is only supported on these platforms
// FIXME: Add musl target env when our minimum supported
// rust version is 1.93. To clarify, statx support is
// introduced to musl in 1.25 as mentioned officially here:
// https://musl.libc.org/releases.html.
// However, rustup target_env building for *-linux-musl
// uses 1.25 musl on all *-linux-musl platforms starting
// in 1.93 stable rust version.
// https://blog.rust-lang.org/2025/12/05/Updating-musl-1.2.5/
any(target_env = "gnu", target_os = "android")
)]
Statx(Statx),
}

#[derive(Debug)]
Expand Down
Loading