Expand description
maitake
🎶🍄 “Dancing mushroom” — an async runtime construction kit.
✨ as seen in the rust compiler test suite!
what is it?
This library is a collection of modular components for building a Rust
async runtime based on core::task
and core::future
, with a focus on
supporting #![no_std]
projects.
Unlike other async runtime implementations, maitake
does not provide a
complete, fully-functional runtime implementation. Instead, it provides reusable
implementations of common functionality, including a task system,
scheduler, a timer wheel, and synchronization primitives.
These components may be combined with other runtime services, such as timers and
I/O resources, to produce a complete, application-specific async runtime.
maitake
was initially designed for use in the mycelium and mnemOS
operating systems, but may be useful for other projects as well.
Note
This is a hobby project. I’m working on it in my spare time, for my own personal use. I’m very happy to share it with the broader Rust community, and contributions and bug reports are always welcome. However, please remember that I’m working on this library for fun, and if it stops being fun…well, you get the idea.
Anyway, feel free to use and enjoy this crate, and to contribute back as much as you want to!
a tour of maitake
maitake
currently provides the following major API components:
-
maitake::task
: themaitake
task system. This module contains theTask
type, representing an asynchronous task (aFuture
that can be spawned on the runtime), and theTaskRef
type, a reference-counted, type-erased pointer to a spawnedTask
.Additionally, it also contains other utility types for working with tasks. These include the
JoinHandle
type, which can be used to await the output of a task once it has been spawned, and thetask::Builder
type, for configuring a task prior to spawning it. -
maitake::scheduler
: schedulers for executing tasks. In order to actually execute asynchronous tasks, one or more schedulers is required. This module contains theScheduler
andStaticScheduler
types, which implement task schedulers, and utilities for constructing and using schedulers. -
maitake::time
: timers and futures for tracking time. This module contains tools for waiting for time-based events in asynchronous systems. It provides theSleep
type, aFuture
which completes after a specified duration, and theTimeout
type, which wraps anotherFuture
and cancels it if it runs for longer than a specified duration without completing.In order to use these futures, a system must have a timer. The
maitake::time
module therefore provides theTimer
type, a hierarchical timer wheel which can track and notify a large number of time-based futures efficiently. ATimer
must be driven by a hardware time source, such as an interrupt or timestamp counter. -
maitake::sync
: asynchronous synchronization primitives. This module provides asynchronous implementations of common synchronization primitives, including aMutex
,RwLock
, andSemaphore
. Additionally, it provides lower-level synchronization types which may be useful when implementing custom synchronization strategies. -
maitake::future
: utility futures. This module provides general-purpose utilityFuture
types that may be used without the Rust standard library.
usage considerations
maitake
is intended primarily for use in bare-metal projects, such as
operating systems, operating system components, and embedded systems. These
bare-metal systems typically do not use the Rust standard library, so maitake
supports #![no_std]
by default, and the use of liballoc
is feature-flagged
for systems where liballoc
is unavailable.
This intended use case has some important implications:
maitake
is not a complete asynchronous runtime
This is in contrast to other async runtimes, like tokio
, async-std
, and
glommio
, which provide everything a userspace application needs to run
async tasks and perform asynchronous IO, with lower-level implementation
details encapsulated behind the runtime’s API. In the bare-metal systems
maitake
is intended for use in, however, it is often necessary to have more
direct control over lower-level implementation details of the runtime.
For example: in an asynchronous runtime, tasks must be stored in non-stack
memory. Runtimes like tokio
and async-std
use the standard library’s
allocator and Box
type to allocate tasks on the heap. In bare-metal systems,
though, liballoc
’s heap allocator may not be available. Such a system may
have no ability to perform dynamic heap allocations, or may implement its own
allocator which may not be compatible with liballoc
.
maitake
is designed to still be usable in those cases — even a system
which cannot dynamically allocate memory could use maitake
in order to
create and schedule a fixed set of tasks that are stored in 'static
s, essentially
allocating tasks at compile-time. Therefore, maitake
provides an
interface for overriding the memory container in which tasks are
stored. In order to provide such an interface, however, maitake
must expose
the in-memory representation of spawned tasks, which other runtimes
typically do not make part of their public APIs.
maitake
does not support unwinding
Rust supports multiple modes of handling panics: panic="abort"
and
panic="unwind"
. When a program is compiled with panic="unwind"
, panics are
handled by unwinding the stack of the panicking thread. This allows the use of
APIs like catch_unwind
, which allows panics to be handled without
terminating the entire program. On the other hand, compiling with
panic="abort"
means that all panics immediately terminate the program.
Bare-metal systems typically do not use stack unwinding. For programs which use
the Rust standard library, support for unwinding is provided by std
. However,
in bare-metal, #![no_std]
systems, it is necessary for the system to implement
its own unwinding system. Therefore, maitake
does not support unwinding.
This is important to note, because supporting unwinding imposes additional
safety considerations. In order to safely support unwinding, many
parts of maitake
, such as runtime internals and synchronization primitives,
would have to take extra steps to ensure that they cannot be left in an invalid
state during unwinding. Ensuring unwind-safety would require the use of standard
library APIs that are not available without std
, so maitake
does not ensure
unwind-safety.
This means that maitake
should not be used in programs compiled with
panic="unwind"
. Typically, no bare-metal program will fall into this category,
but if you are using maitake
in a project which uses std
, it is necessary to
explicitly disable unwinding in that project’s Cargo.toml
.
platform support
In general, maitake
is a platform-agnostic library. It does not interact
directly with the underlying hardware, or use platform-specific features (with
one small exception). Instead, maitake
provides portable implementations of
core runtime components. In some cases, such as the timer wheel,
downstream code must integrate maitake
’s APIs with hardware-specific code for
in order to use them effectively.
support for atomic operations
However, one aspect of maitake
’s implementation may differ slightly across
different target architectures: maitake
relies on atomic operations integers.
Sometimes, atomic operations on integers of specific widths are needed (e.g.,
AtomicU64
), which may not be available on all architectures.
In order to work on architectures which lack atomic operations on 64-bit
integers, maitake
uses the portable-atomic
crate by Taiki Endo. This crate
crate polyfills atomic operations on integers larger than the platform’s pointer
width, when these are not supported in hardware.
In most cases, users of maitake
don’t need to be aware of maitake
’s use of
portable-atomic
. If compiling maitake
for a target architecture that has
native support for 64-bit atomic operations (such as x86_64
or aarch64
), the
native atomics are used automatically. Similarly, if compiling maitake
for any
target that has atomic compare-and-swap operations on any size integer, but
lacks 64-bit atomics (i.e., 32-bit x86 targets like i686
, or 32-bit ARM
targets with atomic operations), the portable-atomic
polyfill is used
automatically. Finally, when compiling for target architectures which lack
atomic operations because they are always single-core, such as MSP430 or AVR
microcontrollers, portable-atomic
simply uses unsynchronized operations with
interrupts temporarily disabled.
The only case where the user must be aware of portable-atomic
is when
compiling for targets which lack atomic operations but are not guaranteed to
always be single-core. This includes ARMv6-M (thumbv6m
), pre-v6 ARM (e.g.,
thumbv4t
, thumbv5te
), and RISC-V targets without the A extension. On these
architectures, the user must manually enable the RUSTFLAGS
configuration
--cfg portable_atomic_unsafe_assume_single_core
if (and only
if) the specific target hardware is known to be single-core. Enabling this cfg
is unsafe, as it will cause unsound behavior on multi-core systems using these
architectures.
Additional configurations for some single-core systems, which determine the
specific sets of interrupts that portable-atomic
will disable when entering a
critical section, are described here.
overriding blocking mutex implementations
In addition to async locks, maitake::sync
also provides a blocking
module, which contains blocking blocking::Mutex
and blocking::RwLock
types. Many of maitake::sync
’s async synchronization primitives, including
WaitQueue
, Mutex
, RwLock
, and Semaphore
, internally use the
blocking::Mutex
type for wait-list synchronization. By default, this type
uses a blocking::DefaultMutex
as the underlying mutex
implementation, which attempts to provide the best generic mutex implementation
based on the currently enabled feature flags.
However, in some cases, it may be desirable to provide a custom mutex
implementation. Therefore, maitake::sync
’s blocking::Mutex
type, and the
async synchronization primitives that depend on it, are generic over a Lock
type parameter which may be overridden using the RawMutex
and
ScopedRawMutex
traits from the mutex-traits
crate, allowing alternative
blocking mutex implementations to be used with maitake::sync
. Using the
mutex-traits
adapters in the mutex
crate, maitake::sync
’s types may
also be used with raw mutex implementations that implement traits from the
lock_api
and critical-section
crates.
See the documentation on overriding mutex implementations for more details.
features
The following features are available (this list is incomplete; you can help by expanding it.)
Feature | Default | Explanation |
---|---|---|
alloc | true | Enables liballoc dependency |
std | false | Enables the Rust standard library, disabling #![no-std] . When std is enabled, the DefaultMutex type will use std::sync::Mutex . This implies the alloc feature. |
| critical-section
| false
| Enables support for the critical-section
crate. This includes a variant of the DefaultMutex
type that uses a critical section, as well as the portable-atomic
crate’s critical-section
feature (as discussed above) |
| no-cache-pad
| false
| Inhibits cache padding for the CachePadded
struct. When this feature is NOT enabled, the size will be determined based on target platform. |
| tracing-01
| false
| Enables support for v0.1.x of tracing
(the current release version). Requires liballoc
.|
| tracing-02
| false
| Enables support for the upcoming v0.2 of tracing
(via a Git dependency). |
| core-error
| false
| Enables implementations of the core::error::Error
trait for maitake
’s error types. Requires a nightly Rust toolchain. |
Modules
- Schedulers for executing tasks.
- Asynchronous synchronization primitives
- The
maitake
task system. - Utilities for tracking time and constructing system timers.
Macros
- Safely constructs a new
StaticScheduler
instance in astatic
initializer.