rspack_parallel/scope.rs
1use std::{cell::RefCell, future::Future, marker::PhantomData, pin::Pin};
2
3use tokio::task::{JoinError, JoinHandle};
4
5/// Scope Token
6pub struct Token<'scope, 'spawner, O> {
7 list: &'spawner RefCell<Vec<JoinHandle<O>>>,
8 _phantom: PhantomData<&'scope mut &'scope ()>,
9}
10
11/// Scope Spawner
12pub struct Spawner<'scope, 'spawner, T, O> {
13 list: &'spawner RefCell<Vec<JoinHandle<O>>>,
14 used: T,
15 _phantom: PhantomData<&'scope mut &'scope ()>,
16}
17
18/// Async scope helper
19///
20/// This function helps you write unsafe
21/// asynchronous structured concurrent code more easily.
22/// but it is **still unsafe**, so need to be careful when using it.
23///
24/// To use it safely,
25/// the user needs to ensure that the task is done within used reference lifetime.
26/// Due to `std::mem::forget`, the Rust currently cannot guarantee it.
27///
28/// From a practical point of view, the following points need to be note
29///
30/// * `.await` as early as possible
31/// * Don't put task into container unless you know what you are doing
32/// * Don't call `std::mem::forget`
33///
34/// # Example
35///
36/// ```rust
37/// # #[tokio::test]
38/// # async fn foo() {
39/// let list: Vec<u32> = vec![1, 2, 3, 4];
40///
41/// rspack_parallel::scope(|token| {
42/// for i in 0..list.len() {
43/// let s = unsafe { token.used(&list) };
44///
45/// s.spawn(move |list| async move {
46/// &list[i];
47/// });
48/// }
49/// })
50/// .await;
51/// # }
52/// ```
53///
54/// This doesn't compile
55///
56/// ```rust,compile_fail
57/// # async fn foo() {
58/// rspack_parallel::scope(|token| {
59/// let list: Vec<u32> = vec![1, 2, 3, 4];
60///
61/// for i in 0..list.len() {
62/// let s = unsafe { token.used(&list) };
63///
64/// s.spawn(move |list| async move {
65/// &list[i];
66/// });
67/// }
68/// })
69/// .await;
70/// # }
71/// ```
72pub async fn scope<'scope, F, O>(f: F) -> Vec<Result<O, JoinError>>
73where
74 for<'spawner> F: FnOnce(Token<'scope, 'spawner, O>),
75 O: Send + 'static,
76{
77 struct ScopeGuard(());
78
79 impl ScopeGuard {
80 fn forget(self) {
81 #[allow(clippy::disallowed_methods)]
82 std::mem::forget(self);
83 }
84 }
85
86 impl Drop for ScopeGuard {
87 fn drop(&mut self) {
88 // avoid unsound caused by poll interruption
89 std::process::abort();
90 }
91 }
92
93 let guard = ScopeGuard(());
94 let list = RefCell::new(Vec::new());
95
96 let token = Token {
97 list: &list,
98 _phantom: PhantomData,
99 };
100
101 f(token);
102
103 let list = RefCell::into_inner(list);
104 let mut output = Vec::with_capacity(list.len());
105
106 for j in list {
107 output.push(j.await);
108 }
109
110 guard.forget();
111 output
112}
113
114impl<'scope, 'spawner, O> Token<'scope, 'spawner, O> {
115 /// Use references
116 ///
117 /// Specify the reference to use when spawning the task.
118 ///
119 /// # Safety
120 ///
121 /// This is not sound.
122 ///
123 /// the user must ensure that `scope` task is legally consumed,
124 /// and assume that the runtime handles the task correctly.
125 pub unsafe fn used<T: 'scope>(&self, used: T) -> Spawner<'scope, 'spawner, T, O> {
126 Spawner {
127 list: self.list,
128 used,
129 _phantom: PhantomData,
130 }
131 }
132}
133
134impl<'scope, T, O> Spawner<'scope, '_, T, O> {
135 /// Spawn task from used reference
136 pub fn spawn<F, Fut>(self, f: F)
137 where
138 // TODO Use AsyncFnOnce
139 F: FnOnce(T) -> Fut + 'static,
140 Fut: Future<Output = O> + Send + 'scope,
141 T: Send + Sync + 'scope,
142 O: Send + 'static,
143 {
144 let fut = f(self.used);
145 let fut: Pin<Box<dyn Future<Output = O> + Send + 'scope>> = Box::pin(fut);
146
147 // # Safety
148 //
149 // The safety guarantee here comes from `Token::used`.
150 // The user needs to ensure that the task will done within used reference lifetime.
151 let fut: Pin<Box<dyn Future<Output = O> + Send + 'static>> =
152 unsafe { std::mem::transmute(fut) };
153
154 let j = rspack_tasks::spawn_in_compiler_context(fut);
155 self.list.borrow_mut().push(j);
156 }
157}