diff options
| author | Dario Nieuwenhuis <[email protected]> | 2022-04-27 04:45:23 +0200 |
|---|---|---|
| committer | Dario Nieuwenhuis <[email protected]> | 2022-04-27 04:56:41 +0200 |
| commit | 1599009a4f5fe1a0f9596b7b27bfd9fd84366377 (patch) | |
| tree | acd5a0bf15ece515c576673cfcfd8a81bd8d91f9 | |
| parent | 6f6c16f44924de4d71d0e5e3acc0908f2dd474e6 (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.rs | 2 | ||||
| -rw-r--r-- | embassy/src/executor/raw/mod.rs | 86 |
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 | ||
