xref: /wasmtime-44.0.1/crates/fiber/src/lib.rs (revision b221fca7)
1 //! > **⚠️ Warning ⚠️**: this crate is an internal-only crate for the Wasmtime
2 //! > project and is not intended for general use. APIs are not strictly
3 //! > reviewed for safety and usage outside of Wasmtime may have bugs. If
4 //! > you're interested in using this feel free to file an issue on the
5 //! > Wasmtime repository to start a discussion about doing so, but otherwise
6 //! > be aware that your usage of this crate is not supported.
7 
8 #![no_std]
9 
10 #[cfg(any(feature = "std", unix, windows))]
11 #[macro_use]
12 extern crate std;
13 extern crate alloc;
14 
15 use alloc::boxed::Box;
16 use anyhow::Error;
17 use core::cell::Cell;
18 use core::marker::PhantomData;
19 use core::ops::Range;
20 
21 cfg_if::cfg_if! {
22     if #[cfg(not(feature = "std"))] {
23         mod nostd;
24         use nostd as imp;
25     } else if #[cfg(miri)] {
26         mod miri;
27         use miri as imp;
28     } else if #[cfg(windows)] {
29         mod windows;
30         use windows as imp;
31     } else if #[cfg(unix)] {
32         mod unix;
33         use unix as imp;
34     } else {
35         compile_error!("fibers are not supported on this platform");
36     }
37 }
38 
39 // Our own stack switcher routines are used on Unix and no_std
40 // platforms, but not on Windows (it has its own fiber API).
41 #[cfg(any(unix, not(feature = "std")))]
42 pub(crate) mod stackswitch;
43 
44 /// Represents an execution stack to use for a fiber.
45 pub struct FiberStack(imp::FiberStack);
46 
47 fn _assert_send_sync() {
48     fn _assert_send<T: Send>() {}
49     fn _assert_sync<T: Sync>() {}
50 
51     _assert_send::<FiberStack>();
52     _assert_sync::<FiberStack>();
53 }
54 
55 pub type Result<T, E = imp::Error> = core::result::Result<T, E>;
56 
57 impl FiberStack {
58     /// Creates a new fiber stack of the given size.
59     pub fn new(size: usize, zeroed: bool) -> Result<Self> {
60         Ok(Self(imp::FiberStack::new(size, zeroed)?))
61     }
62 
63     /// Creates a new fiber stack of the given size.
64     pub fn from_custom(custom: Box<dyn RuntimeFiberStack>) -> Result<Self> {
65         Ok(Self(imp::FiberStack::from_custom(custom)?))
66     }
67 
68     /// Creates a new fiber stack with the given pointer to the bottom of the
69     /// stack plus how large the guard size and stack size are.
70     ///
71     /// The bytes from `bottom` to `bottom.add(guard_size)` should all be
72     /// guaranteed to be unmapped. The bytes from `bottom.add(guard_size)` to
73     /// `bottom.add(guard_size + len)` should be addressable.
74     ///
75     /// # Safety
76     ///
77     /// This is unsafe because there is no validation of the given pointer.
78     ///
79     /// The caller must properly allocate the stack space with a guard page and
80     /// make the pages accessible for correct behavior.
81     pub unsafe fn from_raw_parts(bottom: *mut u8, guard_size: usize, len: usize) -> Result<Self> {
82         Ok(Self(unsafe {
83             imp::FiberStack::from_raw_parts(bottom, guard_size, len)?
84         }))
85     }
86 
87     /// Gets the top of the stack.
88     ///
89     /// Returns `None` if the platform does not support getting the top of the
90     /// stack.
91     pub fn top(&self) -> Option<*mut u8> {
92         self.0.top()
93     }
94 
95     /// Returns the range of where this stack resides in memory if the platform
96     /// supports it.
97     pub fn range(&self) -> Option<Range<usize>> {
98         self.0.range()
99     }
100 
101     /// Is this a manually-managed stack created from raw parts? If so, it is up
102     /// to whoever created it to manage the stack's memory allocation.
103     pub fn is_from_raw_parts(&self) -> bool {
104         self.0.is_from_raw_parts()
105     }
106 
107     /// Returns the range of memory that the guard page(s) reside in.
108     pub fn guard_range(&self) -> Option<Range<*mut u8>> {
109         self.0.guard_range()
110     }
111 }
112 
113 /// A creator of RuntimeFiberStacks.
114 pub unsafe trait RuntimeFiberStackCreator: Send + Sync {
115     /// Creates a new RuntimeFiberStack with the specified size, guard pages should be included,
116     /// memory should be zeroed.
117     ///
118     /// This is useful to plugin previously allocated memory instead of mmap'ing a new stack for
119     /// every instance.
120     fn new_stack(&self, size: usize, zeroed: bool) -> Result<Box<dyn RuntimeFiberStack>, Error>;
121 }
122 
123 /// A fiber stack backed by custom memory.
124 pub unsafe trait RuntimeFiberStack: Send + Sync {
125     /// The top of the allocated stack.
126     fn top(&self) -> *mut u8;
127     /// The valid range of the stack without guard pages.
128     fn range(&self) -> Range<usize>;
129     /// The range of the guard page(s)
130     fn guard_range(&self) -> Range<*mut u8>;
131 }
132 
133 pub struct Fiber<'a, Resume, Yield, Return> {
134     stack: Option<FiberStack>,
135     inner: imp::Fiber,
136     done: Cell<bool>,
137     _phantom: PhantomData<&'a (Resume, Yield, Return)>,
138 }
139 
140 pub struct Suspend<Resume, Yield, Return> {
141     inner: imp::Suspend,
142     _phantom: PhantomData<(Resume, Yield, Return)>,
143 }
144 
145 /// A structure that is stored on a stack frame of a call to `Fiber::resume`.
146 ///
147 /// This is used to both transmit data to a fiber (the resume step) as well as
148 /// acquire data from a fiber (the suspension step).
149 enum RunResult<Resume, Yield, Return> {
150     /// The fiber is currently executing meaning it picked up whatever it was
151     /// resuming with and hasn't yet completed.
152     Executing,
153 
154     /// Resume with this value. Called for each invocation of
155     /// `Fiber::resume`.
156     Resuming(Resume),
157 
158     /// The fiber hasn't finished but has provided the following value
159     /// during its suspension.
160     Yield(Yield),
161 
162     /// The fiber has completed with the provided value and can no
163     /// longer be resumed.
164     Returned(Return),
165 
166     /// The fiber execution panicked.
167     #[cfg(feature = "std")]
168     Panicked(Box<dyn core::any::Any + Send>),
169 }
170 
171 impl<'a, Resume, Yield, Return> Fiber<'a, Resume, Yield, Return> {
172     /// Creates a new fiber which will execute `func` on the given stack.
173     ///
174     /// This function returns a `Fiber` which, when resumed, will execute `func`
175     /// to completion. When desired the `func` can suspend itself via
176     /// `Fiber::suspend`.
177     pub fn new(
178         stack: FiberStack,
179         func: impl FnOnce(Resume, &mut Suspend<Resume, Yield, Return>) -> Return + 'a,
180     ) -> Result<Self> {
181         let inner = imp::Fiber::new(&stack.0, func)?;
182 
183         Ok(Self {
184             stack: Some(stack),
185             inner,
186             done: Cell::new(false),
187             _phantom: PhantomData,
188         })
189     }
190 
191     /// Resumes execution of this fiber.
192     ///
193     /// This function will transfer execution to the fiber and resume from where
194     /// it last left off.
195     ///
196     /// Returns `true` if the fiber finished or `false` if the fiber was
197     /// suspended in the middle of execution.
198     ///
199     /// # Panics
200     ///
201     /// Panics if the current thread is already executing a fiber or if this
202     /// fiber has already finished.
203     ///
204     /// Note that if the fiber itself panics during execution then the panic
205     /// will be propagated to this caller.
206     pub fn resume(&self, val: Resume) -> Result<Return, Yield> {
207         assert!(!self.done.replace(true), "cannot resume a finished fiber");
208         let result = Cell::new(RunResult::Resuming(val));
209         self.inner.resume(&self.stack().0, &result);
210         match result.into_inner() {
211             RunResult::Resuming(_) | RunResult::Executing => unreachable!(),
212             RunResult::Yield(y) => {
213                 self.done.set(false);
214                 Err(y)
215             }
216             RunResult::Returned(r) => Ok(r),
217             #[cfg(feature = "std")]
218             RunResult::Panicked(_payload) => {
219                 use std::panic;
220                 panic::resume_unwind(_payload);
221             }
222         }
223     }
224 
225     /// Returns whether this fiber has finished executing.
226     pub fn done(&self) -> bool {
227         self.done.get()
228     }
229 
230     /// Gets the stack associated with this fiber.
231     pub fn stack(&self) -> &FiberStack {
232         self.stack.as_ref().unwrap()
233     }
234 
235     /// When this fiber has finished executing, reclaim its stack.
236     pub fn into_stack(mut self) -> FiberStack {
237         assert!(self.done());
238         self.stack.take().unwrap()
239     }
240 }
241 
242 impl<Resume, Yield, Return> Suspend<Resume, Yield, Return> {
243     /// Suspend execution of a currently running fiber.
244     ///
245     /// This function will switch control back to the original caller of
246     /// `Fiber::resume`. This function will then return once the `Fiber::resume`
247     /// function is called again.
248     ///
249     /// # Panics
250     ///
251     /// Panics if the current thread is not executing a fiber from this library.
252     pub fn suspend(&mut self, value: Yield) -> Resume {
253         self.inner
254             .switch::<Resume, Yield, Return>(RunResult::Yield(value))
255     }
256 
257     fn execute(
258         inner: imp::Suspend,
259         initial: Resume,
260         func: impl FnOnce(Resume, &mut Suspend<Resume, Yield, Return>) -> Return,
261     ) {
262         let mut suspend = Suspend {
263             inner,
264             _phantom: PhantomData,
265         };
266 
267         #[cfg(feature = "std")]
268         let result = {
269             use std::panic::{self, AssertUnwindSafe};
270             let result = panic::catch_unwind(AssertUnwindSafe(|| (func)(initial, &mut suspend)));
271             match result {
272                 Ok(result) => RunResult::Returned(result),
273                 Err(panic) => RunResult::Panicked(panic),
274             }
275         };
276 
277         // Note that it is sound to omit the `catch_unwind` here: it
278         // will not result in unwinding going off the top of the fiber
279         // stack, because the code on the fiber stack is invoked via
280         // an extern "C" boundary which will panic on unwinds.
281         #[cfg(not(feature = "std"))]
282         let result = RunResult::Returned((func)(initial, &mut suspend));
283 
284         suspend.inner.exit::<Resume, Yield, Return>(result);
285     }
286 }
287 
288 impl<A, B, C> Drop for Fiber<'_, A, B, C> {
289     fn drop(&mut self) {
290         debug_assert!(self.done.get(), "fiber dropped without finishing");
291         unsafe {
292             self.inner.drop::<A, B, C>();
293         }
294     }
295 }
296 
297 #[cfg(all(test))]
298 mod tests {
299     use super::{Fiber, FiberStack};
300     use alloc::string::ToString;
301     use std::cell::Cell;
302     use std::rc::Rc;
303 
304     fn fiber_stack(size: usize) -> FiberStack {
305         FiberStack::new(size, false).unwrap()
306     }
307 
308     #[test]
309     fn small_stacks() {
310         Fiber::<(), (), ()>::new(fiber_stack(0), |_, _| {})
311             .unwrap()
312             .resume(())
313             .unwrap();
314         Fiber::<(), (), ()>::new(fiber_stack(1), |_, _| {})
315             .unwrap()
316             .resume(())
317             .unwrap();
318     }
319 
320     #[test]
321     fn smoke() {
322         let hit = Rc::new(Cell::new(false));
323         let hit2 = hit.clone();
324         let fiber = Fiber::<(), (), ()>::new(fiber_stack(1024 * 1024), move |_, _| {
325             hit2.set(true);
326         })
327         .unwrap();
328         assert!(!hit.get());
329         fiber.resume(()).unwrap();
330         assert!(hit.get());
331     }
332 
333     #[test]
334     fn suspend_and_resume() {
335         let hit = Rc::new(Cell::new(false));
336         let hit2 = hit.clone();
337         let fiber = Fiber::<(), (), ()>::new(fiber_stack(1024 * 1024), move |_, s| {
338             s.suspend(());
339             hit2.set(true);
340             s.suspend(());
341         })
342         .unwrap();
343         assert!(!hit.get());
344         assert!(fiber.resume(()).is_err());
345         assert!(!hit.get());
346         assert!(fiber.resume(()).is_err());
347         assert!(hit.get());
348         assert!(fiber.resume(()).is_ok());
349         assert!(hit.get());
350     }
351 
352     #[test]
353     fn backtrace_traces_to_host() {
354         #[inline(never)] // try to get this to show up in backtraces
355         fn look_for_me() {
356             run_test();
357         }
358         fn assert_contains_host() {
359             let trace = backtrace::Backtrace::new();
360             println!("{trace:?}");
361             assert!(
362                 trace
363                 .frames()
364                 .iter()
365                 .flat_map(|f| f.symbols())
366                 .filter_map(|s| Some(s.name()?.to_string()))
367                 .any(|s| s.contains("look_for_me"))
368                 // TODO: apparently windows unwind routines don't unwind through fibers, so this will always fail. Is there a way we can fix that?
369                 || cfg!(windows)
370                 // TODO: the system libunwind is broken (#2808)
371                 || cfg!(all(target_os = "macos", target_arch = "aarch64"))
372                 // TODO: see comments in `arm.rs` about how this seems to work
373                 // in gdb but not at runtime, unsure why at this time.
374                 || cfg!(target_arch = "arm")
375                 // asan does weird things
376                 || cfg!(asan)
377                 // miri is a bit of a stretch to get working here
378                 || cfg!(miri)
379             );
380         }
381 
382         fn run_test() {
383             let fiber = Fiber::<(), (), ()>::new(fiber_stack(1024 * 1024), move |(), s| {
384                 assert_contains_host();
385                 s.suspend(());
386                 assert_contains_host();
387                 s.suspend(());
388                 assert_contains_host();
389             })
390             .unwrap();
391             assert!(fiber.resume(()).is_err());
392             assert!(fiber.resume(()).is_err());
393             assert!(fiber.resume(()).is_ok());
394         }
395 
396         look_for_me();
397     }
398 
399     #[test]
400     #[cfg(feature = "std")]
401     fn panics_propagated() {
402         use std::panic::{self, AssertUnwindSafe};
403 
404         let a = Rc::new(Cell::new(false));
405         let b = SetOnDrop(a.clone());
406         let fiber = Fiber::<(), (), ()>::new(fiber_stack(1024 * 1024), move |(), _s| {
407             let _ = &b;
408             panic!();
409         })
410         .unwrap();
411         assert!(panic::catch_unwind(AssertUnwindSafe(|| fiber.resume(()))).is_err());
412         assert!(a.get());
413 
414         struct SetOnDrop(Rc<Cell<bool>>);
415 
416         impl Drop for SetOnDrop {
417             fn drop(&mut self) {
418                 self.0.set(true);
419             }
420         }
421     }
422 
423     #[test]
424     fn suspend_and_resume_values() {
425         let fiber = Fiber::new(fiber_stack(1024 * 1024), move |first, s| {
426             assert_eq!(first, 2.0);
427             assert_eq!(s.suspend(4), 3.0);
428             "hello".to_string()
429         })
430         .unwrap();
431         assert_eq!(fiber.resume(2.0), Err(4));
432         assert_eq!(fiber.resume(3.0), Ok("hello".to_string()));
433     }
434 }
435