Skip to content

Support for statically allocating tasks (no_alloc usage) #7

@Dirbaio

Description

@Dirbaio

Motivation

It is currently possible to use async_task with no_std, but it requires having alloc available because tasks are dynamically allocated.

In memory-constrained environments, such as embedded microcontroller devices, it's often useful to statically declare everything and not have any dynamic allocator. The main advantage is that you have compile-time guarantees that your program will never run out of RAM (The linker knows how much RAM the target device has, and will error if all the static variables won't fit).

Generally in an embedded device the following kinds of tasks are present:

  • tasks that start up at boot and run forever
  • tasks that are started and stopped, but never run multiple instances concurrently
  • tasks that run a bounded number of concurrent instances (usually small, 4-16 instances)

It is very rare that you want an arbitrary number of tasks of arbitrary types mixed together. You rarely have enough RAM for it, and it tends to cause fragmentation problems and unpredictable out of RAM errors.

It would be a huge boon for embedded if async_task allowed statically pre-allocating tasks.

Aditionally, this would be especially useful combined with #![feature(type_alias_impl_trait)] (rust-lang/rust#63063), which makes it possible to name the future types of async fns. (naming the type is needed so the user can declare the static variable containing the task)

API proposal

The API could be something like this

// maybe R is not needed
pub struct StaticTask<F, R, S, T> { /* storage for a raw task */ } 

impl StaticTask<F, R, S, T>
where
    F: Future<Output = R> + Send + 'static,
    R: Send + 'static,
    S: Fn(Task<T>) + Send + Sync + 'static,
    T: Send + Sync + 'static,
{
    // create a new StaticTask in "free" state
    // The bit pattern of the return value must be only zero bits and uninitialized bits, so
    // StaticTasks can be placed in the .bss section (otherwise they'd go into .data which wastes flash space)
    pub const fn new() -> Self { ... }

    // If self is in "free" state, change it to "used" state and initialize it with the given future, and return the task and joinhandle.
    // if self is in "used" state, return None.
    pub fn spawn<F, R, S, T>(&'static self, future: F, schedule: S, tag: T) -> Option<(Task<T>, JoinHandle<R, T>)> { .. } 
}

This would be used like this

static MY_TASK: StaticTask<MyFuture, MyFuture::Output, ??, ()> = StaticTask::new()

fn main() {
   if let Some(t, j) = MY_TASK.spawn(my_do_something(), |t| { /* schedule t)}, ()) {
      t.schedule()
   } else {
      // spawn failed because the static task is already running, return some error
   }
}

When the task is no longer running (ie when it would be freed if it was dynamically allocated), the StaticTask is returned to "free" state, so it can be used by .spawn() again.

This would make it possible to mix statically-allocated and dynamically-allocated tasks in the same executor.

Having so many generic arguments in StaticTask is somewhat ugly because the user has to manually specify them, but this is something executor libraries could abstract (ie export a newtype so you only have to set F). A higher-level executor API could be like this:

static MY_TASK: my_executor::Task<MyFuture> = my_executor::Task::new()

fn main() {
  // dynamically allocate
  my_executor::spawn(my_do_something());

  // statically allocate
  MY_TASK.spawn(my_do_something());

  my_executor::run()
}

Alternatives

Add an API where the user can specify a custom allocator for spawning, via some trait. Still, th library would still have to export a type so that user code can know what's the size required for a RawTask of a given future, so they can statically allocate buffers of the right size.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions