//! 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), /// The actor was cooperatively cancelled via `request_stop`: a sentinel /// panic unwound its stack (running Drop) and the trampoline recognised /// the sentinel. Distinct from a user `Panic` — there is no payload to /// propagate. Stopped, } /// The payload of the sentinel panic raised at an observation point when an /// actor has been flagged for cooperative cancellation. Zero-sized; its only /// job is to be recognisable in the trampoline's `catch_unwind` so the /// teardown is reported as `Outcome::Stopped` rather than `Outcome::Panic`. /// /// User code that wraps its own `catch_unwind` can swallow this (cf. Erlang's /// `catch`); the stop flag stays set, so the next observation point re-raises. pub(crate) struct StopSentinel; // 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>> = 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> = 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> = 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 = const { Cell::new(false) }; } pub fn set_current_actor_box(b: Box) { 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 { 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 { 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 = match CURRENT_ACTOR_BOX.with(|c| c.borrow_mut().take()) { Some(b) => b, None => panic!("smarm: trampoline entered without a closure set (core corrupt)"), }; let outcome = match panic::catch_unwind(panic::AssertUnwindSafe(b)) { Ok(()) => Outcome::Exit, Err(payload) => { if payload.is::() { Outcome::Stopped } else { 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. /// (The saved stack pointer lives on the `Slot` as an atomic, not here: /// it is hot scheduling state, read/written without the cold lock.) pub stack: Stack, /// The PID of this actor's supervisor. Used to deliver `Signal` on death. pub supervisor: Pid, /// Cooperative-cancellation flag. `request_stop` sets it (and unparks a /// parked actor); the actor observes it lock-free at its next observation /// point — see `preempt::check_cancelled`. Shared via `Arc` so the running /// actor can poll it without taking the shared mutex. Lives on the `Actor` /// (constructed fresh at spawn), so unlike a `Slot` field it needs no reset /// in the slot-recycling paths. pub stop: std::sync::Arc, /// Trap-exit inbox (roadmap #3). `None` until the actor calls /// `link::trap_exit()`; `Some(tx)` means an abnormal death of a linked /// peer is delivered here as an `ExitSignal` *message* instead of /// cooperatively stopping this actor. Lives on the `Actor` (fresh per /// spawn) for the same reason as `stop`: no slot-recycling reset needed, /// and a restarted child correctly starts out *not* trapping. pub trap: Option>, }