xref: /wasmtime-44.0.1/crates/fiber/src/lib.rs (revision dfc4358d)
1 use anyhow::Error;
2 use std::any::Any;
3 use std::cell::Cell;
4 use std::io;
5 use std::marker::PhantomData;
6 use std::ops::Range;
7 use std::panic::{self, AssertUnwindSafe};
8 
9 cfg_if::cfg_if! {
10     if #[cfg(windows)] {
11         mod windows;
12         use windows as imp;
13     } else if #[cfg(unix)] {
14         mod unix;
15         use unix as imp;
16     } else {
17         compile_error!("fibers are not supported on this platform");
18     }
19 }
20 
21 /// Represents an execution stack to use for a fiber.
22 pub struct FiberStack(imp::FiberStack);
23 
24 impl FiberStack {
25     /// Creates a new fiber stack of the given size.
26     pub fn new(size: usize) -> io::Result<Self> {
27         Ok(Self(imp::FiberStack::new(size)?))
28     }
29 
30     /// Creates a new fiber stack of the given size.
31     pub fn from_custom(custom: Box<dyn RuntimeFiberStack>) -> io::Result<Self> {
32         Ok(Self(imp::FiberStack::from_custom(custom)?))
33     }
34 
35     /// Creates a new fiber stack with the given pointer to the bottom of the
36     /// stack plus the byte length of the stack.
37     ///
38     /// The `bottom` pointer should be addressable for `len` bytes. The page
39     /// beneath `bottom` should be unmapped as a guard page.
40     ///
41     /// # Safety
42     ///
43     /// This is unsafe because there is no validation of the given pointer.
44     ///
45     /// The caller must properly allocate the stack space with a guard page and
46     /// make the pages accessible for correct behavior.
47     pub unsafe fn from_raw_parts(bottom: *mut u8, len: usize) -> io::Result<Self> {
48         Ok(Self(imp::FiberStack::from_raw_parts(bottom, len)?))
49     }
50 
51     /// Gets the top of the stack.
52     ///
53     /// Returns `None` if the platform does not support getting the top of the
54     /// stack.
55     pub fn top(&self) -> Option<*mut u8> {
56         self.0.top()
57     }
58 
59     /// Returns the range of where this stack resides in memory if the platform
60     /// supports it.
61     pub fn range(&self) -> Option<Range<usize>> {
62         self.0.range()
63     }
64 
65     /// Is this a manually-managed stack created from raw parts? If so, it is up
66     /// to whoever created it to manage the stack's memory allocation.
67     pub fn is_from_raw_parts(&self) -> bool {
68         self.0.is_from_raw_parts()
69     }
70 }
71 
72 /// A creator of RuntimeFiberStacks.
73 pub unsafe trait RuntimeFiberStackCreator: Send + Sync {
74     /// Creates a new RuntimeFiberStack with the specified size, guard pages should be included,
75     /// memory should be zeroed.
76     ///
77     /// This is useful to plugin previously allocated memory instead of mmap'ing a new stack for
78     /// every instance.
79     fn new_stack(&self, size: usize) -> Result<Box<dyn RuntimeFiberStack>, Error>;
80 }
81 
82 /// A fiber stack backed by custom memory.
83 pub unsafe trait RuntimeFiberStack: Send + Sync {
84     /// The top of the allocated stack.
85     fn top(&self) -> *mut u8;
86     /// The valid range of the stack without guard pages.
87     fn range(&self) -> Range<usize>;
88 }
89 
90 pub struct Fiber<'a, Resume, Yield, Return> {
91     stack: Option<FiberStack>,
92     inner: imp::Fiber,
93     done: Cell<bool>,
94     _phantom: PhantomData<&'a (Resume, Yield, Return)>,
95 }
96 
97 pub struct Suspend<Resume, Yield, Return> {
98     inner: imp::Suspend,
99     _phantom: PhantomData<(Resume, Yield, Return)>,
100 }
101 
102 enum RunResult<Resume, Yield, Return> {
103     Executing,
104     Resuming(Resume),
105     Yield(Yield),
106     Returned(Return),
107     Panicked(Box<dyn Any + Send>),
108 }
109 
110 impl<'a, Resume, Yield, Return> Fiber<'a, Resume, Yield, Return> {
111     /// Creates a new fiber which will execute `func` on the given stack.
112     ///
113     /// This function returns a `Fiber` which, when resumed, will execute `func`
114     /// to completion. When desired the `func` can suspend itself via
115     /// `Fiber::suspend`.
116     pub fn new(
117         stack: FiberStack,
118         func: impl FnOnce(Resume, &mut Suspend<Resume, Yield, Return>) -> Return + 'a,
119     ) -> io::Result<Self> {
120         let inner = imp::Fiber::new(&stack.0, func)?;
121 
122         Ok(Self {
123             stack: Some(stack),
124             inner,
125             done: Cell::new(false),
126             _phantom: PhantomData,
127         })
128     }
129 
130     /// Resumes execution of this fiber.
131     ///
132     /// This function will transfer execution to the fiber and resume from where
133     /// it last left off.
134     ///
135     /// Returns `true` if the fiber finished or `false` if the fiber was
136     /// suspended in the middle of execution.
137     ///
138     /// # Panics
139     ///
140     /// Panics if the current thread is already executing a fiber or if this
141     /// fiber has already finished.
142     ///
143     /// Note that if the fiber itself panics during execution then the panic
144     /// will be propagated to this caller.
145     pub fn resume(&self, val: Resume) -> Result<Return, Yield> {
146         assert!(!self.done.replace(true), "cannot resume a finished fiber");
147         let result = Cell::new(RunResult::Resuming(val));
148         self.inner.resume(&self.stack().0, &result);
149         match result.into_inner() {
150             RunResult::Resuming(_) | RunResult::Executing => unreachable!(),
151             RunResult::Yield(y) => {
152                 self.done.set(false);
153                 Err(y)
154             }
155             RunResult::Returned(r) => Ok(r),
156             RunResult::Panicked(payload) => std::panic::resume_unwind(payload),
157         }
158     }
159 
160     /// Returns whether this fiber has finished executing.
161     pub fn done(&self) -> bool {
162         self.done.get()
163     }
164 
165     /// Gets the stack associated with this fiber.
166     pub fn stack(&self) -> &FiberStack {
167         self.stack.as_ref().unwrap()
168     }
169 
170     /// When this fiber has finished executing, reclaim its stack.
171     pub fn into_stack(mut self) -> FiberStack {
172         assert!(self.done());
173         self.stack.take().unwrap()
174     }
175 }
176 
177 impl<Resume, Yield, Return> Suspend<Resume, Yield, Return> {
178     /// Suspend execution of a currently running fiber.
179     ///
180     /// This function will switch control back to the original caller of
181     /// `Fiber::resume`. This function will then return once the `Fiber::resume`
182     /// function is called again.
183     ///
184     /// # Panics
185     ///
186     /// Panics if the current thread is not executing a fiber from this library.
187     pub fn suspend(&mut self, value: Yield) -> Resume {
188         self.inner
189             .switch::<Resume, Yield, Return>(RunResult::Yield(value))
190     }
191 
192     fn execute(
193         inner: imp::Suspend,
194         initial: Resume,
195         func: impl FnOnce(Resume, &mut Suspend<Resume, Yield, Return>) -> Return,
196     ) {
197         let mut suspend = Suspend {
198             inner,
199             _phantom: PhantomData,
200         };
201         let result = panic::catch_unwind(AssertUnwindSafe(|| (func)(initial, &mut suspend)));
202         suspend.inner.switch::<Resume, Yield, Return>(match result {
203             Ok(result) => RunResult::Returned(result),
204             Err(panic) => RunResult::Panicked(panic),
205         });
206     }
207 }
208 
209 impl<A, B, C> Drop for Fiber<'_, A, B, C> {
210     fn drop(&mut self) {
211         debug_assert!(self.done.get(), "fiber dropped without finishing");
212     }
213 }
214 
215 #[cfg(test)]
216 mod tests {
217     use super::{Fiber, FiberStack};
218     use std::cell::Cell;
219     use std::panic::{self, AssertUnwindSafe};
220     use std::rc::Rc;
221 
222     #[test]
223     fn small_stacks() {
224         Fiber::<(), (), ()>::new(FiberStack::new(0).unwrap(), |_, _| {})
225             .unwrap()
226             .resume(())
227             .unwrap();
228         Fiber::<(), (), ()>::new(FiberStack::new(1).unwrap(), |_, _| {})
229             .unwrap()
230             .resume(())
231             .unwrap();
232     }
233 
234     #[test]
235     fn smoke() {
236         let hit = Rc::new(Cell::new(false));
237         let hit2 = hit.clone();
238         let fiber = Fiber::<(), (), ()>::new(FiberStack::new(1024 * 1024).unwrap(), move |_, _| {
239             hit2.set(true);
240         })
241         .unwrap();
242         assert!(!hit.get());
243         fiber.resume(()).unwrap();
244         assert!(hit.get());
245     }
246 
247     #[test]
248     fn suspend_and_resume() {
249         let hit = Rc::new(Cell::new(false));
250         let hit2 = hit.clone();
251         let fiber = Fiber::<(), (), ()>::new(FiberStack::new(1024 * 1024).unwrap(), move |_, s| {
252             s.suspend(());
253             hit2.set(true);
254             s.suspend(());
255         })
256         .unwrap();
257         assert!(!hit.get());
258         assert!(fiber.resume(()).is_err());
259         assert!(!hit.get());
260         assert!(fiber.resume(()).is_err());
261         assert!(hit.get());
262         assert!(fiber.resume(()).is_ok());
263         assert!(hit.get());
264     }
265 
266     #[test]
267     fn backtrace_traces_to_host() {
268         #[inline(never)] // try to get this to show up in backtraces
269         fn look_for_me() {
270             run_test();
271         }
272         fn assert_contains_host() {
273             let trace = backtrace::Backtrace::new();
274             println!("{:?}", trace);
275             assert!(
276                 trace
277                 .frames()
278                 .iter()
279                 .flat_map(|f| f.symbols())
280                 .filter_map(|s| Some(s.name()?.to_string()))
281                 .any(|s| s.contains("look_for_me"))
282                 // TODO: apparently windows unwind routines don't unwind through fibers, so this will always fail. Is there a way we can fix that?
283                 || cfg!(windows)
284                 // TODO: the system libunwind is broken (#2808)
285                 || cfg!(all(target_os = "macos", target_arch = "aarch64"))
286             );
287         }
288 
289         fn run_test() {
290             let fiber =
291                 Fiber::<(), (), ()>::new(FiberStack::new(1024 * 1024).unwrap(), move |(), s| {
292                     assert_contains_host();
293                     s.suspend(());
294                     assert_contains_host();
295                     s.suspend(());
296                     assert_contains_host();
297                 })
298                 .unwrap();
299             assert!(fiber.resume(()).is_err());
300             assert!(fiber.resume(()).is_err());
301             assert!(fiber.resume(()).is_ok());
302         }
303 
304         look_for_me();
305     }
306 
307     #[test]
308     fn panics_propagated() {
309         let a = Rc::new(Cell::new(false));
310         let b = SetOnDrop(a.clone());
311         let fiber =
312             Fiber::<(), (), ()>::new(FiberStack::new(1024 * 1024).unwrap(), move |(), _s| {
313                 let _ = &b;
314                 panic!();
315             })
316             .unwrap();
317         assert!(panic::catch_unwind(AssertUnwindSafe(|| fiber.resume(()))).is_err());
318         assert!(a.get());
319 
320         struct SetOnDrop(Rc<Cell<bool>>);
321 
322         impl Drop for SetOnDrop {
323             fn drop(&mut self) {
324                 self.0.set(true);
325             }
326         }
327     }
328 
329     #[test]
330     fn suspend_and_resume_values() {
331         let fiber = Fiber::new(FiberStack::new(1024 * 1024).unwrap(), move |first, s| {
332             assert_eq!(first, 2.0);
333             assert_eq!(s.suspend(4), 3.0);
334             "hello".to_string()
335         })
336         .unwrap();
337         assert_eq!(fiber.resume(2.0), Err(4));
338         assert_eq!(fiber.resume(3.0), Ok("hello".to_string()));
339     }
340 }
341