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.
This commit is contained in:
parent
6f6c16f449
commit
1599009a4f
2 changed files with 55 additions and 33 deletions
|
@ -76,7 +76,7 @@ pub fn run(args: syn::AttributeArgs, f: syn::ItemFn) -> Result<TokenStream, Toke
|
||||||
#visibility fn #task_ident(#fargs) -> #embassy_path::executor::SpawnToken<impl Sized> {
|
#visibility fn #task_ident(#fargs) -> #embassy_path::executor::SpawnToken<impl Sized> {
|
||||||
type Fut = impl ::core::future::Future + 'static;
|
type Fut = impl ::core::future::Future + 'static;
|
||||||
static POOL: #embassy_path::executor::raw::TaskPool<Fut, #pool_size> = #embassy_path::executor::raw::TaskPool::new();
|
static POOL: #embassy_path::executor::raw::TaskPool<Fut, #pool_size> = #embassy_path::executor::raw::TaskPool::new();
|
||||||
POOL.spawn(move || #task_inner_ident(#(#arg_names,)*))
|
unsafe { POOL._spawn_async_fn(move || #task_inner_ident(#(#arg_names,)*)) }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -165,9 +165,9 @@ impl<F: Future + 'static> TaskStorage<F> {
|
||||||
/// on a different executor.
|
/// on a different executor.
|
||||||
pub fn spawn(&'static self, future: impl FnOnce() -> F) -> SpawnToken<impl Sized> {
|
pub fn spawn(&'static self, future: impl FnOnce() -> F) -> SpawnToken<impl Sized> {
|
||||||
if self.spawn_allocate() {
|
if self.spawn_allocate() {
|
||||||
unsafe { self.spawn_initialize(future) }
|
unsafe { SpawnToken::<F>::new(self.spawn_initialize(future)) }
|
||||||
} else {
|
} else {
|
||||||
SpawnToken::new_failed()
|
SpawnToken::<F>::new_failed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,37 +179,11 @@ impl<F: Future + 'static> TaskStorage<F> {
|
||||||
.is_ok()
|
.is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn spawn_initialize<FutFn>(&'static self, future: FutFn) -> SpawnToken<impl Sized>
|
unsafe fn spawn_initialize(&'static self, future: impl FnOnce() -> F) -> NonNull<TaskHeader> {
|
||||||
where
|
|
||||||
FutFn: FnOnce() -> F,
|
|
||||||
{
|
|
||||||
// Initialize the task
|
// Initialize the task
|
||||||
self.raw.poll_fn.write(Self::poll);
|
self.raw.poll_fn.write(Self::poll);
|
||||||
self.future.write(future());
|
self.future.write(future());
|
||||||
|
NonNull::new_unchecked(&self.raw as *const TaskHeader as *mut TaskHeader)
|
||||||
// When send-spawning a task, we construct the future in this thread, and effectively
|
|
||||||
// "send" it to the executor thread by enqueuing it in its queue. Therefore, in theory,
|
|
||||||
// send-spawning should require the future `F` to be `Send`.
|
|
||||||
//
|
|
||||||
// The problem is this is more restrictive than needed. Once the future is executing,
|
|
||||||
// it is never sent to another thread. It is only sent when spawning. It should be
|
|
||||||
// enough for the task's arguments to be Send. (and in practice it's super easy to
|
|
||||||
// accidentally make your futures !Send, for example by holding an `Rc` or a `&RefCell` across an `.await`.)
|
|
||||||
//
|
|
||||||
// We can do it by sending the task args and constructing the future in the executor thread
|
|
||||||
// on first poll. However, this cannot be done in-place, so it'll waste stack space for a copy
|
|
||||||
// of the args.
|
|
||||||
//
|
|
||||||
// Luckily, an `async fn` future contains just the args when freshly constructed. So, if the
|
|
||||||
// args are Send, it's OK to send a !Send future, as long as we do it before first polling it.
|
|
||||||
//
|
|
||||||
// (Note: this is how the generators are implemented today, it's not officially guaranteed yet,
|
|
||||||
// but it's possible it'll be guaranteed in the future. See zulip thread:
|
|
||||||
// https://rust-lang.zulipchat.com/#narrow/stream/187312-wg-async/topic/.22only.20before.20poll.22.20Send.20futures )
|
|
||||||
//
|
|
||||||
// The `FutFn` captures all the args, so if it's Send, the task can be send-spawned.
|
|
||||||
// This is why we return `SpawnToken<FutFn>` below.
|
|
||||||
SpawnToken::<FutFn>::new(NonNull::new_unchecked(&self.raw as *const TaskHeader as _))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn poll(p: NonNull<TaskHeader>) {
|
unsafe fn poll(p: NonNull<TaskHeader>) {
|
||||||
|
@ -261,11 +235,59 @@ impl<F: Future + 'static, const N: usize> TaskPool<F, N> {
|
||||||
pub fn spawn(&'static self, future: impl FnOnce() -> F) -> SpawnToken<impl Sized> {
|
pub fn spawn(&'static self, future: impl FnOnce() -> F) -> SpawnToken<impl Sized> {
|
||||||
for task in &self.pool {
|
for task in &self.pool {
|
||||||
if task.spawn_allocate() {
|
if task.spawn_allocate() {
|
||||||
return unsafe { task.spawn_initialize(future) };
|
return unsafe { SpawnToken::<F>::new(task.spawn_initialize(future)) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SpawnToken::new_failed()
|
SpawnToken::<F>::new_failed()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like spawn(), but allows the task to be send-spawned if the args are Send even if
|
||||||
|
/// the future is !Send.
|
||||||
|
///
|
||||||
|
/// Not covered by semver guarantees. DO NOT call this directly. Intended to be used
|
||||||
|
/// by the Embassy macros ONLY.
|
||||||
|
///
|
||||||
|
/// SAFETY: `future` must be a closure of the form `move || my_async_fn(args)`, where `my_async_fn`
|
||||||
|
/// is an `async fn`, NOT a hand-written `Future`.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub unsafe fn _spawn_async_fn<FutFn>(&'static self, future: FutFn) -> SpawnToken<impl Sized>
|
||||||
|
where
|
||||||
|
FutFn: FnOnce() -> F,
|
||||||
|
{
|
||||||
|
// When send-spawning a task, we construct the future in this thread, and effectively
|
||||||
|
// "send" it to the executor thread by enqueuing it in its queue. Therefore, in theory,
|
||||||
|
// send-spawning should require the future `F` to be `Send`.
|
||||||
|
//
|
||||||
|
// The problem is this is more restrictive than needed. Once the future is executing,
|
||||||
|
// it is never sent to another thread. It is only sent when spawning. It should be
|
||||||
|
// enough for the task's arguments to be Send. (and in practice it's super easy to
|
||||||
|
// accidentally make your futures !Send, for example by holding an `Rc` or a `&RefCell` across an `.await`.)
|
||||||
|
//
|
||||||
|
// We can do it by sending the task args and constructing the future in the executor thread
|
||||||
|
// on first poll. However, this cannot be done in-place, so it'll waste stack space for a copy
|
||||||
|
// of the args.
|
||||||
|
//
|
||||||
|
// Luckily, an `async fn` future contains just the args when freshly constructed. So, if the
|
||||||
|
// args are Send, it's OK to send a !Send future, as long as we do it before first polling it.
|
||||||
|
//
|
||||||
|
// (Note: this is how the generators are implemented today, it's not officially guaranteed yet,
|
||||||
|
// but it's possible it'll be guaranteed in the future. See zulip thread:
|
||||||
|
// https://rust-lang.zulipchat.com/#narrow/stream/187312-wg-async/topic/.22only.20before.20poll.22.20Send.20futures )
|
||||||
|
//
|
||||||
|
// The `FutFn` captures all the args, so if it's Send, the task can be send-spawned.
|
||||||
|
// This is why we return `SpawnToken<FutFn>` below.
|
||||||
|
//
|
||||||
|
// This ONLY holds for `async fn` futures. The other `spawn` methods can be called directly
|
||||||
|
// by the user, with arbitrary hand-implemented futures. This is why these return `SpawnToken<F>`.
|
||||||
|
|
||||||
|
for task in &self.pool {
|
||||||
|
if task.spawn_allocate() {
|
||||||
|
return SpawnToken::<FutFn>::new(task.spawn_initialize(future));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SpawnToken::<FutFn>::new_failed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue