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