├── .gitignore ├── Cargo.toml ├── src ├── lib.s └── lib.rs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Mike Pedersen "] 3 | edition = "2018" 4 | name = "rye" 5 | version = "0.1.0" 6 | 7 | [build-dependencies] 8 | cc = "1.0.61" 9 | 10 | [dev-dependencies] 11 | static_assertions = "1.1.0" -------------------------------------------------------------------------------- /src/lib.s: -------------------------------------------------------------------------------- 1 | .intel_syntax noprefix 2 | .section .text.yield_fiber 3 | .global yield_fiber 4 | .section .text.enter_fiber 5 | .global enter_fiber 6 | 7 | enter_fiber: 8 | mov [rsp-6*8], rbx 9 | mov [rsp-5*8], rbp 10 | mov [rsp-4*8], r12 11 | mov [rsp-3*8], r13 12 | mov [rsp-2*8], r14 13 | mov [rsp-1*8], r15 14 | mov rcx, rdi 15 | mov rdi, rsp 16 | mov r8, rsi 17 | mov rsi, rdx 18 | mov rsp, rcx 19 | call r8 20 | ud2 21 | 22 | yield_fiber: 23 | mov [rsp-6*8], rbx 24 | mov [rsp-5*8], rbp 25 | mov [rsp-4*8], r12 26 | mov [rsp-3*8], r13 27 | mov [rsp-2*8], r14 28 | mov [rsp-1*8], r15 29 | mov rax, rsp 30 | mov rsp, rdi 31 | mov rbx, [rsp-6*8] 32 | mov rbp, [rsp-5*8] 33 | mov r12, [rsp-4*8] 34 | mov r13, [rsp-3*8] 35 | mov r14, [rsp-2*8] 36 | mov r15, [rsp-1*8] 37 | ret 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rye 2 | 3 | Rye is a minimal, x86-64-only experiment into adding fibers to Rust. 4 | 5 | Rye exposes an API that allows spawning, scheduling, and deallocating fibers. This API, while 6 | largely safe, rests on a lot of unsafe assumptions not necessarily guaranteed by the rust 7 | compiler. This is just an experiment and you should not use it for anything critical. 8 | 9 | Rye has no central place where fibers are registered. Instead, when a fiber is yielded to it 10 | receives a handle to the yielding fiber. 11 | 12 | ## Example 13 | 14 | ```rust 15 | use rye::{Fiber, AllocStack}; 16 | 17 | // Create the fiber 18 | let (stack, fiber) = Fiber::spawn(AllocStack::new(4096), |main| { 19 | println!("Hello from fiber!"); 20 | main.yield_to(); 21 | }); 22 | 23 | // Yield to the fiber and return. This prints: 24 | // Hello from main! 25 | // Hello from fiber! 26 | // Back to main! 27 | println!("Hello from main!"); 28 | let fiber = fiber.yield_to(); 29 | println!("Back to main!"); 30 | 31 | // Reclaim stack to deallocate fiber 32 | stack.reclaim(fiber); 33 | ``` 34 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Rye is a minimal, x86-64-only experiment into adding fibers to Rust. 2 | //! 3 | //! Rye exposes an API that allows spawning, scheduling, and deallocating fibers. This API, while 4 | //! largely safe, rests on a lot of unsafe assumptions not necessarily guaranteed by the rust 5 | //! compiler. This is just an experiment and you should not use it for anything critical. 6 | //! 7 | //! Rye has no central place where fibers are registered. Instead, when a fiber is yielded to it 8 | //! receives a handle to the yielding fiber. 9 | //! 10 | //! # Example 11 | //! 12 | //! ``` 13 | //! use rye::{Fiber, AllocStack}; 14 | //! 15 | //! // Create the fiber 16 | //! let (stack, fiber) = Fiber::spawn(AllocStack::new(4096), |main| { 17 | //! println!("Hello from fiber!"); 18 | //! main.yield_to(); 19 | //! }); 20 | //! 21 | //! // Yield to the fiber and return. This prints: 22 | //! // Hello from main! 23 | //! // Hello from fiber! 24 | //! // Back to main! 25 | //! println!("Hello from main!"); 26 | //! let fiber = fiber.yield_to(); 27 | //! println!("Back to main!"); 28 | //! 29 | //! // Reclaim stack to deallocate fiber 30 | //! stack.reclaim(fiber); 31 | //! ``` 32 | 33 | use std::{ 34 | alloc::{alloc, dealloc, Layout}, 35 | ffi::c_void, 36 | mem::ManuallyDrop, 37 | panic::{catch_unwind, AssertUnwindSafe}, 38 | process::abort, 39 | ptr, 40 | }; 41 | 42 | extern "C" { 43 | fn enter_fiber( 44 | stack: *mut u8, 45 | func: extern "C" fn(prev: *mut FiberCont, data: *mut c_void), 46 | data: *mut c_void, 47 | ) -> *mut FiberCont; 48 | 49 | fn yield_fiber(fiber: *mut FiberCont) -> *mut FiberCont; 50 | } 51 | 52 | enum FiberCont {} 53 | 54 | /// A lightweight thread for cooperative multitasking. 55 | /// 56 | /// Unlike a thread, a fiber must be yielded to in order to run. Other threads will not be preempted 57 | /// for this to run. 58 | pub struct Fiber { 59 | fiber: *mut FiberCont, 60 | } 61 | 62 | impl Fiber { 63 | unsafe fn from_raw(fiber: *mut FiberCont) -> Self { 64 | Fiber { fiber } 65 | } 66 | 67 | /// Creates a new fiber that runs the given closure on the given stack. 68 | /// 69 | /// Control must never reach the end of the function and panics may not propagate unhandled to 70 | /// the top of the fiber. Should the function return or a unhandled panic propagate through, the 71 | /// program will abort. 72 | /// 73 | /// Instead, a fiber may halt by yielding to another fiber and letting that other fiber 74 | /// reclaiming the stack using the `FiberStack` returned when the fiber was spawned. 75 | pub fn spawn(stack: S, f: F) -> (FiberStack, Fiber) 76 | where 77 | S: StackBytes, 78 | F: FnOnce(Fiber) + 'static, 79 | { 80 | extern "C" fn exec(prev: *mut FiberCont, f: *mut c_void) 81 | where 82 | F: FnOnce(Fiber) + 'static, 83 | { 84 | let (f, fiber) = 85 | unsafe { (ptr::read(f as *mut F), Fiber::from_raw(yield_fiber(prev))) }; 86 | 87 | match catch_unwind(AssertUnwindSafe(|| (f)(fiber))) { 88 | Ok(()) => eprintln!("Aborting: Control reached end of fiber"), 89 | Err(err) => eprintln!("Aborting: Unhandled panic in fiber: {:?}", err), 90 | } 91 | 92 | abort() 93 | } 94 | 95 | unsafe { 96 | let (lo, hi) = stack.bytes(); 97 | assert!(lo as usize % 16 == 0, "Stack must be aligned to 16 bytes"); 98 | assert!(lo < hi, "Stack must be non-empty"); 99 | 100 | let mut f = ManuallyDrop::new(f); 101 | let result = enter_fiber(hi, exec::, &mut *f as *mut _ as *mut c_void); 102 | 103 | ( 104 | FiberStack(ManuallyDrop::new(stack)), 105 | Fiber::from_raw(result), 106 | ) 107 | } 108 | } 109 | 110 | /// Stops the execution of the calling fiber and switches control to the given fiber. 111 | /// 112 | /// This method returns once another fiber yields to the calling fiber. The return value is then 113 | /// the fiber that yielded to the calling fiber. 114 | pub fn yield_to(self) -> Fiber { 115 | unsafe { Fiber::from_raw(yield_fiber(self.fiber)) } 116 | } 117 | } 118 | 119 | /// A stack that is in use by a fiber. This can be used to reclaim the original original stack `S` 120 | /// once the fiber executing on the stack has stopped. 121 | pub struct FiberStack(ManuallyDrop); 122 | 123 | impl FiberStack { 124 | /// Whether the given fiber is contained within this stack. 125 | pub fn contains(&self, fiber: &Fiber) -> bool { 126 | let fiber = fiber.fiber as *const _; 127 | let (lo, hi) = self.0.bytes(); 128 | fiber >= lo && fiber < hi 129 | } 130 | 131 | /// Reclaims the underlying stack. 132 | /// 133 | /// Makes no checks to verify that the fiber on the stack has stopped. 134 | /// 135 | /// Prefer using `reclaim` or `reclaim_unsafe` if possible. 136 | /// 137 | /// # Safety 138 | /// 139 | /// You must ensure that no fiber is executing on the given stack. Failure to ensure this will 140 | /// result in undefined behavior. 141 | pub unsafe fn reclaim_unchecked(self) -> S { 142 | ManuallyDrop::into_inner(self.0) 143 | } 144 | 145 | /// Reclaims the stack the given fiber is executing on. 146 | /// 147 | /// # Panics 148 | /// 149 | /// Will panic if the given fiber is not executing on this stack. 150 | pub fn reclaim(self, fiber: Fiber) -> S { 151 | assert!(self.contains(&fiber), "Fiber must be within stack"); 152 | unsafe { self.reclaim_unchecked() } 153 | } 154 | } 155 | 156 | /// A byte slice suitable for executing a fiber on. 157 | pub unsafe trait StackBytes { 158 | /// Returns a range of bytes for executing a fiber on. The following conditions must be met for 159 | /// the returned tuple `(lo, hi)`: 160 | /// 161 | /// 1. The byte range [`lo`, `hi`[ must be readble and writable memory. 162 | /// 2. `lo` and `hi` must be aligned to 16 bytes. 163 | /// 3. `lo` must be greater than `hi`. 164 | /// 4. Every call to `bytes` for a given `StackBytes` instance must result in the same values 165 | /// returned for `lo` and `hi`. 166 | /// 5. The instance must have unique ownership over the range of bytes in [`lo`, `hi`[. 167 | /// 6. The range must remain readable and writable if the `StackBytes` instance is moved. 168 | fn bytes(&self) -> (*mut u8, *mut u8); 169 | } 170 | 171 | /// A stack slice that is allocated on the heap. 172 | pub struct AllocStack { 173 | ptr: *mut u8, 174 | layout: Layout, 175 | } 176 | 177 | impl AllocStack { 178 | /// Allocates a new `AllocStack` on the heap. 179 | /// 180 | /// # Panics 181 | /// 182 | /// Panics if `size` is 0 or `size` rounded up to nearest multiple of 16 overflows `usize`. 183 | pub fn new(size: usize) -> AllocStack { 184 | assert!(size > 0); 185 | let layout = Layout::from_size_align(size, 16).unwrap(); 186 | unsafe { 187 | AllocStack { 188 | ptr: alloc(layout), 189 | layout, 190 | } 191 | } 192 | } 193 | } 194 | 195 | impl Drop for AllocStack { 196 | fn drop(&mut self) { 197 | unsafe { dealloc(self.ptr, self.layout) } 198 | } 199 | } 200 | 201 | unsafe impl StackBytes for AllocStack { 202 | fn bytes(&self) -> (*mut u8, *mut u8) { 203 | unsafe { (self.ptr, self.ptr.add(self.layout.size())) } 204 | } 205 | } 206 | 207 | unsafe impl Send for AllocStack {} 208 | 209 | unsafe impl Sync for AllocStack {} 210 | 211 | #[cfg(test)] 212 | mod std_tests { 213 | use super::{AllocStack, Fiber, FiberStack}; 214 | use static_assertions::{assert_impl_all, assert_not_impl_any}; 215 | use std::{ 216 | cell::{Cell, RefCell}, 217 | panic::{RefUnwindSafe, UnwindSafe}, 218 | rc::Rc, 219 | }; 220 | 221 | assert_not_impl_any!(Fiber: Send); 222 | assert_impl_all!(Fiber: Unpin, UnwindSafe, RefUnwindSafe); 223 | assert_impl_all!(AllocStack: Send, Sync, Unpin, UnwindSafe, RefUnwindSafe); 224 | 225 | fn stack() -> AllocStack { 226 | AllocStack::new(16384) 227 | } 228 | 229 | #[derive(Clone)] 230 | struct Tester(Rc>); 231 | 232 | impl Tester { 233 | fn new() -> Self { 234 | Tester(Rc::new(Cell::new(0))) 235 | } 236 | 237 | fn step(&self, expected: usize) { 238 | assert_eq!(self.0.get(), expected); 239 | self.0.set(self.0.get() + 1); 240 | } 241 | 242 | fn spawn(&self, f: F) -> (FiberStack, Fiber) 243 | where 244 | F: FnOnce(Tester, Fiber) + 'static, 245 | { 246 | let this = self.clone(); 247 | Fiber::spawn(stack(), move |fib| f(this, fib)) 248 | } 249 | } 250 | 251 | #[test] 252 | fn basic() { 253 | let tester = Tester::new(); 254 | 255 | tester.step(0); 256 | 257 | let (stack, fiber) = tester.spawn(move |tester, prev| { 258 | tester.step(2); 259 | prev.yield_to(); 260 | }); 261 | 262 | tester.step(1); 263 | let fiber = fiber.yield_to(); 264 | tester.step(3); 265 | stack.reclaim(fiber); 266 | } 267 | 268 | #[test] 269 | fn yield_in_fiber() { 270 | let tester = Tester::new(); 271 | 272 | let (s1, f1) = tester.spawn(move |tester, prev| { 273 | tester.step(2); 274 | prev.yield_to(); 275 | }); 276 | 277 | let (s2, f2) = tester.spawn(move |tester, prev| { 278 | tester.step(1); 279 | let f1 = f1.yield_to(); 280 | tester.step(3); 281 | s1.reclaim(f1); 282 | prev.yield_to(); 283 | }); 284 | 285 | tester.step(0); 286 | let f2 = f2.yield_to(); 287 | tester.step(4); 288 | s2.reclaim(f2); 289 | } 290 | 291 | #[test] 292 | fn ping_pong() { 293 | let tester = Tester::new(); 294 | 295 | let (s1, f1) = tester.spawn(move |tester, f2| { 296 | tester.step(2); 297 | let f2 = f2.yield_to(); 298 | tester.step(4); 299 | let f2 = f2.yield_to(); 300 | tester.step(6); 301 | f2.yield_to(); 302 | }); 303 | 304 | let (s2, f2) = tester.spawn(move |tester, main| { 305 | tester.step(1); 306 | let f1 = f1.yield_to(); 307 | tester.step(3); 308 | let f1 = f1.yield_to(); 309 | tester.step(5); 310 | let f1 = f1.yield_to(); 311 | tester.step(7); 312 | s1.reclaim(f1); 313 | main.yield_to(); 314 | }); 315 | 316 | tester.step(0); 317 | let f2 = f2.yield_to(); 318 | tester.step(8); 319 | s2.reclaim(f2); 320 | } 321 | 322 | #[test] 323 | fn spawn_in_fiber() { 324 | let tester = Tester::new(); 325 | 326 | let (s1, f1) = tester.spawn(move |tester, main| { 327 | let (s2, f2) = tester.spawn(move |tester, f1| { 328 | tester.step(2); 329 | f1.yield_to(); 330 | }); 331 | 332 | tester.step(1); 333 | let f2 = f2.yield_to(); 334 | tester.step(3); 335 | s2.reclaim(f2); 336 | main.yield_to(); 337 | }); 338 | 339 | tester.step(0); 340 | let f1 = f1.yield_to(); 341 | tester.step(4); 342 | s1.reclaim(f1); 343 | } 344 | 345 | #[test] 346 | fn yield_from_inner_to_main() { 347 | let tester = Tester::new(); 348 | let s1_send = Rc::new(RefCell::>>::new(None)); 349 | let s2_send = Rc::new(RefCell::new(None)); 350 | let s1_recv = s1_send.clone(); 351 | let s2_recv = s2_send.clone(); 352 | 353 | let (s1, f1) = tester.spawn(move |tester, main| { 354 | let (s2, f2) = tester.spawn(move |tester, f1| { 355 | tester.step(2); 356 | (*s1_recv).borrow_mut().take().unwrap().reclaim(f1); 357 | main.yield_to(); 358 | }); 359 | 360 | *s2_send.borrow_mut() = Some(s2); 361 | tester.step(1); 362 | f2.yield_to(); 363 | }); 364 | 365 | *s1_send.borrow_mut() = Some(s1); 366 | 367 | tester.step(0); 368 | let f2 = f1.yield_to(); 369 | (*s2_recv).borrow_mut().take().unwrap().reclaim(f2); 370 | tester.step(3); 371 | } 372 | } 373 | --------------------------------------------------------------------------------