With heap allocations Out Of Memory is always a possibility and can occur in any place where a collection may need to grow: for example, all alloc::Vec.push invocations can potentially generate an OOM condition. Thus some operations can implicitly fail. Some alloc collections expose try_reserve methods that let you check for potential OOM conditions when growing the collection but you need be proactive about using them.
If you exclusively use heapless collections and you don't use a memory allocator for anything else then an OOM condition is impossible. Instead, you'll have to deal with collections running out of capacity on a case by case basis. That is you'll have deal with all the Results returned by methods like Vec.push.
OOM failures can be harder to debug than say unwrap-ing on all Results returned by heapless::Vec.push because the observed location of failure may not match with the location of the cause of the problem. For example, even vec.reserve(1) can trigger an OOM if the allocator is nearly exhausted because some other collection was leaking memory (memory leaks are possible in safe Rust).
Reasoning about memory usage of heap allocated collections is hard because the capacity of long lived collections can change at runtime. Some operations may implicitly reallocate the collection increasing its memory usage, and some collections expose methods like shrink_to_fit that can potentially reduce the memory used by the collection -- ultimately, it's up to the allocator to decide whether to actually shrink the memory allocation or not. Additionally, the allocator may have to deal with memory fragmentation which can increase the apparent memory usage.
On the other hand if you exclusively use fixed capacity collections, store most of them in static variables and set a maximum size for the call stack then the linker will detect if you try to use more memory than what's physically available.
Furthermore, fixed capacity collections allocated on the stack will be reported by -Z emit-stack-sizes flag which means that tools that analyze stack usage (like stack-sizes) will include them in their analysis.
However, fixed capacity collections can not be shrunk which can result in lower load factors (the ratio between the size of the collection and its capacity) than what relocatable collections can achieve.
If you are building time sensitive applications or hard real time applications then you care, maybe a lot, about the worst case execution time of the different parts of your program.
The alloc collections can reallocate so the WCET of operations that may grow the collection will also include the time it takes to reallocate the collection, which itself depends on the runtime capacity of the collection. This makes it hard to determine the WCET of, for example, the alloc::Vec.push operation as it depends on both the allocator being used and its runtime capacity.
On the other hand fixed capacity collections never reallocate so all operations have a predictable execution time. For example, heapless::Vec.push executes in constant time.
alloc requires setting up a global allocator whereas heapless does not. However, heapless requires you to pick the capacity of each collection that you instantiate.
The alloc API will be familiar to virtually every Rust developer. The heapless API tries to closely mimic the alloc API but it will never be exactly the same due to its explicit error handling -- some developers may feel the explicit error handling is excessive or too cumbersome.
This chapter aims to collect various useful design patterns for embedded Rust.
This is a set of common and recommended patterns for writing hardware abstraction layers (HALs) for microcontrollers in Rust. These patterns are intended to be used in addition to the existing Rust API Guidelines when writing HALs for microcontrollers.
• Naming
• GPIO
• Naming (crate aligns with Rust naming conventions)
• The crate is named appropriately (C-CRATE-NAME)
• Interoperability (crate interacts nicely with other library functionality)
• Wrapper types provide a destructor method (C-FREE)
• HALs reexport their register access crate (C-REEXPORT-PAC)
• Types implement the embedded-hal traits (C-HAL-TRAITS)
• Predictability (crate enables legible code that acts how it looks)
• Constructors are used instead of extension traits (C-CTOR)
• GPIO Interfaces (GPIO Interfaces follow a common pattern)
• Pin types are zero-sized by default (C-ZST-PIN)
• Pin types provide methods to erase pin and port (C-ERASED-PIN)
• Pin state should be encoded as type parameters (C-PIN-STATE)
HAL crates should be named after the chip or family of chips they aim to support. Their name should end with -hal to distinguish them from register access crates. The name should not contain underscores (use dashes instead).