embassy
embassy copied to clipboard
Support generic tasks or improve documentation on why it's not possible (and perhaps suggest workarounds)
I was looking at #1837 which didn't really have a resolution, in my mind. So this feature request comes from a bit of ignorance as I don't know why this wouldn't be possible. The best work around for now seems to be to generate tasks using a macro.
What I basically want is the ability to hand a task any device like Spi, Pwm Uart, etc. It seems to me like the proc macro is making the function generic (on nightly) behind the scenes using RPIT, so some level of genericness seems possible at least? Based on the mentioned issue it seems like it was possible to bypass the macro check using TAIT in arguments to make it compile as well.
If there is a fundamental blocker I think it would be very helpful to have some documentation on how this can be worked around.
On a separate note, I've been trying out Embassy for a small project at work and it has been a breath of fresh air for embedded development. :+1:
I'm new to this project as well but I recently had the same thought (and would like to double check my reasoning!) so thought I'd share my understanding.
An async executor needs to be able to know about the tasks that it is responsible for polling. Additionally those tasks need somewhere to store their in-progress state. In embassy (on stable) the latter is achieved as follows -
- An async function undergoes compiler magic to transform it into an unnamed type we'll call
F, which implementsFuture. This type encompasses the in-progress state of the function and so its size depends directly on the types used by the function. - To use an async function with embassy, a task is created using the
embassy_executor::taskannotation macro. It's implementation wraps the annotated function in a new function (accepting the same arguments). - The wrapper function additionally declares a local
staticvariable which is a referenceTaskPoolRefto theTaskPool<F>for the original function. - When invoked the wrapper uses this reference to create a task by adding a closure, capturing the wrapped function and the invocation arguments, to the pool and returning a reference
TaskRef(SpawnTokenis a newtype wrapping aTaskReffor a few reasons inconsequential to this explanation).
The crux of the generics issue is that (source) -
A static item defined in a generic scope (for example in a blanket or default implementation) will result in exactly one static item being defined, as if the static definition was pulled out of the current scope into the module. There will not be one item per monomorphization.
The problematic code is happening across steps 3 & 4 above. Breaking it down -
- The generically typed
TaskPool<F>is lazily allocated on first access byTaskPoolRefusing anArena. - The allocation size if based on the inferred (i.e.
_) type ofFfor this first (monomorhpized) instance. - Subsequent access to the
TaskPool<F>is viaTaskPoolRefwhich unsafely casts to the inferred type ofFfor this (monomorhpized) instance.
This works fine assuming there is only one F - the first (monomorhpized) instance is thereby guaranteed to be the only (monomorhpized) instance. Embassy enforces this invariant by preventing generic types (along with other checks e.g. no variadics).
If this invariant wasn't there, these different (monomorhpized) instances could potentially require different amounts of memory to store and/or be storing different types and/or in a different order. Thereby resulting in undefined behaviour after the unsafe cast.
On nightly, TAIT allows F to be named as Fut and used in a static type, thereby delegating responsibility for upholding the invariant to the compiler. This also eliminates the need for TaskPoolRef because TaskPool<F> can be statically defined as TaskPool<Fut>.
TLDR; with or without TAIT you somehow need to ensure there is a static TaskPool<F> for each F, meaning you need to define a task per F.
Here's an example of how this invariant could be lifted in a std context. However, applying that here would require some form of runtime allocation with the typical problems that would involve.
An outstanding question I had was - does nightly's use of TAIT allow the explicit generic check to be removed? Quickly butchering the macro code and hacking the tests suggests it should work although the error is obtuse so I can see the benefit of leaving it in place -
#[task]
async fn task1<T>(trace: T) where T: Tracer + 'static {
trace.push("poll task1")
}
error: type parameter `T` is part of concrete type but not used in parameter list for the `impl Trait` type alias
--> tests/test.rs:65:5
|
65 | #[task]
| ^^^^^^^
|
= note: this error originates in the attribute macro `task` (in Nightly builds, run with -Z macro-backtrace for more info)
I'm new to the Rust, and stumbled at similar problem. I found out that passing impl with 'static often works.
Example GPIO task (with Flex that supports both reads and writes
#[embassy_executor::task]
async fn touch_task(
mut flex: Flex<'static, impl Pin + 'static>,
...
)
and PWM
#[embassy_executor::task]
async fn pwm_task(
mut pwm: Pwm<'static, impl embassy_rp::pwm::Channel>,
...
) {
...
}
Interesting, I hadn't tried that. I think it's an embassy bug though.
Based on my reasoning above, I would expect that the block on generics is meant to extend to impl T. No matter whether it's qualified with 'static (I understand this to just disallow implementations that are or contain non-static references), nor its nested depth in the argument type (e.g. impl T, T<impl U>, etc.). However, I can't see any checks for it.
I think it's just happening to work in this case because the types implementing the Pin/Channel trait happen to have a compatible memory layout (read as - undefined behaviour). Or possibly you're only using a single implementation in your binary? Not sure...
oh yes this is definitely a bug!