async-io-mini

Crates.ioasync-io-mini
lib.rsasync-io-mini
version0.2.0
sourcesrc
created_at2024-09-10 14:01:47.365907
updated_at2024-09-10 14:01:47.365907
descriptionAsync I/O fork for embedded systems
homepage
repositoryhttps://github.com/ivmarkov/async-io-mini
max_upload_size
id1370402
size104,351
(ivmarkov)

documentation

README

async-io-mini

CI License Cargo Documentation

Async I/O and timers for MCUs.

This crate is a fork of the splendid async-io crate targetting MCUs and ESP-IDF in particular.

How to use?

async-io-mini is a drop-in, API-compatible replacement for the Async and Timer types from async-io.

So either:

  • Just replace all use async_io occurances in your crate with use async_io_mini
  • Or - in your Cargo.toml - replace:
    • async-io = "..."
    • with async-io = { package = "async-io-mini", ... }

Justification

While async-io supports a ton of operating systems - including ESP-IDF for the Espressif MCU chips - it does have a non-trivial memory consumption in the hidden thread named async-io. Since its hidden Reactor object is initialized lazily, it so happens that it is first allocated on-stack, and then it is moved into the static context. This requires the async-io thread (as well as any thread from where you are polling sockets) to have at least 8K stack, which - by MCU standards! - is relatively large if you are memory-constrained.

In contrast, async-io-mini:

  • Needs < 3K of stack with ESP-IDF (and that's only because ESP-IDF interrupts are executed on the stack of the interrupted thread, i.e. we need to leave some room for these);
  • It's reactor is allocated to the static context eagerly as its constructor function is const (hence no stack blowups);
  • The reactor has a smaller memory footprint too (~ 500 bytes), as it is hard-coded to the select syscall and does not support timers. MCUs (with lwIP) usually have max file and socket handles in the lower tens (~ 20 in ESP-IDF) so all structures can be limited to that size;
  • No heap allocations - initially and during polling.

Further, async-io has a non-trivial set of dependencies (again - for MCUs; for regular OSes it is a dwarf by any meaningful measurement!): rustix, polling, async-lock, event, tracing, parking-lot and more. Nothing wrong with with that per-se, but that's a large implementation surface that e.g. recently is triggering a possible miscompilation on Espressif xtensa targets (NOT that this is a justification not to root-cause and fix the problem!).

async-io-mini only has the following non-optional dependencies:

  • libc (which indirectly comes with Rust STD anyway);
  • heapless (for heapless::Vec and nothing else);
  • log (might become optional);
  • enumset (not crucial, might remove).

Enhancements

The Timer type of async_io_mini is based on the embassy-time crate, and as such should offer a higher resolution on embedded operating systems like the ESP-IDF than what can be normally achieved by implementing timers using the timeout parameter of the select syscall (as async-io does).

The reason for this is that on the ESP-IDF, the timeout parameter of select provides a resolution of 10ms (one FreeRTOS sys-tick), while embassy-time is implemented using the ESP-IDF Timer service, which provides resolutions up to 1 microsecond.

With that said, for greenfield code that does not need to be compatible with async-io, use the native embassy_time::Timer and embassy_time::Ticker rather than async_io_mini::Timer, because the latter has a larger memory footprint (40 bytes on 32bit archs) compared to the embassy-time types (8 and 16 bytes each).

Limitations

No equivalent of async_io::block_on

Implementing socket polling as a shared task between the hidden async-io-mini thread and the thread calling async_io_mini::block_on is not trivial and probably not worth it on MCUs. Just use futures_lite::block_on or the block_on equivalent for your OS (i.e. esp_idf_svc::hal::task::block_on for the ESP-IDF).

Implementation

Async

The first time Async is used, a thread named async-io-mini will be spawned. The purpose of this thread is to wait for I/O events reported by the operating system, and then wake appropriate futures blocked on I/O when they can be resumed.

To wait for the next I/O event, the "async-io-mini" thread uses the select syscall, and is thus only useful for MCUs (might just be the ESP-IDF) where the number of file or socket handles is very small anyway.

Timer

As per above, the Timer type is a wrapper around the functionality provided by the embassy-time crate.

Examples

Connect to example.com:80, or time out after 10 seconds.

use async_io_mini::{Async, Timer};
use futures_lite::{future::FutureExt, io};

use std::net::{TcpStream, ToSocketAddrs};
use std::time::Duration;

let addr = "example.com:80".to_socket_addrs()?.next().unwrap();

let stream = Async::<TcpStream>::connect(addr).or(async {
    Timer::after(Duration::from_secs(10)).await;
    Err(io::ErrorKind::TimedOut.into())
})
.await?;

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Commit count: 0

cargo fmt