use std::any::Any;
use std::cell::Cell;
use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
const UCONTEXT_BYTES: usize = 5120;
const STACK_BYTES: usize = 4 << 20;
const POOL_MAX: usize = 32;
const GUARD_BYTES: usize = 4096;
#[repr(C, align(64))]
struct UContext([u8; UCONTEXT_BYTES]);
unsafe extern "C" {
fn getcontext(ucp: *mut UContext) -> i32;
fn makecontext(ucp: *mut UContext, func: extern "C" fn(), argc: i32, ...);
fn swapcontext(oucp: *mut UContext, ucp: *const UContext) -> i32;
fn mmap(addr: *mut u8, len: usize, prot: i32, flags: i32, fd: i32, off: i64) -> *mut u8;
fn munmap(addr: *mut u8, len: usize) -> i32;
fn mprotect(addr: *mut u8, len: usize, prot: i32) -> i32;
}
const PROT_NONE: i32 = 0;
const PROT_RW: i32 = 3;
const MAP_PRIVATE_ANON_STACK: i32 = 0x02 | 0x20 | 0x20000 | 0x4000; const MAP_FAILED: *mut u8 = !0usize as *mut u8;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FiberState {
Yielded,
Finished,
}
struct Inner {
fiber_ctx: UContext,
caller_ctx: UContext,
entry: Option<Box<dyn FnOnce()>>,
panic: Option<Box<dyn Any + Send>>,
done: bool,
map: *mut u8,
}
pub struct Fiber {
inner: Box<Inner>,
}
thread_local! {
static POOL: std::cell::RefCell<Vec<usize>> = const { std::cell::RefCell::new(Vec::new()) };
static CURRENT: Cell<*mut Inner> = const { Cell::new(std::ptr::null_mut()) };
}
extern "C" fn trampoline() {
let inner = CURRENT.with(Cell::get);
let f = unsafe { (*inner).entry.take() };
if let Some(f) = f
&& let Err(p) = catch_unwind(AssertUnwindSafe(f))
{
unsafe { (*inner).panic = Some(p) };
}
unsafe {
(*inner).done = true;
swapcontext(&raw mut (*inner).fiber_ctx, &raw const (*inner).caller_ctx);
}
unreachable!("a finished fiber was resumed");
}
impl Fiber {
pub fn new(entry: impl FnOnce() + 'static) -> Option<Fiber> {
let total = STACK_BYTES + GUARD_BYTES;
let pooled = POOL.with(|p| p.borrow_mut().pop());
let map = match pooled {
Some(m) => m as *mut u8,
None => {
let map = unsafe {
mmap(
std::ptr::null_mut(),
total,
PROT_RW,
MAP_PRIVATE_ANON_STACK,
-1,
0,
)
};
if map == MAP_FAILED {
return None;
}
unsafe { mprotect(map, GUARD_BYTES, PROT_NONE) };
map
}
};
let mut inner = Box::new(Inner {
fiber_ctx: UContext([0; UCONTEXT_BYTES]),
caller_ctx: UContext([0; UCONTEXT_BYTES]),
entry: Some(Box::new(entry)),
panic: None,
done: false,
map,
});
unsafe {
if getcontext(&raw mut inner.fiber_ctx) != 0 {
release_stack(map);
return None;
}
set_stack(
&mut inner.fiber_ctx,
map.add(GUARD_BYTES),
STACK_BYTES,
std::ptr::null_mut(),
);
makecontext(&raw mut inner.fiber_ctx, trampoline, 0);
}
Some(Fiber { inner })
}
pub fn resume(&mut self) -> FiberState {
assert!(!self.inner.done, "resume of a finished fiber");
let inner: *mut Inner = &raw mut *self.inner;
let prev = CURRENT.with(|c| c.replace(inner));
unsafe { swapcontext(&raw mut (*inner).caller_ctx, &raw const (*inner).fiber_ctx) };
CURRENT.with(|c| c.set(prev));
if let Some(p) = self.inner.panic.take() {
resume_unwind(p);
}
if self.inner.done {
FiberState::Finished
} else {
FiberState::Yielded
}
}
pub fn is_finished(&self) -> bool {
self.inner.done
}
}
impl Drop for Fiber {
fn drop(&mut self) {
release_stack(self.inner.map);
}
}
fn release_stack(map: *mut u8) {
let kept = POOL.with(|p| {
let mut p = p.borrow_mut();
let room = p.len() < POOL_MAX;
if room {
p.push(map as usize);
}
room
});
if !kept {
unsafe { munmap(map, STACK_BYTES + GUARD_BYTES) };
}
}
pub fn yield_now() -> bool {
let inner = CURRENT.with(Cell::get);
if inner.is_null() {
return false;
}
unsafe { swapcontext(&raw mut (*inner).fiber_ctx, &raw const (*inner).caller_ctx) };
true
}
pub fn on_fiber() -> bool {
!CURRENT.with(Cell::get).is_null()
}
unsafe fn set_stack(uc: &mut UContext, sp: *mut u8, size: usize, link: *mut UContext) {
let w = uc.0.as_mut_ptr().cast::<usize>();
unsafe {
w.add(1).write(link as usize); w.add(2).write(sp as usize); w.add(3).write(0); w.add(4).write(size); }
}
#[cfg(test)]
mod tests {
use super::*;
use std::rc::Rc;
#[test]
fn runs_to_completion_without_yielding() {
let hit = Rc::new(Cell::new(0));
let h = hit.clone();
let mut f = Fiber::new(move || h.set(7)).unwrap();
assert!(!on_fiber());
assert_eq!(f.resume(), FiberState::Finished);
assert_eq!(hit.get(), 7);
assert!(f.is_finished());
}
#[test]
fn yields_and_resumes_in_order() {
let log = Rc::new(std::cell::RefCell::new(Vec::new()));
let l = log.clone();
let mut f = Fiber::new(move || {
assert!(on_fiber());
l.borrow_mut().push(1);
assert!(yield_now());
l.borrow_mut().push(3);
yield_now();
l.borrow_mut().push(5);
})
.unwrap();
assert_eq!(f.resume(), FiberState::Yielded);
log.borrow_mut().push(2);
assert_eq!(f.resume(), FiberState::Yielded);
log.borrow_mut().push(4);
assert_eq!(f.resume(), FiberState::Finished);
assert_eq!(*log.borrow(), [1, 2, 3, 4, 5]);
}
#[test]
fn interleaved_fibers_keep_their_own_stacks() {
let out = Rc::new(std::cell::RefCell::new(Vec::new()));
let mk = |tag: i32| {
let o = out.clone();
Fiber::new(move || {
let local = [tag; 64];
yield_now();
o.borrow_mut().push(local.iter().sum::<i32>());
})
.unwrap()
};
let (mut a, mut b) = (mk(1), mk(2));
a.resume();
b.resume();
assert_eq!(b.resume(), FiberState::Finished);
assert_eq!(a.resume(), FiberState::Finished);
assert_eq!(*out.borrow(), [128, 64]);
}
#[test]
fn yield_on_main_stack_is_a_no_op() {
assert!(!yield_now());
}
#[test]
fn panic_is_re_raised_on_the_resumer() {
let mut f = Fiber::new(|| panic!("boom")).unwrap();
let r = catch_unwind(AssertUnwindSafe(|| f.resume()));
assert!(r.is_err());
}
#[test]
fn nested_fibers() {
let log = Rc::new(std::cell::RefCell::new(Vec::new()));
let l = log.clone();
let mut outer = Fiber::new(move || {
let l2 = l.clone();
let mut inner = Fiber::new(move || {
l2.borrow_mut().push("in1");
yield_now();
l2.borrow_mut().push("in2");
})
.unwrap();
inner.resume();
l.borrow_mut().push("out1");
yield_now();
inner.resume();
l.borrow_mut().push("out2");
})
.unwrap();
outer.resume();
outer.resume();
assert_eq!(*log.borrow(), ["in1", "out1", "in2", "out2"]);
}
}