Skip to main content

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}