v0.1: green-thread actors, supervision, channels, benchmark

Hand-rolled context switching on mmap'd stacks with guard pages,
allocator-driven RDTSC preemption, unbounded MPSC channels, supervision
via per-slot Signal mailboxes, root supervisor as sentinel PID.

Lib + tests + benches clean check/clippy. All 29 tests pass.
Bench: smarm 3.4% over serial baseline, within 160us of tokio
current-thread on prime-counting fan-out.
This commit is contained in:
Claude
2026-05-22 05:01:51 +00:00
commit 0e9d9d7d5f
17 changed files with 1938 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
//! Actor descriptor and trampoline.
//!
//! An `Actor` owns its stack and holds the closure it will run. The
//! `trampoline` is a fixed `extern "C-unwind" fn()` that every actor enters
//! through; it pulls the closure out of a thread-local set by the scheduler
//! immediately before resume, invokes it inside `catch_unwind`, records the
//! outcome, and switches back to the scheduler.
//!
//! Why a thread-local and not, say, passing the closure pointer via a
//! register? Because the first resume goes through `ret`, not `call`, and
//! we have no other channel for parameters. The scheduler sets the
//! thread-local, switches in, the trampoline reads it. After the first
//! resume the closure has been consumed, so subsequent resumes don't need it.
use crate::context::switch_to_scheduler;
use crate::pid::Pid;
use crate::stack::Stack;
use std::any::Any;
use std::cell::{Cell, RefCell};
use std::panic;
/// What an actor produced when it finished. Stored on the actor's slot,
/// drained by `JoinHandle::join` once the slot is marked done.
pub enum Outcome {
Exit,
Panic(Box<dyn Any + Send>),
}
// Thread-locals that the scheduler writes immediately before `switch_to_actor`.
thread_local! {
/// The closure for the actor we're about to resume *for the first time*.
/// Consumed on first entry into the trampoline; `None` thereafter.
static CURRENT_ACTOR_BOX: RefCell<Option<Box<dyn FnOnce() + Send>>> =
const { RefCell::new(None) };
/// The PID of the actor currently executing on this OS thread.
/// Set on every resume so that `self_pid()` works inside actor code.
static CURRENT_PID: Cell<Option<Pid>> = const { Cell::new(None) };
/// Filled by the trampoline when the actor returns (normally or via
/// panic). The scheduler reads this after `switch_to_actor` returns.
static LAST_OUTCOME: RefCell<Option<Outcome>> = const { RefCell::new(None) };
/// Set by the trampoline on completion; reset by the scheduler before
/// each resume so it never sees stale state.
static ACTOR_DONE: Cell<bool> = const { Cell::new(false) };
}
pub fn set_current_actor_box(b: Box<dyn FnOnce() + Send>) {
CURRENT_ACTOR_BOX.with(|c| *c.borrow_mut() = Some(b));
}
pub fn set_current_pid(p: Pid) {
CURRENT_PID.with(|c| c.set(Some(p)));
}
pub fn clear_current_pid() {
CURRENT_PID.with(|c| c.set(None));
}
pub fn current_pid() -> Option<Pid> {
CURRENT_PID.with(|c| c.get())
}
pub fn reset_actor_done() {
ACTOR_DONE.with(|c| c.set(false));
}
pub fn is_actor_done() -> bool {
ACTOR_DONE.with(|c| c.get())
}
pub fn take_last_outcome() -> Option<Outcome> {
LAST_OUTCOME.with(|r| r.borrow_mut().take())
}
/// The function whose address is written as the `ret` target on every actor
/// stack. The compiler must not inline this away. `extern "C-unwind"` permits
/// unwinding to cross the boundary, but `catch_unwind` here means unwinding
/// never actually does.
pub extern "C-unwind" fn trampoline() {
let b = CURRENT_ACTOR_BOX.with(|c| c.borrow_mut().take())
.expect("trampoline entered without a closure set");
let outcome = match panic::catch_unwind(panic::AssertUnwindSafe(b)) {
Ok(()) => Outcome::Exit,
Err(payload) => Outcome::Panic(payload),
};
LAST_OUTCOME.with(|r| *r.borrow_mut() = Some(outcome));
ACTOR_DONE.with(|c| c.set(true));
// Hand control back. The scheduler will tear down our slot and never
// resume us again.
unsafe { switch_to_scheduler() };
// Unreachable. If it isn't, the scheduler has a bug.
unreachable!("scheduler resumed a done actor");
}
/// One actor's worth of state. Owned by the scheduler's slot table.
pub struct Actor {
/// The PID this actor was assigned at spawn time.
pub pid: Pid,
/// The stack the actor runs on. Dropped (munmap'd) when the actor dies.
pub stack: Stack,
/// The saved stack pointer. Updated on every yield.
pub sp: usize,
/// The PID of this actor's supervisor. Used to deliver `Signal` on death.
pub supervisor: Pid,
}