This commit is contained in:
Kenneth Allen 2026-09-28 23:22:04 +10:00
parent 891788af2a
commit ae5df39323
7 changed files with 240 additions and 45 deletions

23
Cargo.lock generated
View File

@ -95,6 +95,8 @@ dependencies = [
"log",
"send_wrapper",
"slab",
"slotmap",
"unchecked-refcell",
]
[[package]]
@ -323,6 +325,15 @@ version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "slotmap"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038"
dependencies = [
"version_check",
]
[[package]]
name = "syn"
version = "2.0.119"
@ -365,6 +376,12 @@ dependencies = [
"syn 3.0.6",
]
[[package]]
name = "unchecked-refcell"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05442d1d94026b1d3e8ca10750c1461ce60731aee72c698526574e0e91c6e7a2"
[[package]]
name = "unicode-ident"
version = "1.0.26"
@ -377,6 +394,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "windows-link"
version = "0.2.1"

View File

@ -4,9 +4,13 @@ version = "0.1.0"
edition = "2024"
[dependencies]
env_logger = "0.11.11"
io-uring = "0.7.15"
libc = "0.2.189"
log = "0.4.34"
send_wrapper = "0.6.0"
slab = "0.4.12"
slotmap = "1.1.1"
unchecked-refcell = "0.2.2"
[dev-dependencies]
env_logger = "0.11.11"

View File

View File

@ -1,19 +1,31 @@
#![feature(local_waker)]
#![feature(waker_fn)]
#![feature(context_ext)]
use std::{
cell::RefCell, pin::Pin, rc::{Rc, Weak}, task::{ContextBuilder, Poll, local_waker_fn, waker_fn}, thread::sleep, time::Instant,
ops::DerefMut as _,
pin::Pin,
ptr,
rc::{Rc, Weak},
task::{ContextBuilder, Poll, local_waker_fn, waker_fn},
time::Instant,
};
use io_uring::{
IoUring,
opcode::Nop,
register::Restriction,
types::{SubmitArgs, Timespec},
};
use libc::{ETIME};
use libc::ETIME;
use log::trace;
use send_wrapper::SendWrapper;
use slab::Slab;
use slotmap::{SlotMap, new_key_type};
use unchecked_refcell::UncheckedRefCell;
use crate::op::Op;
pub mod op;
mod timer;
struct Task {
@ -21,41 +33,41 @@ struct Task {
fut: Option<Pin<Box<dyn Future<Output = ()>>>>,
}
new_key_type! { struct TaskId; }
pub struct RuntimeInner {
timer: timer::TimerHeap,
ring: IoUring,
tasks: Slab<Task>,
runqueue: Vec<usize>,
tasks: SlotMap<TaskId, Task>,
runqueue: Vec<TaskId>,
}
pub struct Runtime(Rc<RefCell<RuntimeInner>>);
struct RuntimeWeak(Weak<RefCell<RuntimeInner>>);
pub struct Runtime(Rc<UncheckedRefCell<RuntimeInner>>);
struct RuntimeWeak(Weak<UncheckedRefCell<RuntimeInner>>);
thread_local! {
static RT: RefCell<Option<Runtime>> = RefCell::new(None);
static RT: UncheckedRefCell<Option<Runtime>> = const { UncheckedRefCell::new(None) };
}
impl Runtime {
pub fn new() -> Self {
let ring = IoUring::builder()
.setup_cqsize(256)
.setup_r_disabled()
//.setup_r_disabled()
.setup_defer_taskrun()
.setup_single_issuer()
.setup_no_sqarray()
.build(128)
.expect("failed to create ring");
let submitter = ring.submitter();
submitter
.register_restrictions(&mut [
// TODO
])
.expect("failed to register restrictions on ring");
submitter
.register_enable_rings()
.expect("failed to enable rings");
Self(Rc::new(RefCell::new(RuntimeInner {
//let submitter = ring.submitter();
//submitter
//.register_restrictions(&mut [Restriction::register_op(Nop::CODE)])
//.expect("failed to register restrictions on ring");
//submitter
//.register_enable_rings()
//.expect("failed to enable ring");
Self(Rc::new(UncheckedRefCell::new(RuntimeInner {
timer: Default::default(),
ring,
@ -78,28 +90,32 @@ impl Runtime {
}
pub fn run(&mut self) {
RT.set(Some(Runtime(Rc::clone(&self.0))));
RT.with(|cell| {
let mut lock = cell.borrow_mut();
if lock.is_some() {
panic!("runtime already initialized on this thread");
}
*lock = Some(Runtime(Rc::clone(&self.0)))
});
let mut lock = self.0.borrow_mut();
while !lock.tasks.is_empty() {
// Run until no ready tasks, no timers, no future CQEs
trace!("Polling tasks");
loop {
let Some(task_id) = lock.runqueue.pop() else {
break;
};
trace!("Polling task {}", task_id);
trace!("Polling {} tasks", lock.tasks.len());
while let Some(task_id) = lock.runqueue.pop() {
trace!("Polling task {:?}", task_id);
lock.tasks[task_id].ready = false;
let waker = {
let inner = SendWrapper::new(self.downgrade());
waker_fn(move || {
if let Some(inner) = inner.0.upgrade() {
trace!("Waking {}", task_id);
trace!("Waking {:?}", task_id);
let mut lock = inner.borrow_mut();
let ready = &mut lock.tasks[task_id].ready;
if !*ready {
trace!("Enqueueing {}", task_id);
trace!("Enqueueing {:?}", task_id);
*ready = true;
lock.runqueue.push(task_id);
}
@ -110,24 +126,29 @@ impl Runtime {
let inner = SendWrapper::new(self.downgrade());
local_waker_fn(move || {
if let Some(inner) = inner.0.upgrade() {
trace!("Local waking {}", task_id);
trace!("Local waking {:?}", task_id);
let mut lock = inner.borrow_mut();
let ready = &mut lock.tasks[task_id].ready;
if !*ready {
trace!("Local enqueueing {}", task_id);
trace!("Local enqueueing {:?}", task_id);
*ready = true;
lock.runqueue.push(task_id);
}
}
})
};
let mut cx = ContextBuilder::from_waker(&waker)
.local_waker(&local_waker)
.build();
// TODO we shouldn't need to take task out
let mut task = lock.tasks[task_id].fut.take().unwrap();
drop(lock);
let mut cx = ContextBuilder::from_waker(&waker)
.local_waker(&local_waker)
.ext(self)
.build();
// Unlock runtime while running user code
let outcome = task.as_mut().poll(&mut cx);
lock = self.0.borrow_mut();
match outcome {
Poll::Ready(()) => {
lock.tasks.remove(task_id);
@ -169,14 +190,26 @@ impl Runtime {
None => 0,
};
match lock.ring.submitter().submit_with_args(want, &args) {
Ok(n) => {
debug_assert_eq!(n, 0);
}
Ok(_) => {}
Err(err) if err.raw_os_error() == Some(ETIME) && wait.is_some() => {}
e => {
e.expect("failed to enter ring");
}
}
loop {
let entry = lock.ring.completion().next();
let Some(cqe) = entry else {
break;
};
trace!("Processing CQE {:?}", &cqe);
debug_assert!(cqe.result() >= 0);
let user_data = ptr::with_exposed_provenance(cqe.user_data().try_into().unwrap());
let op: Rc<UncheckedRefCell<Op>> = unsafe { Rc::from_raw(user_data) };
drop(lock);
Op::handle(op);
lock = self.0.borrow_mut();
}
}
trace!("Exiting");
}
@ -186,19 +219,47 @@ impl Runtime {
mod tests {
use std::time::Duration;
use crate::timer::delay;
use crate::{op::no_op, timer::delay};
use super::*;
fn init() {
let _ = env_logger::builder().is_test(true).try_init();
}
#[test]
fn it_works() {
env_logger::init();
init();
let mut runtime = Runtime::new();
runtime.spawn(Box::pin(async {
println!("1");
delay(Duration::from_secs(1)).await;
no_op().await;
println!("2");
delay(Duration::from_secs(1)).await;
println!("3");
no_op().await;
println!("4");
}));
runtime.run();
}
#[test]
fn concurrency() {
init();
let mut runtime = Runtime::new();
for x in 0..10 {
runtime.spawn(Box::pin(async move {
println!("Starting {}", x);
delay(Duration::from_millis(x * 100)).await;
for y in 0..10 {
println!("Checkin {}ms", y * 1000 + x * 100);
delay(Duration::from_secs(1)).await;
no_op().await;
}
println!("Finishing {}", x);
}));
}
runtime.run();
}
}

102
src/op.rs Normal file
View File

@ -0,0 +1,102 @@
use std::{
mem,
ops::DerefMut as _,
pin::Pin,
rc::Rc,
task::{Context, LocalWaker, Poll},
};
use io_uring::opcode::Nop;
use log::trace;
use unchecked_refcell::UncheckedRefCell;
use crate::{RT, Runtime, RuntimeInner, op::NoOp::Cancelled};
pub struct OpEntry(Rc<UncheckedRefCell<Op>>);
pub enum Op {
NoOp(NoOp),
}
enum NoOp {
New,
Live(LocalWaker),
Done,
Cancelled,
}
pub fn no_op() -> OpEntry {
OpEntry(Rc::new(UncheckedRefCell::new(Op::NoOp(NoOp::New))))
}
impl Future for OpEntry {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut lock = self.0.borrow_mut();
match &mut *lock {
Op::NoOp(NoOp::Cancelled) => unreachable!(),
Op::NoOp(NoOp::Done) => Poll::Ready(()),
Op::NoOp(op @ NoOp::New) => {
{
let rt: &Runtime = cx.ext().downcast_ref().unwrap();
let mut lock = rt.0.borrow_mut();
let mut sq = lock.ring.submission();
let entry = Nop::new().build().user_data(
Rc::into_raw(self.0.clone())
.expose_provenance()
.try_into()
.unwrap(),
);
trace!("Pushing SQE {:?}", &entry);
unsafe { sq.push(&entry) }.expect("failed to submit no-op");
}
*op = NoOp::Live(cx.local_waker().clone());
Poll::Pending
}
Op::NoOp(NoOp::Live(waker)) => {
waker.clone_from(cx.local_waker());
Poll::Pending
}
}
}
}
impl Op {
pub fn handle(this: Rc<UncheckedRefCell<Self>>) {
// TODO don't drop for multishot accepts
let mut lock = this.borrow_mut();
match lock.deref_mut() {
Op::NoOp(op) => match mem::replace(op, NoOp::Done) {
NoOp::New | NoOp::Done => unreachable!(),
NoOp::Cancelled => {}
NoOp::Live(waker) => waker.wake(),
},
}
}
}
/*
impl Drop for OpEntry {
fn drop(&mut self) {
let mut lock = self.0.borrow_mut();
match lock.deref_mut() {
Op::NoOp(op) => {
match mem::replace(op, NoOp::Cancelled) {
NoOp::New | NoOp::Done => {}
NoOp::Cancelled => unreachable!(),
NoOp::Live(_) => {
RT.with(|rt| {
let outer = rt.borrow();
let mut lock = outer.as_ref().expect("future cancelled outside runtime thread").0.borrow_mut();
let mut sq = lock.ring.submission();
let entry = AsyncCancel::new(self.0.as_ptr().addr().try_into().unwrap()).build();
todo!()
})
}
}
}
}
}
}
*/

View File

@ -103,11 +103,9 @@ impl Drop for Timer {
pub fn delay_until(deadline: Instant) -> Timer {
Timer {
slot: RT.with_borrow(|rt| {
let rt = rt
.as_ref()
.expect("called from outside curio runtime thread");
let mut lock = rt.0.as_ref().borrow_mut();
slot: RT.with(|rt| {
let rt = rt.borrow();
let mut lock = rt.as_ref().unwrap().0.as_ref().borrow_mut();
lock.timer.insert(deadline)
}),
}

7
todo.txt Normal file
View File

@ -0,0 +1,7 @@
Minimal wakers
Nop reactor
Sleep reactor
File reactor
Network reactor
Task soundness
Unchecked SQ/CQ without debug_assertions