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