Merge origin/main into macos
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -8,11 +8,12 @@
|
||||
//! The public surface intentionally mirrors `std::fs` where that shape makes sense, while using
|
||||
//! async methods for operations that may block the caller.
|
||||
|
||||
use alloc::sync::Arc;
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::io;
|
||||
use std::os::fd::{AsRawFd, OwnedFd};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::op::fs::{
|
||||
FileType as RawFileType, FsOp, MetadataTarget, OpenOptions as OpOpenOptions,
|
||||
|
||||
@@ -34,7 +34,10 @@ pub(crate) mod trace_targets {
|
||||
pub const DRIVER: &str = "ruin_runtime::driver";
|
||||
pub const RUNTIME: &str = "ruin_runtime::runtime";
|
||||
pub const SCHEDULER: &str = "ruin_runtime::scheduler";
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub const TIMER: &str = "ruin_runtime::timer";
|
||||
#[cfg(debug_assertions)]
|
||||
pub const ASYNC: &str = "ruin_runtime::async";
|
||||
}
|
||||
|
||||
@@ -46,6 +49,7 @@ pub mod net;
|
||||
pub mod op;
|
||||
#[doc(hidden)]
|
||||
pub mod platform;
|
||||
pub mod stdio;
|
||||
#[doc(hidden)]
|
||||
pub mod sys;
|
||||
pub mod time;
|
||||
|
||||
@@ -3,14 +3,15 @@
|
||||
//! The public surface follows the general shape of `std::net`, but uses async methods for socket
|
||||
//! operations that would otherwise block the caller.
|
||||
|
||||
use std::future::Future;
|
||||
use core::future::Future;
|
||||
use core::pin::Pin;
|
||||
use core::task::{Context, Poll};
|
||||
use core::time::Duration;
|
||||
|
||||
use std::io;
|
||||
use std::net::{Shutdown, SocketAddr, ToSocketAddrs};
|
||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Duration;
|
||||
|
||||
use hyper::rt::{Read as HyperRead, ReadBufCursor, Write as HyperWrite};
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
//! Public runtime driver primitives.
|
||||
//! RUIN Runtime Driver for Linux x86_64.
|
||||
//!
|
||||
//!
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::cell::RefCell;
|
||||
@@ -88,12 +90,21 @@ pub struct ReadyEvents {
|
||||
|
||||
/// Low-level Linux runtime driver backed by `io_uring`.
|
||||
pub struct Driver {
|
||||
/// The `io_uring` instance driving this runtime thread.
|
||||
ring: IoUring,
|
||||
/// Shared notifier that other threads can use to wake this runtime thread.
|
||||
notifier: Arc<NotifierInner>,
|
||||
/// Next sequence number for generated completion tokens.
|
||||
next_token: Cell<u64>,
|
||||
/// The token of the currently active timer, if any timer is armed.
|
||||
active_timer_token: Cell<Option<u64>>,
|
||||
/// Accumulated count of pending wake notifications that have not yet been triggered.
|
||||
pending_wakes: Cell<u64>,
|
||||
/// Accumulated count of pending timer expirations that have not yet been triggered.
|
||||
pending_timers: Cell<u64>,
|
||||
/// Map of active completion tokens to associated handlers. When a CQE is received with a token in this map, the
|
||||
/// corresponding handler will be invoked with the CQE and removed from the map. This is the core mechanism by which
|
||||
/// async operations are tracked and dispatched to their continuations.
|
||||
completions: RefCell<HashMap<u64, CompletionHandler>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,12 @@ pub struct TimeoutHandle {
|
||||
_local: Rc<()>,
|
||||
}
|
||||
|
||||
impl TimeoutHandle {
|
||||
pub fn clear(&self) {
|
||||
clear_timeout(self);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
/// Handle returned by [`set_interval`].
|
||||
pub struct IntervalHandle {
|
||||
@@ -58,6 +64,12 @@ pub struct IntervalHandle {
|
||||
_local: Rc<()>,
|
||||
}
|
||||
|
||||
impl IntervalHandle {
|
||||
pub fn clear(&self) {
|
||||
clear_interval(self);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle returned by [`queue_future`].
|
||||
///
|
||||
/// Awaiting a join handle yields the output of the queued future.
|
||||
@@ -68,9 +80,10 @@ pub struct JoinHandle<T> {
|
||||
/// Future returned by [`yield_now`].
|
||||
///
|
||||
/// Awaiting this future will immediately yield control back to the runtime scheduler, allowing other queued microtasks
|
||||
/// to run before the current task continues executing. Note that continuation of futures runs as a microtask, so this
|
||||
/// to run before the current task continues executing. Note that continuations of futures run as microtasks, so this
|
||||
/// can only yield to other microtasks and not to macrotasks (driver events such as file or network I/O, timers, or
|
||||
/// channel messages).
|
||||
/// channel messages). To yield to macrotasks, you must allow the flow of execution to return to the runtime event loop
|
||||
/// and flush the full microtask queue, for example by awaiting a timer.
|
||||
pub struct YieldNow {
|
||||
yielded: bool,
|
||||
}
|
||||
@@ -213,17 +226,19 @@ where
|
||||
);
|
||||
|
||||
if delay.is_zero() {
|
||||
unimplemented!("Zero-delay intervals are not yet implemented.")
|
||||
// TODO: vibeshit got this completely wrong
|
||||
// Zero-delay intervals never touch the timer heap or arm an io_uring
|
||||
// timer (a past-deadline kernel timer would fire on every event loop
|
||||
// iteration, spinning the runtime at 100 % CPU). Instead the callback
|
||||
// is stored in `immediate_intervals` and self-schedules as a macrotask
|
||||
// each turn, mirroring JS `setInterval(f, 0)` semantics.
|
||||
let callback = Rc::new(RefCell::new(Box::new(callback) as Box<dyn FnMut()>));
|
||||
current_thread()
|
||||
.immediate_intervals
|
||||
.borrow_mut()
|
||||
.insert(id, Rc::clone(&callback));
|
||||
schedule_immediate_interval(owner, id);
|
||||
// let callback = Rc::new(RefCell::new(Box::new(callback) as Box<dyn FnMut()>));
|
||||
// current_thread()
|
||||
// .immediate_intervals
|
||||
// .borrow_mut()
|
||||
// .insert(id, Rc::clone(&callback));
|
||||
// schedule_immediate_interval(owner, id);
|
||||
} else {
|
||||
let deadline = deadline_from_now(delay);
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -660,6 +675,8 @@ impl Future for YieldNow {
|
||||
}
|
||||
}
|
||||
|
||||
type ImmediateIntervals = RefCell<HashMap<usize, Rc<RefCell<Box<dyn FnMut()>>>>>;
|
||||
|
||||
struct ThreadState {
|
||||
driver: Driver,
|
||||
shared: Arc<ThreadShared>,
|
||||
@@ -669,7 +686,7 @@ struct ThreadState {
|
||||
timers: RefCell<TimerHeap>,
|
||||
/// Zero-delay intervals bypasses the timer heap entirely. Each entry
|
||||
/// re-enqueues itself as a macrotask on every turn.
|
||||
immediate_intervals: RefCell<HashMap<usize, IntervalCallback>>,
|
||||
immediate_intervals: ImmediateIntervals,
|
||||
next_timer_id: Cell<usize>,
|
||||
children: RefCell<Vec<ChildWorker>>,
|
||||
}
|
||||
@@ -1251,31 +1268,32 @@ fn clear_timer(owner: *const ThreadState, id: usize) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Enqueues one macrotask turn for a zero-delay interval.
|
||||
///
|
||||
/// The macrotask checks whether the interval is still live (not cleared), fires
|
||||
/// the callback, and re-enqueues itself for the next turn.
|
||||
fn schedule_immediate_interval(owner: *const ThreadState, id: usize) {
|
||||
push_local_macrotask(Box::new(move || {
|
||||
let callback = current_thread()
|
||||
.immediate_intervals
|
||||
.borrow()
|
||||
.get(&id)
|
||||
.map(Rc::clone);
|
||||
let Some(callback) = callback else {
|
||||
return; // interval was cleared before this turn ran
|
||||
};
|
||||
(callback.borrow_mut())();
|
||||
// Re-enqueue for the next turn if still live.
|
||||
if current_thread()
|
||||
.immediate_intervals
|
||||
.borrow()
|
||||
.contains_key(&id)
|
||||
{
|
||||
schedule_immediate_interval(owner, id);
|
||||
}
|
||||
}));
|
||||
}
|
||||
// TODO: vibeshit got this completely wrong
|
||||
// /// Enqueues one macrotask turn for a zero-delay interval.
|
||||
// ///
|
||||
// /// The macrotask checks whether the interval is still live (not cleared), fires
|
||||
// /// the callback, and re-enqueues itself for the next turn.
|
||||
// fn schedule_immediate_interval(owner: *const ThreadState, id: usize) {
|
||||
// push_local_macrotask(Box::new(move || {
|
||||
// let callback = current_thread()
|
||||
// .immediate_intervals
|
||||
// .borrow()
|
||||
// .get(&id)
|
||||
// .map(Rc::clone);
|
||||
// let Some(callback) = callback else {
|
||||
// return; // interval was cleared before this turn ran
|
||||
// };
|
||||
// (callback.borrow_mut())();
|
||||
// // Re-enqueue for the next turn if still live.
|
||||
// if current_thread()
|
||||
// .immediate_intervals
|
||||
// .borrow()
|
||||
// .contains_key(&id)
|
||||
// {
|
||||
// schedule_immediate_interval(owner, id);
|
||||
// }
|
||||
// }));
|
||||
// }
|
||||
|
||||
fn dispatch_expired_timers() {
|
||||
let now = deadline_from_now(Duration::ZERO);
|
||||
|
||||
@@ -58,6 +58,12 @@ pub struct IntervalHandle {
|
||||
_local: Rc<()>,
|
||||
}
|
||||
|
||||
impl IntervalHandle {
|
||||
pub fn clear(&self) {
|
||||
clear_interval(self);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle returned by [`queue_future`].
|
||||
///
|
||||
/// Awaiting a join handle yields the output of the queued future.
|
||||
|
||||
198
lib/runtime/src/stdio.rs
Normal file
198
lib/runtime/src/stdio.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
//! Async standard-input helpers.
|
||||
|
||||
use std::io;
|
||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
|
||||
use std::thread;
|
||||
|
||||
use crate::op::completion::completion_for_current_thread;
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
use crate::platform::linux_x86_64::runtime::with_current_driver;
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
use crate::platform::linux_x86_64::uring::{IORING_OP_READ, IoUringCqe, IoUringSqe};
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
use std::cell::Cell;
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
thread_local! {
|
||||
static STDIN_URING_SUPPORTED: Cell<Option<bool>> = const { Cell::new(None) };
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
const FILE_CURSOR: u64 = u64::MAX;
|
||||
const READ_CHUNK_BYTES: usize = 1024;
|
||||
|
||||
/// Async line-oriented stdin reader.
|
||||
///
|
||||
/// The reader is `io_uring`-first. When the active stdin fd rejects `IORING_OP_READ`, the module
|
||||
/// falls back to a helper-thread blocking read on the same duplicated fd.
|
||||
pub struct Stdin {
|
||||
fd: OwnedFd,
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Opens an async stdin reader.
|
||||
pub fn stdin() -> io::Result<Stdin> {
|
||||
let raw = cvt(unsafe { libc::fcntl(libc::STDIN_FILENO, libc::F_DUPFD_CLOEXEC, 0) })?;
|
||||
Ok(Stdin {
|
||||
fd: unsafe { OwnedFd::from_raw_fd(raw) },
|
||||
buffer: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
impl Stdin {
|
||||
/// Reads a single UTF-8 line, including the trailing newline when present.
|
||||
///
|
||||
/// Returns `Ok(None)` on EOF.
|
||||
pub async fn read_line(&mut self) -> io::Result<Option<String>> {
|
||||
loop {
|
||||
if let Some(index) = self.buffer.iter().position(|byte| *byte == b'\n') {
|
||||
let line = self.buffer.drain(..=index).collect::<Vec<_>>();
|
||||
return decode_line(line).map(Some);
|
||||
}
|
||||
|
||||
let chunk = self.read_chunk().await?;
|
||||
if chunk.is_empty() {
|
||||
if self.buffer.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let line = std::mem::take(&mut self.buffer);
|
||||
return decode_line(line).map(Some);
|
||||
}
|
||||
|
||||
self.buffer.extend_from_slice(&chunk);
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_chunk(&self) -> io::Result<Vec<u8>> {
|
||||
let fd = self.fd.as_raw_fd();
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
{
|
||||
let support = STDIN_URING_SUPPORTED.with(Cell::get);
|
||||
if support != Some(false) {
|
||||
match submit_uring_read(fd, READ_CHUNK_BYTES).await {
|
||||
Ok(bytes) => {
|
||||
STDIN_URING_SUPPORTED.with(|state| state.set(Some(true)));
|
||||
return Ok(bytes);
|
||||
}
|
||||
Err(error) if should_fallback_to_offload(&error) => {
|
||||
STDIN_URING_SUPPORTED.with(|state| state.set(Some(false)));
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
offload(move || {
|
||||
let mut buffer = vec![0; READ_CHUNK_BYTES];
|
||||
let read = blocking_read(fd, &mut buffer)?;
|
||||
if read == 0 {
|
||||
buffer.clear();
|
||||
} else {
|
||||
buffer.truncate(read);
|
||||
}
|
||||
Ok(buffer)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
async fn offload<T: Send + 'static>(
|
||||
task: impl FnOnce() -> io::Result<T> + Send + 'static,
|
||||
) -> io::Result<T> {
|
||||
let (future, handle) = completion_for_current_thread::<io::Result<T>>();
|
||||
thread::Builder::new()
|
||||
.name("ruin-runtime-stdio-offload".into())
|
||||
.spawn(move || handle.complete(task()))
|
||||
.map_err(io::Error::other)?;
|
||||
future.await
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
async fn submit_uring_read(fd: RawFd, len: usize) -> io::Result<Vec<u8>> {
|
||||
let mut buffer = vec![0; len];
|
||||
let ptr = buffer.as_mut_ptr();
|
||||
let capacity = buffer.len();
|
||||
submit_uring(
|
||||
move |sqe| {
|
||||
sqe.opcode = IORING_OP_READ;
|
||||
sqe.fd = fd;
|
||||
sqe.addr = ptr as u64;
|
||||
sqe.len = capacity as u32;
|
||||
sqe.off = FILE_CURSOR;
|
||||
},
|
||||
move |cqe| {
|
||||
let read = cqe_to_result(cqe)? as usize;
|
||||
buffer.truncate(read);
|
||||
Ok(buffer)
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
async fn submit_uring<T: Send + 'static, M>(
|
||||
fill: impl FnOnce(&mut IoUringSqe),
|
||||
map: M,
|
||||
) -> io::Result<T>
|
||||
where
|
||||
M: FnOnce(IoUringCqe) -> io::Result<T> + Send + 'static,
|
||||
{
|
||||
let (future, handle) = completion_for_current_thread::<io::Result<T>>();
|
||||
let callback_handle = handle.clone();
|
||||
let token = with_current_driver(|driver| {
|
||||
driver.submit_operation(fill, move |cqe| {
|
||||
callback_handle.complete(map(cqe));
|
||||
})
|
||||
})?;
|
||||
|
||||
handle.set_cancel(move || {
|
||||
let _ = with_current_driver(|driver| driver.cancel_operation(token));
|
||||
});
|
||||
|
||||
future.await
|
||||
}
|
||||
|
||||
fn blocking_read(fd: RawFd, buffer: &mut [u8]) -> io::Result<usize> {
|
||||
loop {
|
||||
let read =
|
||||
unsafe { libc::read(fd, buffer.as_mut_ptr().cast::<libc::c_void>(), buffer.len()) };
|
||||
if read >= 0 {
|
||||
return Ok(read as usize);
|
||||
}
|
||||
|
||||
let error = io::Error::last_os_error();
|
||||
if error.kind() == io::ErrorKind::Interrupted {
|
||||
continue;
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_line(bytes: Vec<u8>) -> io::Result<String> {
|
||||
String::from_utf8(bytes).map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
fn cqe_to_result(cqe: IoUringCqe) -> io::Result<i32> {
|
||||
if cqe.res < 0 {
|
||||
Err(io::Error::from_raw_os_error(-cqe.res))
|
||||
} else {
|
||||
Ok(cqe.res)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
fn should_fallback_to_offload(error: &io::Error) -> bool {
|
||||
matches!(
|
||||
error.raw_os_error(),
|
||||
Some(libc::EINVAL | libc::ENOSYS | libc::EOPNOTSUPP)
|
||||
)
|
||||
}
|
||||
|
||||
fn cvt(value: libc::c_int) -> io::Result<libc::c_int> {
|
||||
if value == -1 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,14 @@
|
||||
//! These helpers integrate with the runtime's timer queue and are designed to be used from
|
||||
//! futures scheduled with [`crate::queue_future`] or one of the runtime entry macros.
|
||||
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::fmt;
|
||||
use std::future::{Future, poll_fn};
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
use std::task::Waker;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Duration;
|
||||
use alloc::rc::Rc;
|
||||
use core::cell::{Cell, RefCell};
|
||||
use core::fmt;
|
||||
use core::future::{Future, poll_fn};
|
||||
use core::pin::Pin;
|
||||
use core::task::Waker;
|
||||
use core::task::{Context, Poll};
|
||||
use core::time::Duration;
|
||||
|
||||
use crate::{clear_timeout, set_timeout};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user