esp_alloc/lib.rs
1//! A `no_std` heap allocator for RISC-V and Xtensa processors from
2//! Espressif. Supports all currently available ESP32 devices.
3//!
4//! **NOTE:** using this as your global allocator requires using Rust 1.68 or
5//! greater, or the `nightly` release channel.
6//!
7//! # Using this as your Global Allocator
8//!
9//! ```rust,no_run
10//! use esp_alloc as _;
11//!
12//! fn init_heap() {
13//! const HEAP_SIZE: usize = 32 * 1024;
14//! static mut HEAP: MaybeUninit<[u8; HEAP_SIZE]> = MaybeUninit::uninit();
15//!
16//! unsafe {
17//! esp_alloc::HEAP.add_region(esp_alloc::HeapRegion::new(
18//! HEAP.as_mut_ptr() as *mut u8,
19//! HEAP_SIZE,
20//! esp_alloc::MemoryCapability::Internal.into(),
21//! ));
22//! }
23//! }
24//! ```
25//!
26//! Alternatively, you can use the `heap_allocator!` macro to configure the
27//! global allocator with a given size:
28//!
29//! ```rust,no_run
30//! esp_alloc::heap_allocator!(size: 32 * 1024);
31//! ```
32//!
33//! # Using this with the nightly `allocator_api`-feature
34//!
35//! Sometimes you want to have more control over allocations.
36//!
37//! For that, it's convenient to use the nightly `allocator_api`-feature,
38//! which allows you to specify an allocator for single allocations.
39//!
40//! **NOTE:** To use this, you have to enable the crate's `nightly` feature
41//! flag.
42//!
43//! Create and initialize an allocator to use in single allocations:
44//!
45//! ```rust,no_run
46//! static PSRAM_ALLOCATOR: esp_alloc::EspHeap = esp_alloc::EspHeap::empty();
47//!
48//! fn init_psram_heap() {
49//! unsafe {
50//! PSRAM_ALLOCATOR.add_region(esp_alloc::HeapRegion::new(
51//! psram::psram_vaddr_start() as *mut u8,
52//! psram::PSRAM_BYTES,
53//! esp_alloc::MemoryCapability::External.into(),
54//! ));
55//! }
56//! }
57//! ```
58//!
59//! And then use it in an allocation:
60//!
61//! ```rust,no_run
62//! let large_buffer: Vec<u8, _> = Vec::with_capacity_in(1048576, &PSRAM_ALLOCATOR);
63//! ```
64//!
65//! Alternatively, you can use the `psram_allocator!` macro to configure the
66//! global allocator to use PSRAM:
67//!
68//! ```rust,no_run
69//! let p = esp_hal::init(esp_hal::Config::default());
70//! esp_alloc::psram_allocator!(p.PSRAM, esp_hal::psram);
71//! ```
72//!
73//! You can also use the `ExternalMemory` allocator to allocate PSRAM memory
74//! with the global allocator:
75//!
76//! ```rust,no_run
77//! let p = esp_hal::init(esp_hal::Config::default());
78//! esp_alloc::psram_allocator!(p.PSRAM, esp_hal::psram);
79//!
80//! let mut vec = Vec::<u32>::new_in(esp_alloc::ExternalMemory);
81//! ```
82//!
83//! ## `allocator_api` feature on stable Rust
84//!
85//! `esp-alloc` implements the allocator trait from [`allocator_api2`], which
86//! provides the nightly-only `allocator_api` features in stable Rust. The crate
87//! contains implementations for `Box` and `Vec`.
88//!
89//! To use the `allocator_api2` features, you need to add the crate to your
90//! `Cargo.toml`. Note that we do not enable the `alloc` feature by default, but
91//! you will need it for the `Box` and `Vec` types.
92//!
93//! ```toml
94//! allocator-api2 = { version = "0.3", default-features = false, features = ["alloc"] }
95//! ```
96//!
97//! With this, you can use the `Box` and `Vec` types from `allocator_api2`, with
98//! `esp-alloc` allocators:
99//!
100//! ```rust,no_run
101//! let p = esp_hal::init(esp_hal::Config::default());
102//! esp_alloc::heap_allocator!(size: 64000);
103//! esp_alloc::psram_allocator!(p.PSRAM, esp_hal::psram);
104//!
105//! let mut vec: Vec<u32, _> = Vec::new_in(esp_alloc::InternalMemory);
106//!
107//! vec.push(0xabcd1234);
108//! assert_eq!(vec[0], 0xabcd1234);
109//! ```
110//!
111//! Note that if you use the nightly `allocator_api` feature, you can use the
112//! `Box` and `Vec` types from `alloc`. `allocator_api2` is still available as
113//! an option, but types from `allocator_api2` are not compatible with the
114//! standard library types.
115//!
116//! # Heap stats
117//!
118//! You can also get stats about the heap usage at anytime with:
119//!
120//! ```rust,no_run
121//! let stats: HeapStats = esp_alloc::HEAP.stats();
122//! // HeapStats implements the Display and defmt::Format traits, so you can
123//! // pretty-print the heap stats.
124//! println!("{}", stats);
125//! ```
126//!
127//! Example output:
128//!
129//! ```txt
130//! HEAP INFO
131//! Size: 131068
132//! Current usage: 46148
133//! Max usage: 46148
134//! Total freed: 0
135//! Total allocated: 46148
136//! Memory Layout:
137//! Internal | ████████████░░░░░░░░░░░░░░░░░░░░░░░ | Used: 35% (Used 46148 of 131068, free: 84920)
138//! ```
139//! ## Feature Flags
140#![doc = document_features::document_features!(feature_label = r#"<span class="stab portability"><code>{feature}</code></span>"#)]
141#![no_std]
142#![cfg_attr(feature = "nightly", feature(allocator_api))]
143#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/46717278")]
144
145mod allocators;
146mod heap;
147mod macros;
148#[cfg(feature = "compat")]
149mod malloc;
150
151use core::{
152 alloc::{GlobalAlloc, Layout},
153 fmt::Display,
154 ptr::{self, NonNull},
155};
156
157/// Re-exported crates.
158pub mod export {
159 pub use enumset;
160}
161
162pub use allocators::*;
163use enumset::{EnumSet, EnumSetType};
164use esp_sync::NonReentrantMutex;
165
166use crate::heap::Heap;
167
168/// The global allocator instance
169#[cfg_attr(feature = "global-allocator", global_allocator)]
170pub static HEAP: EspHeap = EspHeap::empty();
171
172#[cfg(feature = "alloc-hooks")]
173unsafe extern "Rust" {
174 fn _esp_alloc_alloc(heap: &EspHeap, caps: EnumSet<MemoryCapability>, ptr: usize, size: usize);
175 fn _esp_alloc_dealloc(heap: &EspHeap, ptr: usize, size: usize);
176}
177
178const BAR_WIDTH: usize = 35;
179
180fn write_bar(f: &mut core::fmt::Formatter<'_>, usage_percent: usize) -> core::fmt::Result {
181 let used_blocks = BAR_WIDTH * usage_percent / 100;
182 (0..used_blocks).try_for_each(|_| write!(f, "█"))?;
183 (used_blocks..BAR_WIDTH).try_for_each(|_| write!(f, "░"))
184}
185
186#[cfg(feature = "defmt")]
187fn write_bar_defmt(fmt: defmt::Formatter, usage_percent: usize) {
188 let used_blocks = BAR_WIDTH * usage_percent / 100;
189 (0..used_blocks).for_each(|_| defmt::write!(fmt, "█"));
190 (used_blocks..BAR_WIDTH).for_each(|_| defmt::write!(fmt, "░"));
191}
192
193#[derive(EnumSetType, Debug)]
194/// Describes the properties of a memory region
195pub enum MemoryCapability {
196 /// Memory must be internal; specifically it should not disappear when
197 /// flash/spiram cache is switched off
198 Internal,
199 /// Memory must be in SPI RAM
200 External,
201}
202
203/// Stats for a heap region
204#[derive(Debug)]
205pub struct RegionStats {
206 /// Total usable size of the heap region in bytes.
207 pub size: usize,
208
209 /// Currently used size of the heap region in bytes.
210 pub used: usize,
211
212 /// Free size of the heap region in bytes.
213 pub free: usize,
214
215 /// Capabilities of the memory region.
216 pub capabilities: EnumSet<MemoryCapability>,
217}
218
219impl Display for RegionStats {
220 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
221 let usage_percent = self.used * 100 / self.size;
222
223 // Display Memory type
224 if self.capabilities.contains(MemoryCapability::Internal) {
225 write!(f, "Internal")?;
226 } else if self.capabilities.contains(MemoryCapability::External) {
227 write!(f, "External")?;
228 } else {
229 write!(f, "Unknown")?;
230 }
231
232 write!(f, " | ")?;
233
234 write_bar(f, usage_percent)?;
235
236 write!(
237 f,
238 " | Used: {}% (Used {} of {}, free: {})",
239 usage_percent, self.used, self.size, self.free
240 )
241 }
242}
243
244#[cfg(feature = "defmt")]
245#[allow(clippy::if_same_then_else)]
246impl defmt::Format for RegionStats {
247 fn format(&self, fmt: defmt::Formatter<'_>) {
248 let usage_percent = self.used * 100 / self.size;
249
250 if self.capabilities.contains(MemoryCapability::Internal) {
251 defmt::write!(fmt, "Internal");
252 } else if self.capabilities.contains(MemoryCapability::External) {
253 defmt::write!(fmt, "External");
254 } else {
255 defmt::write!(fmt, "Unknown");
256 }
257
258 defmt::write!(fmt, " | ");
259
260 write_bar_defmt(fmt, usage_percent);
261
262 defmt::write!(
263 fmt,
264 " | Used: {}% (Used {} of {}, free: {})",
265 usage_percent,
266 self.used,
267 self.size,
268 self.free
269 );
270 }
271}
272
273/// A memory region to be used as heap memory
274pub struct HeapRegion {
275 heap: Heap,
276 capabilities: EnumSet<MemoryCapability>,
277}
278
279impl HeapRegion {
280 /// Create a new [HeapRegion] with the given capabilities
281 ///
282 /// # Safety
283 ///
284 /// - The supplied memory region must be available for the entire program (`'static`).
285 /// - The supplied memory region must be exclusively available to the heap only, no aliasing.
286 /// - `size > 0`.
287 pub unsafe fn new(
288 heap_bottom: *mut u8,
289 size: usize,
290 capabilities: EnumSet<MemoryCapability>,
291 ) -> Self {
292 Self {
293 heap: unsafe { Heap::new(heap_bottom, size) },
294 capabilities,
295 }
296 }
297
298 /// Return stats for the current memory region
299 pub fn stats(&self) -> RegionStats {
300 RegionStats {
301 size: self.size(),
302 used: self.used(),
303 free: self.free(),
304 capabilities: self.capabilities,
305 }
306 }
307
308 fn size(&self) -> usize {
309 self.heap.size()
310 }
311
312 fn used(&self) -> usize {
313 self.heap.used()
314 }
315
316 fn free(&self) -> usize {
317 self.heap.free()
318 }
319
320 fn allocate(&mut self, layout: Layout) -> Option<NonNull<u8>> {
321 self.heap.allocate(layout)
322 }
323
324 unsafe fn try_deallocate(&mut self, ptr: NonNull<u8>, layout: Layout) -> bool {
325 unsafe { self.heap.try_deallocate(ptr, layout) }
326 }
327}
328
329/// Stats for a heap allocator
330///
331/// Enable the "internal-heap-stats" feature if you want collect additional heap
332/// informations at the cost of extra cpu time during every alloc/dealloc.
333#[derive(Debug)]
334pub struct HeapStats {
335 /// Granular stats for all the configured memory regions.
336 pub region_stats: [Option<RegionStats>; 3],
337
338 /// Total size of all combined heap regions in bytes.
339 pub size: usize,
340
341 /// Current usage of the heap across all configured regions in bytes.
342 pub current_usage: usize,
343
344 /// Estimation of the max used heap in bytes.
345 #[cfg(feature = "internal-heap-stats")]
346 pub max_usage: usize,
347
348 /// Estimation of the total allocated bytes since initialization.
349 #[cfg(feature = "internal-heap-stats")]
350 pub total_allocated: u64,
351
352 /// Estimation of the total freed bytes since initialization.
353 #[cfg(feature = "internal-heap-stats")]
354 pub total_freed: u64,
355}
356
357impl Display for HeapStats {
358 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
359 writeln!(f, "HEAP INFO")?;
360 writeln!(f, "Size: {}", self.size)?;
361 writeln!(f, "Current usage: {}", self.current_usage)?;
362 #[cfg(feature = "internal-heap-stats")]
363 {
364 writeln!(f, "Max usage: {}", self.max_usage)?;
365 writeln!(f, "Total freed: {}", self.total_freed)?;
366 writeln!(f, "Total allocated: {}", self.total_allocated)?;
367 }
368 writeln!(f, "Memory Layout: ")?;
369 for region in self.region_stats.iter() {
370 if let Some(region) = region.as_ref() {
371 region.fmt(f)?;
372 writeln!(f)?;
373 }
374 }
375 Ok(())
376 }
377}
378
379#[cfg(feature = "defmt")]
380impl defmt::Format for HeapStats {
381 fn format(&self, fmt: defmt::Formatter<'_>) {
382 defmt::write!(fmt, "HEAP INFO\n");
383 defmt::write!(fmt, "Size: {}\n", self.size);
384 defmt::write!(fmt, "Current usage: {}\n", self.current_usage);
385 #[cfg(feature = "internal-heap-stats")]
386 {
387 defmt::write!(fmt, "Max usage: {}\n", self.max_usage);
388 defmt::write!(fmt, "Total freed: {}\n", self.total_freed);
389 defmt::write!(fmt, "Total allocated: {}\n", self.total_allocated);
390 }
391 defmt::write!(fmt, "Memory Layout:\n");
392 for region in self.region_stats.iter() {
393 if let Some(region) = region.as_ref() {
394 defmt::write!(fmt, "{}\n", region);
395 }
396 }
397 }
398}
399
400/// Internal stats to keep track across multiple regions.
401#[cfg(feature = "internal-heap-stats")]
402struct InternalHeapStats {
403 max_usage: usize,
404 total_allocated: u64,
405 total_freed: u64,
406}
407
408struct EspHeapInner {
409 heap: [Option<HeapRegion>; 3],
410 #[cfg(feature = "internal-heap-stats")]
411 internal_heap_stats: InternalHeapStats,
412}
413
414impl EspHeapInner {
415 /// Crate a new UNINITIALIZED heap allocator
416 pub const fn empty() -> Self {
417 EspHeapInner {
418 heap: [const { None }; 3],
419 #[cfg(feature = "internal-heap-stats")]
420 internal_heap_stats: InternalHeapStats {
421 max_usage: 0,
422 total_allocated: 0,
423 total_freed: 0,
424 },
425 }
426 }
427
428 pub unsafe fn add_region(&mut self, region: HeapRegion) {
429 let free = self
430 .heap
431 .iter()
432 .enumerate()
433 .find(|v| v.1.is_none())
434 .map(|v| v.0);
435
436 if let Some(free) = free {
437 self.heap[free] = Some(region);
438 } else {
439 panic!(
440 "Exceeded the maximum of {} heap memory regions",
441 self.heap.len()
442 );
443 }
444 }
445
446 /// Returns an estimate of the amount of bytes in use in all memory regions.
447 pub fn used(&self) -> usize {
448 let mut used = 0;
449 for region in self.heap.iter() {
450 if let Some(region) = region.as_ref() {
451 used += region.heap.used();
452 }
453 }
454 used
455 }
456
457 /// Return usage stats for the [EspHeap].
458 ///
459 /// Note:
460 /// [HeapStats] directly implements [Display], so this function can be
461 /// called from within `println!()` to pretty-print the usage of the
462 /// heap.
463 pub fn stats(&self) -> HeapStats {
464 let mut region_stats: [Option<RegionStats>; 3] = [const { None }; 3];
465
466 let mut used = 0;
467 let mut free = 0;
468 for (id, region) in self.heap.iter().enumerate() {
469 if let Some(region) = region.as_ref() {
470 let stats = region.stats();
471 free += stats.free;
472 used += stats.used;
473 region_stats[id] = Some(region.stats());
474 }
475 }
476
477 cfg_if::cfg_if! {
478 if #[cfg(feature = "internal-heap-stats")] {
479 HeapStats {
480 region_stats,
481 size: free + used,
482 current_usage: used,
483 max_usage: self.internal_heap_stats.max_usage,
484 total_allocated: self.internal_heap_stats.total_allocated,
485 total_freed: self.internal_heap_stats.total_freed,
486 }
487 } else {
488 HeapStats {
489 region_stats,
490 size: free + used,
491 current_usage: used,
492 }
493 }
494 }
495 }
496
497 /// Returns an estimate of the amount of bytes available.
498 pub fn free(&self) -> usize {
499 self.free_caps(EnumSet::empty())
500 }
501
502 /// The free heap satisfying the given requirements
503 pub fn free_caps(&self, capabilities: EnumSet<MemoryCapability>) -> usize {
504 let mut free = 0;
505 for region in self.heap.iter().filter(|region| {
506 if region.is_some() {
507 region
508 .as_ref()
509 .unwrap()
510 .capabilities
511 .is_superset(capabilities)
512 } else {
513 false
514 }
515 }) {
516 if let Some(region) = region.as_ref() {
517 free += region.heap.free();
518 }
519 }
520 free
521 }
522
523 /// Allocate memory in a region satisfying the given requirements.
524 ///
525 /// # Safety
526 ///
527 /// This function is unsafe because undefined behavior can result
528 /// if the caller does not ensure that `layout` has non-zero size.
529 ///
530 /// The allocated block of memory may or may not be initialized.
531 unsafe fn alloc_caps(
532 &mut self,
533 capabilities: EnumSet<MemoryCapability>,
534 layout: Layout,
535 ) -> *mut u8 {
536 #[cfg(feature = "internal-heap-stats")]
537 let before = self.used();
538 let mut iter = self
539 .heap
540 .iter_mut()
541 .filter_map(|region| region.as_mut())
542 .filter(|region| region.capabilities.is_superset(capabilities));
543
544 let allocation = loop {
545 let Some(region) = iter.next() else {
546 return ptr::null_mut();
547 };
548
549 if let Some(res) = region.allocate(layout) {
550 break res;
551 }
552 };
553
554 #[cfg(feature = "internal-heap-stats")]
555 {
556 // We need to call used because the heap impls have some internal overhead
557 // so we cannot use the size provided by the layout.
558 let used = self.used();
559
560 self.internal_heap_stats.total_allocated = self
561 .internal_heap_stats
562 .total_allocated
563 .saturating_add((used - before) as u64);
564 self.internal_heap_stats.max_usage =
565 core::cmp::max(self.internal_heap_stats.max_usage, used);
566 }
567
568 allocation.as_ptr()
569 }
570}
571
572/// A memory allocator
573///
574/// In addition to what Rust's memory allocator can do it allows to allocate
575/// memory in regions satisfying specific needs.
576pub struct EspHeap {
577 inner: NonReentrantMutex<EspHeapInner>,
578}
579
580impl EspHeap {
581 /// Crate a new UNINITIALIZED heap allocator
582 pub const fn empty() -> Self {
583 EspHeap {
584 inner: NonReentrantMutex::new(EspHeapInner::empty()),
585 }
586 }
587
588 /// Add a memory region to the heap
589 ///
590 /// `heap_bottom` is a pointer to the location of the bottom of the heap.
591 ///
592 /// `size` is the size of the heap in bytes.
593 ///
594 /// You can add up to three regions per allocator.
595 ///
596 /// Note that:
597 ///
598 /// - Memory is allocated from the first suitable memory region first
599 ///
600 /// - The heap grows "upwards", towards larger addresses. Thus `end_addr` must be larger than
601 /// `start_addr`
602 ///
603 /// - The size of the heap is `(end_addr as usize) - (start_addr as usize)`. The allocator won't
604 /// use the byte at `end_addr`.
605 ///
606 /// # Safety
607 ///
608 /// - The supplied memory region must be available for the entire program (a `'static`
609 /// lifetime).
610 /// - The supplied memory region must be exclusively available to the heap only, no aliasing.
611 /// - `size > 0`.
612 pub unsafe fn add_region(&self, region: HeapRegion) {
613 self.inner.with(|heap| unsafe { heap.add_region(region) })
614 }
615
616 /// Returns an estimate of the amount of bytes in use in all memory regions.
617 pub fn used(&self) -> usize {
618 self.inner.with(|heap| heap.used())
619 }
620
621 /// Return usage stats for the [EspHeap].
622 ///
623 /// Note:
624 /// [HeapStats] directly implements [Display], so this function can be
625 /// called from within `println!()` to pretty-print the usage of the
626 /// heap.
627 pub fn stats(&self) -> HeapStats {
628 self.inner.with(|heap| heap.stats())
629 }
630
631 /// Returns an estimate of the amount of bytes available.
632 pub fn free(&self) -> usize {
633 self.inner.with(|heap| heap.free())
634 }
635
636 /// The free heap satisfying the given requirements
637 pub fn free_caps(&self, capabilities: EnumSet<MemoryCapability>) -> usize {
638 self.inner.with(|heap| heap.free_caps(capabilities))
639 }
640
641 /// Allocate memory in a region satisfying the given requirements.
642 ///
643 /// # Safety
644 ///
645 /// This function is unsafe because undefined behavior can result
646 /// if the caller does not ensure that `layout` has non-zero size.
647 ///
648 /// The allocated block of memory may or may not be initialized.
649 pub unsafe fn alloc_caps(
650 &self,
651 capabilities: EnumSet<MemoryCapability>,
652 layout: Layout,
653 ) -> *mut u8 {
654 let ptr = self
655 .inner
656 .with(|heap| unsafe { heap.alloc_caps(capabilities, layout) });
657
658 #[cfg(feature = "alloc-hooks")]
659 unsafe {
660 _esp_alloc_alloc(self, capabilities, ptr.addr(), layout.size());
661 }
662
663 ptr
664 }
665
666 /// Deallocate memory.
667 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
668 #[cfg(feature = "alloc-hooks")]
669 unsafe {
670 _esp_alloc_dealloc(self, ptr.addr(), layout.size());
671 }
672
673 let Some(ptr) = NonNull::new(ptr) else {
674 return;
675 };
676
677 self.inner.with(|this| {
678 #[cfg(feature = "internal-heap-stats")]
679 let before = this.used();
680 let mut iter = this.heap.iter_mut();
681
682 while let Some(Some(region)) = iter.next() {
683 if unsafe { region.try_deallocate(ptr, layout) } {
684 break;
685 }
686 }
687
688 #[cfg(feature = "internal-heap-stats")]
689 {
690 // We need to call `used()` because [linked_list_allocator::Heap] does internal
691 // size alignment so we cannot use the size provided by the
692 // layout.
693 this.internal_heap_stats.total_freed = this
694 .internal_heap_stats
695 .total_freed
696 .saturating_add((before - this.used()) as u64);
697 }
698 })
699 }
700}
701
702unsafe impl GlobalAlloc for EspHeap {
703 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
704 unsafe { self.alloc_caps(EnumSet::empty(), layout) }
705 }
706
707 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
708 unsafe { self.dealloc(ptr, layout) }
709 }
710}