#async-executor #executor #async #borrowing #async-channel

no-std edge-executor

Async executor suitable for embedded environments

7 unstable releases

0.4.1 Nov 9, 2023
0.4.0 Oct 18, 2023
0.3.1 Aug 20, 2023
0.3.0 Oct 17, 2022
0.1.1 Jul 30, 2022

#193 in Embedded development

Download history 1111/week @ 2023-12-06 1150/week @ 2023-12-13 479/week @ 2023-12-20 346/week @ 2023-12-27 1280/week @ 2024-01-03 1032/week @ 2024-01-10 895/week @ 2024-01-17 1284/week @ 2024-01-24 1305/week @ 2024-01-31 1608/week @ 2024-02-07 1542/week @ 2024-02-14 2461/week @ 2024-02-21 1616/week @ 2024-02-28 1918/week @ 2024-03-06 1408/week @ 2024-03-13 760/week @ 2024-03-20

5,995 downloads per month
Used in 4 crates (via rbd_dimmer)

MIT/Apache

20KB
186 lines

edge-executor

CI crates.io Documentation

This crate ships a minimal async executor suitable for microcontrollers and embedded systems in general.

A no_std drop-in replacement for smol's async-executor, with the implementation being a thin wrapper around smol's async-task as well.

Examples

// ESP-IDF example, local execution, local borrows.
// With STD enabled, you can also just use `edge_executor::block_on` 
// instead of `esp_idf_svc::hal::task::block_on`.

use edge_executor::LocalExecutor;
use esp_idf_svc::hal::task::block_on;

fn main() {
    let local_ex: LocalExecutor = Default::default();

    // Borrowed by `&mut` inside the future spawned on the executor
    let mut data = 3;

    let data = &mut data;

    let task = local_ex.spawn(async move {
        *data += 1;

        *data
    });

    let res = block_on(local_ex.run(async { task.await * 2 }));

    assert_eq!(res, 8);
}
// STD example, work-stealing execution.

use async_channel::unbounded;
use easy_parallel::Parallel;

use edge_executor::{Executor, block_on};

fn main() {
    let ex: Executor = Default::default();
    let (signal, shutdown) = unbounded::<()>();

    Parallel::new()
        // Run four executor threads.
        .each(0..4, |_| block_on(ex.run(shutdown.recv())))
        // Run the main future on the current thread.
        .finish(|| block_on(async {
            println!("Hello world!");
            drop(signal);
        }));
}
// WASM example.

use log::{info, Level};

use edge_executor::LocalExecutor;

use static_cell::StaticCell;
use wasm_bindgen_futures::spawn_local;

use gloo_timers::future::TimeoutFuture;

static LOCAL_EX: StaticCell<LocalExecutor> = StaticCell::new();

fn main() {
    console_log::init_with_level(Level::Info).unwrap();

    // Local executor (futures can be `!Send`) yet `'static`
    let local_ex = &*LOCAL_EX.init(Default::default());

    local_ex
        .spawn(async {
            loop {
                info!("Tick");
                TimeoutFuture::new(1000).await;
            }
        })
        .detach();

    spawn_local(local_ex.run(core::future::pending::<()>()));
}

Highlights

  • no_std (but does need alloc):
    • The executor uses allocations in a controlled way: only when a new task is being spawn, as well as during the construction of the executor itself;
    • For a no_std and "no_alloc" executor, look at embassy-executor, which statically pre-allocates all tasks.
  • Works on targets which have no core::sync::atomic support, thanks to portable-atomic;
  • Does not assume an RTOS and can run completely bare-metal too;
  • Lockless, atomic-based, bounded task queue by default, which works well for waking the executor directly from an ISR on e.g. FreeRTOS or ESP-IDF (unbounded also an option with feature unbounded, yet that might mean potential allocations in an ISR context, which should be avoided).

Great features carried over from async-executor:

  • Stack borrows: futures spawned on the executor need to live only as long as the executor itself. No F: Future + 'static constraints;
  • Completely portable and async. Executor::run simply returns a Future. Polling this future runs the executor, i.e. block_on(executor.run(core::future:pending::<()>()));
  • const new constructor function.

NOTE: To compile on no_std targets that do not have atomics in Rust core (i.e. riscv32imc-unknown-none-elf and similar single-core MCUs), enable features portable-atomic and critical-section. I.e.:

cargo build --features portable-atomic,critical-section --no-default-features --target <your-target>

Dependencies

~0.5–1MB
~16K SLoC