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://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}