aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDario Nieuwenhuis <[email protected]>2022-04-27 04:45:23 +0200
committerDario Nieuwenhuis <[email protected]>2022-04-27 04:56:41 +0200
commit1599009a4f5fe1a0f9596b7b27bfd9fd84366377 (patch)
treeacd5a0bf15ece515c576673cfcfd8a81bd8d91f9
parent6f6c16f44924de4d71d0e5e3acc0908f2dd474e6 (diff)
executor: "send-spawn is OK if the args are Send" only holds for async fn futures.
The normal `spawn()` methods can be called directly by the user, with arbitrary hand-implemented futures. We can't enforce they're only called with `async fn` futures. Therefore, make these require `F: Send`, and add a "private" one only for use in the macro, which can enforce it.
-rw-r--r--embassy-macros/src/macros/task.rs2
-rw-r--r--embassy/src/executor/raw/mod.rs86
2 files changed, 55 insertions, 33 deletions
diff --git a/embassy-macros/src/macros/task.rs b/embassy-macros/src/macros/task.rs
index 96932d77c..e48de3d63 100644
--- a/embassy-macros/src/macros/task.rs
+++ b/embassy-macros/src/macros/task.rs
@@ -76,7 +76,7 @@ pub fn run(args: syn::AttributeArgs, f: syn::ItemFn) -> Result<TokenStream, Toke
76 #visibility fn #task_ident(#fargs) -> #embassy_path::executor::SpawnToken<impl Sized> { 76 #visibility fn #task_ident(#fargs) -> #embassy_path::executor::SpawnToken<impl Sized> {
77 type Fut = impl ::core::future::Future + 'static; 77 type Fut = impl ::core::future::Future + 'static;
78 static POOL: #embassy_path::executor::raw::TaskPool<Fut, #pool_size> = #embassy_path::executor::raw::TaskPool::new(); 78 static POOL: #embassy_path::executor::raw::TaskPool<Fut, #pool_size> = #embassy_path::executor::raw::TaskPool::new();
79 POOL.spawn(move || #task_inner_ident(#(#arg_names,)*)) 79 unsafe { POOL._spawn_async_fn(move || #task_inner_ident(#(#arg_names,)*)) }
80 } 80 }
81 }; 81 };
82 82
diff --git a/embassy/src/executor/raw/mod.rs b/embassy/src/executor/raw/mod.rs
index 6b14b8e8c..5cf399cdf 100644
--- a/embassy/src/executor/raw/mod.rs
+++ b/embassy/src/executor/raw/mod.rs
@@ -165,9 +165,9 @@ impl<F: Future + 'static> TaskStorage<F> {
165 /// on a different executor. 165 /// on a different executor.
166 pub fn spawn(&'static self, future: impl FnOnce() -> F) -> SpawnToken<impl Sized> { 166 pub fn spawn(&'static self, future: impl FnOnce() -> F) -> SpawnToken<impl Sized> {
167 if self.spawn_allocate() { 167 if self.spawn_allocate() {
168 unsafe { self.spawn_initialize(future) } 168 unsafe { SpawnToken::<F>::new(self.spawn_initialize(future)) }
169 } else { 169 } else {
170 SpawnToken::new_failed() 170 SpawnToken::<F>::new_failed()
171 } 171 }
172 } 172 }
173 173
@@ -179,37 +179,11 @@ impl<F: Future + 'static> TaskStorage<F> {
179 .is_ok() 179 .is_ok()
180 } 180 }
181 181
182 unsafe fn spawn_initialize<FutFn>(&'static self, future: FutFn) -> SpawnToken<impl Sized> 182 unsafe fn spawn_initialize(&'static self, future: impl FnOnce() -> F) -> NonNull<TaskHeader> {
183 where
184 FutFn: FnOnce() -> F,
185 {
186 // Initialize the task 183 // Initialize the task
187 self.raw.poll_fn.write(Self::poll); 184 self.raw.poll_fn.write(Self::poll);
188 self.future.write(future()); 185 self.future.write(future());
189 186 NonNull::new_unchecked(&self.raw as *const TaskHeader as *mut TaskHeader)
190 // When send-spawning a task, we construct the future in this thread, and effectively
191 // "send" it to the executor thread by enqueuing it in its queue. Therefore, in theory,
192 // send-spawning should require the future `F` to be `Send`.
193 //
194 // The problem is this is more restrictive than needed. Once the future is executing,
195 // it is never sent to another thread. It is only sent when spawning. It should be
196 // enough for the task's arguments to be Send. (and in practice it's super easy to
197 // accidentally make your futures !Send, for example by holding an `Rc` or a `&RefCell` across an `.await`.)
198 //
199 // We can do it by sending the task args and constructing the future in the executor thread
200 // on first poll. However, this cannot be done in-place, so it'll waste stack space for a copy
201 // of the args.
202 //
203 // Luckily, an `async fn` future contains just the args when freshly constructed. So, if the
204 // args are Send, it's OK to send a !Send future, as long as we do it before first polling it.
205 //
206 // (Note: this is how the generators are implemented today, it's not officially guaranteed yet,
207 // but it's possible it'll be guaranteed in the future. See zulip thread:
208 // https://rust-lang.zulipchat.com/#narrow/stream/187312-wg-async/topic/.22only.20before.20poll.22.20Send.20futures )
209 //
210 // The `FutFn` captures all the args, so if it's Send, the task can be send-spawned.
211 // This is why we return `SpawnToken<FutFn>` below.
212 SpawnToken::<FutFn>::new(NonNull::new_unchecked(&self.raw as *const TaskHeader as _))
213 } 187 }
214 188
215 unsafe fn poll(p: NonNull<TaskHeader>) { 189 unsafe fn poll(p: NonNull<TaskHeader>) {
@@ -261,11 +235,59 @@ impl<F: Future + 'static, const N: usize> TaskPool<F, N> {
261 pub fn spawn(&'static self, future: impl FnOnce() -> F) -> SpawnToken<impl Sized> { 235 pub fn spawn(&'static self, future: impl FnOnce() -> F) -> SpawnToken<impl Sized> {
262 for task in &self.pool { 236 for task in &self.pool {
263 if task.spawn_allocate() { 237 if task.spawn_allocate() {
264 return unsafe { task.spawn_initialize(future) }; 238 return unsafe { SpawnToken::<F>::new(task.spawn_initialize(future)) };
239 }
240 }
241
242 SpawnToken::<F>::new_failed()
243 }
244
245 /// Like spawn(), but allows the task to be send-spawned if the args are Send even if
246 /// the future is !Send.
247 ///
248 /// Not covered by semver guarantees. DO NOT call this directly. Intended to be used
249 /// by the Embassy macros ONLY.
250 ///
251 /// SAFETY: `future` must be a closure of the form `move || my_async_fn(args)`, where `my_async_fn`
252 /// is an `async fn`, NOT a hand-written `Future`.
253 #[doc(hidden)]
254 pub unsafe fn _spawn_async_fn<FutFn>(&'static self, future: FutFn) -> SpawnToken<impl Sized>
255 where
256 FutFn: FnOnce() -> F,
257 {
258 // When send-spawning a task, we construct the future in this thread, and effectively
259 // "send" it to the executor thread by enqueuing it in its queue. Therefore, in theory,
260 // send-spawning should require the future `F` to be `Send`.
261 //
262 // The problem is this is more restrictive than needed. Once the future is executing,
263 // it is never sent to another thread. It is only sent when spawning. It should be
264 // enough for the task's arguments to be Send. (and in practice it's super easy to
265 // accidentally make your futures !Send, for example by holding an `Rc` or a `&RefCell` across an `.await`.)
266 //
267 // We can do it by sending the task args and constructing the future in the executor thread
268 // on first poll. However, this cannot be done in-place, so it'll waste stack space for a copy
269 // of the args.
270 //
271 // Luckily, an `async fn` future contains just the args when freshly constructed. So, if the
272 // args are Send, it's OK to send a !Send future, as long as we do it before first polling it.
273 //
274 // (Note: this is how the generators are implemented today, it's not officially guaranteed yet,
275 // but it's possible it'll be guaranteed in the future. See zulip thread:
276 // https://rust-lang.zulipchat.com/#narrow/stream/187312-wg-async/topic/.22only.20before.20poll.22.20Send.20futures )
277 //
278 // The `FutFn` captures all the args, so if it's Send, the task can be send-spawned.
279 // This is why we return `SpawnToken<FutFn>` below.
280 //
281 // This ONLY holds for `async fn` futures. The other `spawn` methods can be called directly
282 // by the user, with arbitrary hand-implemented futures. This is why these return `SpawnToken<F>`.
283
284 for task in &self.pool {
285 if task.spawn_allocate() {
286 return SpawnToken::<FutFn>::new(task.spawn_initialize(future));
265 } 287 }
266 } 288 }
267 289
268 SpawnToken::new_failed() 290 SpawnToken::<FutFn>::new_failed()
269 } 291 }
270} 292}
271 293