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://docs.espressif.com/projects/rust/esp-rs-grey-bg.svg")]
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_select! {
478 feature = "internal-heap-stats" => HeapStats {
479 region_stats,
480 size: free + used,
481 current_usage: used,
482 max_usage: self.internal_heap_stats.max_usage,
483 total_allocated: self.internal_heap_stats.total_allocated,
484 total_freed: self.internal_heap_stats.total_freed,
485 },
486 _ => HeapStats {
487 region_stats,
488 size: free + used,
489 current_usage: used,
490 },
491 }
492 }
493
494 /// Returns an estimate of the amount of bytes available.
495 pub fn free(&self) -> usize {
496 self.free_caps(EnumSet::empty())
497 }
498
499 /// The free heap satisfying the given requirements
500 pub fn free_caps(&self, capabilities: EnumSet<MemoryCapability>) -> usize {
501 let mut free = 0;
502 for region in self.heap.iter().filter(|region| {
503 if region.is_some() {
504 region
505 .as_ref()
506 .unwrap()
507 .capabilities
508 .is_superset(capabilities)
509 } else {
510 false
511 }
512 }) {
513 if let Some(region) = region.as_ref() {
514 free += region.heap.free();
515 }
516 }
517 free
518 }
519
520 /// Allocate memory in a region satisfying the given requirements.
521 ///
522 /// # Safety
523 ///
524 /// This function is unsafe because undefined behavior can result
525 /// if the caller does not ensure that `layout` has non-zero size.
526 ///
527 /// The allocated block of memory may or may not be initialized.
528 unsafe fn alloc_caps(
529 &mut self,
530 capabilities: EnumSet<MemoryCapability>,
531 layout: Layout,
532 ) -> *mut u8 {
533 #[cfg(feature = "internal-heap-stats")]
534 let before = self.used();
535 let mut iter = self
536 .heap
537 .iter_mut()
538 .filter_map(|region| region.as_mut())
539 .filter(|region| region.capabilities.is_superset(capabilities));
540
541 let allocation = loop {
542 let Some(region) = iter.next() else {
543 return ptr::null_mut();
544 };
545
546 if let Some(res) = region.allocate(layout) {
547 break res;
548 }
549 };
550
551 #[cfg(feature = "internal-heap-stats")]
552 {
553 // We need to call used because the heap impls have some internal overhead
554 // so we cannot use the size provided by the layout.
555 let used = self.used();
556
557 self.internal_heap_stats.total_allocated = self
558 .internal_heap_stats
559 .total_allocated
560 .saturating_add((used - before) as u64);
561 self.internal_heap_stats.max_usage =
562 core::cmp::max(self.internal_heap_stats.max_usage, used);
563 }
564
565 allocation.as_ptr()
566 }
567}
568
569/// A memory allocator
570///
571/// In addition to what Rust's memory allocator can do it allows to allocate
572/// memory in regions satisfying specific needs.
573pub struct EspHeap {
574 inner: NonReentrantMutex<EspHeapInner>,
575}
576
577impl EspHeap {
578 /// Crate a new UNINITIALIZED heap allocator
579 pub const fn empty() -> Self {
580 EspHeap {
581 inner: NonReentrantMutex::new(EspHeapInner::empty()),
582 }
583 }
584
585 /// Add a memory region to the heap
586 ///
587 /// `heap_bottom` is a pointer to the location of the bottom of the heap.
588 ///
589 /// `size` is the size of the heap in bytes.
590 ///
591 /// You can add up to three regions per allocator.
592 ///
593 /// Note that:
594 ///
595 /// - Memory is allocated from the first suitable memory region first
596 ///
597 /// - The heap grows "upwards", towards larger addresses. Thus `end_addr` must be larger than
598 /// `start_addr`
599 ///
600 /// - The size of the heap is `(end_addr as usize) - (start_addr as usize)`. The allocator won't
601 /// use the byte at `end_addr`.
602 ///
603 /// # Safety
604 ///
605 /// - The supplied memory region must be available for the entire program (a `'static`
606 /// lifetime).
607 /// - The supplied memory region must be exclusively available to the heap only, no aliasing.
608 /// - `size > 0`.
609 pub unsafe fn add_region(&self, region: HeapRegion) {
610 self.inner.with(|heap| unsafe { heap.add_region(region) })
611 }
612
613 /// Returns an estimate of the amount of bytes in use in all memory regions.
614 pub fn used(&self) -> usize {
615 self.inner.with(|heap| heap.used())
616 }
617
618 /// Return usage stats for the [EspHeap].
619 ///
620 /// Note:
621 /// [HeapStats] directly implements [Display], so this function can be
622 /// called from within `println!()` to pretty-print the usage of the
623 /// heap.
624 pub fn stats(&self) -> HeapStats {
625 self.inner.with(|heap| heap.stats())
626 }
627
628 /// Returns an estimate of the amount of bytes available.
629 pub fn free(&self) -> usize {
630 self.inner.with(|heap| heap.free())
631 }
632
633 /// The free heap satisfying the given requirements
634 pub fn free_caps(&self, capabilities: EnumSet<MemoryCapability>) -> usize {
635 self.inner.with(|heap| heap.free_caps(capabilities))
636 }
637
638 /// Allocate memory in a region satisfying the given requirements.
639 ///
640 /// # Safety
641 ///
642 /// This function is unsafe because undefined behavior can result
643 /// if the caller does not ensure that `layout` has non-zero size.
644 ///
645 /// The allocated block of memory may or may not be initialized.
646 pub unsafe fn alloc_caps(
647 &self,
648 capabilities: EnumSet<MemoryCapability>,
649 layout: Layout,
650 ) -> *mut u8 {
651 let ptr = self
652 .inner
653 .with(|heap| unsafe { heap.alloc_caps(capabilities, layout) });
654
655 #[cfg(feature = "alloc-hooks")]
656 unsafe {
657 _esp_alloc_alloc(self, capabilities, ptr.addr(), layout.size());
658 }
659
660 ptr
661 }
662
663 /// Deallocate memory.
664 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
665 #[cfg(feature = "alloc-hooks")]
666 unsafe {
667 _esp_alloc_dealloc(self, ptr.addr(), layout.size());
668 }
669
670 let Some(ptr) = NonNull::new(ptr) else {
671 return;
672 };
673
674 self.inner.with(|this| {
675 #[cfg(feature = "internal-heap-stats")]
676 let before = this.used();
677 // Skip empty slots instead of stopping at the first one - the region array is
678 // not necessarily densely populated.
679 for region in this.heap.iter_mut().filter_map(|region| region.as_mut()) {
680 if unsafe { region.try_deallocate(ptr, layout) } {
681 break;
682 }
683 }
684
685 #[cfg(feature = "internal-heap-stats")]
686 {
687 // We need to call `used()` because [linked_list_allocator::Heap] does internal
688 // size alignment so we cannot use the size provided by the
689 // layout.
690 this.internal_heap_stats.total_freed = this
691 .internal_heap_stats
692 .total_freed
693 .saturating_add((before - this.used()) as u64);
694 }
695 })
696 }
697}
698
699unsafe impl GlobalAlloc for EspHeap {
700 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
701 unsafe { self.alloc_caps(EnumSet::empty(), layout) }
702 }
703
704 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
705 unsafe { self.dealloc(ptr, layout) }
706 }
707}