Coverage Report

Created: 2021-03-22 08:29

/src/wasmtime/crates/fiber/src/lib.rs
Line
Count
Source (jump to first uncovered line)
1
use std::any::Any;
2
use std::cell::Cell;
3
use std::io;
4
use std::marker::PhantomData;
5
use std::panic::{self, AssertUnwindSafe};
6
7
#[cfg(windows)]
8
mod windows;
9
#[cfg(windows)]
10
use windows as imp;
11
12
#[cfg(unix)]
13
mod unix;
14
#[cfg(unix)]
15
use unix as imp;
16
17
pub struct Fiber<'a, Resume, Yield, Return> {
18
    inner: imp::Fiber,
19
    done: Cell<bool>,
20
    _phantom: PhantomData<&'a (Resume, Yield, Return)>,
21
}
22
23
pub struct Suspend<Resume, Yield, Return> {
24
    inner: imp::Suspend,
25
    _phantom: PhantomData<(Resume, Yield, Return)>,
26
}
27
28
enum RunResult<Resume, Yield, Return> {
29
    Executing,
30
    Resuming(Resume),
31
    Yield(Yield),
32
    Returned(Return),
33
    Panicked(Box<dyn Any + Send>),
34
}
35
36
impl<'a, Resume, Yield, Return> Fiber<'a, Resume, Yield, Return> {
37
    /// Creates a new fiber which will execute `func` on a new native stack of
38
    /// size `stack_size`.
39
    ///
40
    /// This function returns a `Fiber` which, when resumed, will execute `func`
41
    /// to completion. When desired the `func` can suspend itself via
42
    /// `Fiber::suspend`.
43
    pub fn new(
44
        stack_size: usize,
45
        func: impl FnOnce(Resume, &Suspend<Resume, Yield, Return>) -> Return + 'a,
46
    ) -> io::Result<Fiber<'a, Resume, Yield, Return>> {
47
        Ok(Fiber {
48
            inner: imp::Fiber::new(stack_size, func)?,
49
            done: Cell::new(false),
50
            _phantom: PhantomData,
51
        })
52
    }
53
54
    /// Creates a new fiber with existing stack space that will execute `func`.
55
    ///
56
    /// This function returns a `Fiber` which, when resumed, will execute `func`
57
    /// to completion. When desired the `func` can suspend itself via
58
    /// `Fiber::suspend`.
59
    ///
60
    /// # Safety
61
    ///
62
    /// The caller must properly allocate the stack space with a guard page and
63
    /// make the pages accessible for correct behavior.
64
    pub unsafe fn new_with_stack(
65
        top_of_stack: *mut u8,
66
        func: impl FnOnce(Resume, &Suspend<Resume, Yield, Return>) -> Return + 'a,
67
    ) -> io::Result<Fiber<'a, Resume, Yield, Return>> {
68
        Ok(Fiber {
69
            inner: imp::Fiber::new_with_stack(top_of_stack, func)?,
70
            done: Cell::new(false),
71
            _phantom: PhantomData,
72
        })
73
    }
74
75
    /// Resumes execution of this fiber.
76
    ///
77
    /// This function will transfer execution to the fiber and resume from where
78
    /// it last left off.
79
    ///
80
    /// Returns `true` if the fiber finished or `false` if the fiber was
81
    /// suspended in the middle of execution.
82
    ///
83
    /// # Panics
84
    ///
85
    /// Panics if the current thread is already executing a fiber or if this
86
    /// fiber has already finished.
87
    ///
88
    /// Note that if the fiber itself panics during execution then the panic
89
    /// will be propagated to this caller.
90
0
    pub fn resume(&self, val: Resume) -> Result<Return, Yield> {
91
0
        assert!(!self.done.replace(true), "cannot resume a finished fiber");
92
0
        let result = Cell::new(RunResult::Resuming(val));
93
0
        self.inner.resume(&result);
94
0
        match result.into_inner() {
95
0
            RunResult::Resuming(_) | RunResult::Executing => unreachable!(),
96
0
            RunResult::Yield(y) => {
97
0
                self.done.set(false);
98
0
                Err(y)
99
            }
100
0
            RunResult::Returned(r) => Ok(r),
101
0
            RunResult::Panicked(payload) => std::panic::resume_unwind(payload),
102
        }
103
0
    }
104
105
    /// Returns whether this fiber has finished executing.
106
    pub fn done(&self) -> bool {
107
        self.done.get()
108
    }
109
}
110
111
impl<Resume, Yield, Return> Suspend<Resume, Yield, Return> {
112
    /// Suspend execution of a currently running fiber.
113
    ///
114
    /// This function will switch control back to the original caller of
115
    /// `Fiber::resume`. This function will then return once the `Fiber::resume`
116
    /// function is called again.
117
    ///
118
    /// # Panics
119
    ///
120
    /// Panics if the current thread is not executing a fiber from this library.
121
    pub fn suspend(&self, value: Yield) -> Resume {
122
        self.inner
123
            .switch::<Resume, Yield, Return>(RunResult::Yield(value))
124
    }
125
126
    fn execute(
127
        inner: imp::Suspend,
128
        initial: Resume,
129
        func: impl FnOnce(Resume, &Suspend<Resume, Yield, Return>) -> Return,
130
    ) {
131
        let suspend = Suspend {
132
            inner,
133
            _phantom: PhantomData,
134
        };
135
        let result = panic::catch_unwind(AssertUnwindSafe(|| (func)(initial, &suspend)));
136
        suspend.inner.switch::<Resume, Yield, Return>(match result {
137
            Ok(result) => RunResult::Returned(result),
138
            Err(panic) => RunResult::Panicked(panic),
139
        });
140
    }
141
}
142
143
impl<A, B, C> Drop for Fiber<'_, A, B, C> {
144
    fn drop(&mut self) {
145
        debug_assert!(self.done.get(), "fiber dropped without finishing");
146
    }
147
}
148
149
#[cfg(test)]
150
mod tests {
151
    use super::Fiber;
152
    use std::cell::Cell;
153
    use std::panic::{self, AssertUnwindSafe};
154
    use std::rc::Rc;
155
156
    #[test]
157
    fn small_stacks() {
158
        Fiber::<(), (), ()>::new(0, |_, _| {})
159
            .unwrap()
160
            .resume(())
161
            .unwrap();
162
        Fiber::<(), (), ()>::new(1, |_, _| {})
163
            .unwrap()
164
            .resume(())
165
            .unwrap();
166
    }
167
168
    #[test]
169
    fn smoke() {
170
        let hit = Rc::new(Cell::new(false));
171
        let hit2 = hit.clone();
172
        let fiber = Fiber::<(), (), ()>::new(1024 * 1024, move |_, _| {
173
            hit2.set(true);
174
        })
175
        .unwrap();
176
        assert!(!hit.get());
177
        fiber.resume(()).unwrap();
178
        assert!(hit.get());
179
    }
180
181
    #[test]
182
    fn suspend_and_resume() {
183
        let hit = Rc::new(Cell::new(false));
184
        let hit2 = hit.clone();
185
        let fiber = Fiber::<(), (), ()>::new(1024 * 1024, move |_, s| {
186
            s.suspend(());
187
            hit2.set(true);
188
            s.suspend(());
189
        })
190
        .unwrap();
191
        assert!(!hit.get());
192
        assert!(fiber.resume(()).is_err());
193
        assert!(!hit.get());
194
        assert!(fiber.resume(()).is_err());
195
        assert!(hit.get());
196
        assert!(fiber.resume(()).is_ok());
197
        assert!(hit.get());
198
    }
199
200
    #[test]
201
    fn backtrace_traces_to_host() {
202
        #[inline(never)] // try to get this to show up in backtraces
203
        fn look_for_me() {
204
            run_test();
205
        }
206
        fn assert_contains_host() {
207
            let trace = backtrace::Backtrace::new();
208
            println!("{:?}", trace);
209
            assert!(
210
                trace
211
                .frames()
212
                .iter()
213
                .flat_map(|f| f.symbols())
214
                .filter_map(|s| Some(s.name()?.to_string()))
215
                .any(|s| s.contains("look_for_me"))
216
                // TODO: apparently windows unwind routines don't unwind through fibers, so this will always fail. Is there a way we can fix that?
217
                || cfg!(windows)
218
            );
219
        }
220
221
        fn run_test() {
222
            let fiber = Fiber::<(), (), ()>::new(1024 * 1024, move |(), s| {
223
                assert_contains_host();
224
                s.suspend(());
225
                assert_contains_host();
226
                s.suspend(());
227
                assert_contains_host();
228
            })
229
            .unwrap();
230
            assert!(fiber.resume(()).is_err());
231
            assert!(fiber.resume(()).is_err());
232
            assert!(fiber.resume(()).is_ok());
233
        }
234
235
        look_for_me();
236
    }
237
238
    #[test]
239
    fn panics_propagated() {
240
        let a = Rc::new(Cell::new(false));
241
        let b = SetOnDrop(a.clone());
242
        let fiber = Fiber::<(), (), ()>::new(1024 * 1024, move |(), _s| {
243
            drop(&b);
244
            panic!();
245
        })
246
        .unwrap();
247
        assert!(panic::catch_unwind(AssertUnwindSafe(|| fiber.resume(()))).is_err());
248
        assert!(a.get());
249
250
        struct SetOnDrop(Rc<Cell<bool>>);
251
252
        impl Drop for SetOnDrop {
253
            fn drop(&mut self) {
254
                self.0.set(true);
255
            }
256
        }
257
    }
258
259
    #[test]
260
    fn suspend_and_resume_values() {
261
        let fiber = Fiber::new(1024 * 1024, move |first, s| {
262
            assert_eq!(first, 2.0);
263
            assert_eq!(s.suspend(4), 3.0);
264
            "hello".to_string()
265
        })
266
        .unwrap();
267
        assert_eq!(fiber.resume(2.0), Err(4));
268
        assert_eq!(fiber.resume(3.0), Ok("hello".to_string()));
269
    }
270
}