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
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
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
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
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
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
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
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
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!()]
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
157pub use allocators::*;
158use enumset::{EnumSet, EnumSetType};
159use esp_sync::NonReentrantMutex;
160
161use crate::heap::Heap;
162
163/// The global allocator instance
164#[global_allocator]
165pub static HEAP: EspHeap = EspHeap::empty();
166
167const BAR_WIDTH: usize = 35;
168
169fn write_bar(f: &mut core::fmt::Formatter<'_>, usage_percent: usize) -> core::fmt::Result {
170    let used_blocks = BAR_WIDTH * usage_percent / 100;
171    (0..used_blocks).try_for_each(|_| write!(f, "█"))?;
172    (used_blocks..BAR_WIDTH).try_for_each(|_| write!(f, "░"))
173}
174
175#[cfg(feature = "defmt")]
176fn write_bar_defmt(fmt: defmt::Formatter, usage_percent: usize) {
177    let used_blocks = BAR_WIDTH * usage_percent / 100;
178    (0..used_blocks).for_each(|_| defmt::write!(fmt, "█"));
179    (used_blocks..BAR_WIDTH).for_each(|_| defmt::write!(fmt, "░"));
180}
181
182#[derive(EnumSetType, Debug)]
183/// Describes the properties of a memory region
184pub enum MemoryCapability {
185    /// Memory must be internal; specifically it should not disappear when
186    /// flash/spiram cache is switched off
187    Internal,
188    /// Memory must be in SPI RAM
189    External,
190}
191
192/// Stats for a heap region
193#[derive(Debug)]
194pub struct RegionStats {
195    /// Total usable size of the heap region in bytes.
196    pub size: usize,
197
198    /// Currently used size of the heap region in bytes.
199    pub used: usize,
200
201    /// Free size of the heap region in bytes.
202    pub free: usize,
203
204    /// Capabilities of the memory region.
205    pub capabilities: EnumSet<MemoryCapability>,
206}
207
208impl Display for RegionStats {
209    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
210        let usage_percent = self.used * 100 / self.size;
211
212        // Display Memory type
213        if self.capabilities.contains(MemoryCapability::Internal) {
214            write!(f, "Internal")?;
215        } else if self.capabilities.contains(MemoryCapability::External) {
216            write!(f, "External")?;
217        } else {
218            write!(f, "Unknown")?;
219        }
220
221        write!(f, " | ")?;
222
223        write_bar(f, usage_percent)?;
224
225        write!(
226            f,
227            " | Used: {}% (Used {} of {}, free: {})",
228            usage_percent, self.used, self.size, self.free
229        )
230    }
231}
232
233#[cfg(feature = "defmt")]
234impl defmt::Format for RegionStats {
235    fn format(&self, fmt: defmt::Formatter<'_>) {
236        let usage_percent = self.used * 100 / self.size;
237
238        if self.capabilities.contains(MemoryCapability::Internal) {
239            defmt::write!(fmt, "Internal");
240        } else if self.capabilities.contains(MemoryCapability::External) {
241            defmt::write!(fmt, "External");
242        } else {
243            defmt::write!(fmt, "Unknown");
244        }
245
246        defmt::write!(fmt, " | ");
247
248        write_bar_defmt(fmt, usage_percent);
249
250        defmt::write!(
251            fmt,
252            " | Used: {}% (Used {} of {}, free: {})",
253            usage_percent,
254            self.used,
255            self.size,
256            self.free
257        );
258    }
259}
260
261/// A memory region to be used as heap memory
262pub struct HeapRegion {
263    heap: Heap,
264    capabilities: EnumSet<MemoryCapability>,
265}
266
267impl HeapRegion {
268    /// Create a new [HeapRegion] with the given capabilities
269    ///
270    /// # Safety
271    ///
272    /// - The supplied memory region must be available for the entire program (`'static`).
273    /// - The supplied memory region must be exclusively available to the heap only, no aliasing.
274    /// - `size > 0`.
275    pub unsafe fn new(
276        heap_bottom: *mut u8,
277        size: usize,
278        capabilities: EnumSet<MemoryCapability>,
279    ) -> Self {
280        Self {
281            heap: unsafe { Heap::new(heap_bottom, size) },
282            capabilities,
283        }
284    }
285
286    /// Return stats for the current memory region
287    pub fn stats(&self) -> RegionStats {
288        RegionStats {
289            size: self.size(),
290            used: self.used(),
291            free: self.free(),
292            capabilities: self.capabilities,
293        }
294    }
295
296    fn size(&self) -> usize {
297        self.heap.size()
298    }
299
300    fn used(&self) -> usize {
301        self.heap.used()
302    }
303
304    fn free(&self) -> usize {
305        self.heap.free()
306    }
307
308    fn allocate(&mut self, layout: Layout) -> Option<NonNull<u8>> {
309        self.heap.allocate(layout)
310    }
311
312    unsafe fn try_deallocate(&mut self, ptr: NonNull<u8>, layout: Layout) -> bool {
313        unsafe { self.heap.try_deallocate(ptr, layout) }
314    }
315}
316
317/// Stats for a heap allocator
318///
319/// Enable the "internal-heap-stats" feature if you want collect additional heap
320/// informations at the cost of extra cpu time during every alloc/dealloc.
321#[derive(Debug)]
322pub struct HeapStats {
323    /// Granular stats for all the configured memory regions.
324    pub region_stats: [Option<RegionStats>; 3],
325
326    /// Total size of all combined heap regions in bytes.
327    pub size: usize,
328
329    /// Current usage of the heap across all configured regions in bytes.
330    pub current_usage: usize,
331
332    /// Estimation of the max used heap in bytes.
333    #[cfg(feature = "internal-heap-stats")]
334    pub max_usage: usize,
335
336    /// Estimation of the total allocated bytes since initialization.
337    #[cfg(feature = "internal-heap-stats")]
338    pub total_allocated: usize,
339
340    /// Estimation of the total freed bytes since initialization.
341    #[cfg(feature = "internal-heap-stats")]
342    pub total_freed: usize,
343}
344
345impl Display for HeapStats {
346    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
347        writeln!(f, "HEAP INFO")?;
348        writeln!(f, "Size: {}", self.size)?;
349        writeln!(f, "Current usage: {}", self.current_usage)?;
350        #[cfg(feature = "internal-heap-stats")]
351        {
352            writeln!(f, "Max usage: {}", self.max_usage)?;
353            writeln!(f, "Total freed: {}", self.total_freed)?;
354            writeln!(f, "Total allocated: {}", self.total_allocated)?;
355        }
356        writeln!(f, "Memory Layout: ")?;
357        for region in self.region_stats.iter() {
358            if let Some(region) = region.as_ref() {
359                region.fmt(f)?;
360                writeln!(f)?;
361            }
362        }
363        Ok(())
364    }
365}
366
367#[cfg(feature = "defmt")]
368impl defmt::Format for HeapStats {
369    fn format(&self, fmt: defmt::Formatter<'_>) {
370        defmt::write!(fmt, "HEAP INFO\n");
371        defmt::write!(fmt, "Size: {}\n", self.size);
372        defmt::write!(fmt, "Current usage: {}\n", self.current_usage);
373        #[cfg(feature = "internal-heap-stats")]
374        {
375            defmt::write!(fmt, "Max usage: {}\n", self.max_usage);
376            defmt::write!(fmt, "Total freed: {}\n", self.total_freed);
377            defmt::write!(fmt, "Total allocated: {}\n", self.total_allocated);
378        }
379        defmt::write!(fmt, "Memory Layout:\n");
380        for region in self.region_stats.iter() {
381            if let Some(region) = region.as_ref() {
382                defmt::write!(fmt, "{}\n", region);
383            }
384        }
385    }
386}
387
388/// Internal stats to keep track across multiple regions.
389#[cfg(feature = "internal-heap-stats")]
390struct InternalHeapStats {
391    max_usage: usize,
392    total_allocated: usize,
393    total_freed: usize,
394}
395
396struct EspHeapInner {
397    heap: [Option<HeapRegion>; 3],
398    #[cfg(feature = "internal-heap-stats")]
399    internal_heap_stats: InternalHeapStats,
400}
401
402impl EspHeapInner {
403    /// Crate a new UNINITIALIZED heap allocator
404    pub const fn empty() -> Self {
405        EspHeapInner {
406            heap: [const { None }; 3],
407            #[cfg(feature = "internal-heap-stats")]
408            internal_heap_stats: InternalHeapStats {
409                max_usage: 0,
410                total_allocated: 0,
411                total_freed: 0,
412            },
413        }
414    }
415
416    pub unsafe fn add_region(&mut self, region: HeapRegion) {
417        let free = self
418            .heap
419            .iter()
420            .enumerate()
421            .find(|v| v.1.is_none())
422            .map(|v| v.0);
423
424        if let Some(free) = free {
425            self.heap[free] = Some(region);
426        } else {
427            panic!(
428                "Exceeded the maximum of {} heap memory regions",
429                self.heap.len()
430            );
431        }
432    }
433
434    /// Returns an estimate of the amount of bytes in use in all memory regions.
435    pub fn used(&self) -> usize {
436        let mut used = 0;
437        for region in self.heap.iter() {
438            if let Some(region) = region.as_ref() {
439                used += region.heap.used();
440            }
441        }
442        used
443    }
444
445    /// Return usage stats for the [EspHeap].
446    ///
447    /// Note:
448    /// [HeapStats] directly implements [Display], so this function can be
449    /// called from within `println!()` to pretty-print the usage of the
450    /// heap.
451    pub fn stats(&self) -> HeapStats {
452        let mut region_stats: [Option<RegionStats>; 3] = [const { None }; 3];
453
454        let mut used = 0;
455        let mut free = 0;
456        for (id, region) in self.heap.iter().enumerate() {
457            if let Some(region) = region.as_ref() {
458                let stats = region.stats();
459                free += stats.free;
460                used += stats.used;
461                region_stats[id] = Some(region.stats());
462            }
463        }
464
465        cfg_if::cfg_if! {
466            if #[cfg(feature = "internal-heap-stats")] {
467                HeapStats {
468                    region_stats,
469                    size: free + used,
470                    current_usage: used,
471                    max_usage: self.internal_heap_stats.max_usage,
472                    total_allocated: self.internal_heap_stats.total_allocated,
473                    total_freed: self.internal_heap_stats.total_freed,
474                }
475            } else {
476                HeapStats {
477                    region_stats,
478                    size: free + used,
479                    current_usage: used,
480                }
481            }
482        }
483    }
484
485    /// Returns an estimate of the amount of bytes available.
486    pub fn free(&self) -> usize {
487        self.free_caps(EnumSet::empty())
488    }
489
490    /// The free heap satisfying the given requirements
491    pub fn free_caps(&self, capabilities: EnumSet<MemoryCapability>) -> usize {
492        let mut free = 0;
493        for region in self.heap.iter().filter(|region| {
494            if region.is_some() {
495                region
496                    .as_ref()
497                    .unwrap()
498                    .capabilities
499                    .is_superset(capabilities)
500            } else {
501                false
502            }
503        }) {
504            if let Some(region) = region.as_ref() {
505                free += region.heap.free();
506            }
507        }
508        free
509    }
510
511    /// Allocate memory in a region satisfying the given requirements.
512    ///
513    /// # Safety
514    ///
515    /// This function is unsafe because undefined behavior can result
516    /// if the caller does not ensure that `layout` has non-zero size.
517    ///
518    /// The allocated block of memory may or may not be initialized.
519    unsafe fn alloc_caps(
520        &mut self,
521        capabilities: EnumSet<MemoryCapability>,
522        layout: Layout,
523    ) -> *mut u8 {
524        #[cfg(feature = "internal-heap-stats")]
525        let before = self.used();
526        let mut iter = self
527            .heap
528            .iter_mut()
529            .filter_map(|region| region.as_mut())
530            .filter(|region| region.capabilities.is_superset(capabilities));
531
532        let allocation = loop {
533            let Some(region) = iter.next() else {
534                return ptr::null_mut();
535            };
536
537            if let Some(res) = region.allocate(layout) {
538                break res;
539            }
540        };
541
542        #[cfg(feature = "internal-heap-stats")]
543        {
544            // We need to call used because the heap impls have some internal overhead
545            // so we cannot use the size provided by the layout.
546            let used = self.used();
547
548            self.internal_heap_stats.total_allocated += used - before;
549            self.internal_heap_stats.max_usage =
550                core::cmp::max(self.internal_heap_stats.max_usage, used);
551        }
552
553        allocation.as_ptr()
554    }
555}
556
557/// A memory allocator
558///
559/// In addition to what Rust's memory allocator can do it allows to allocate
560/// memory in regions satisfying specific needs.
561pub struct EspHeap {
562    inner: NonReentrantMutex<EspHeapInner>,
563}
564
565impl EspHeap {
566    /// Crate a new UNINITIALIZED heap allocator
567    pub const fn empty() -> Self {
568        EspHeap {
569            inner: NonReentrantMutex::new(EspHeapInner::empty()),
570        }
571    }
572
573    /// Add a memory region to the heap
574    ///
575    /// `heap_bottom` is a pointer to the location of the bottom of the heap.
576    ///
577    /// `size` is the size of the heap in bytes.
578    ///
579    /// You can add up to three regions per allocator.
580    ///
581    /// Note that:
582    ///
583    /// - Memory is allocated from the first suitable memory region first
584    ///
585    /// - The heap grows "upwards", towards larger addresses. Thus `end_addr` must be larger than
586    ///   `start_addr`
587    ///
588    /// - The size of the heap is `(end_addr as usize) - (start_addr as usize)`. The allocator won't
589    ///   use the byte at `end_addr`.
590    ///
591    /// # Safety
592    ///
593    /// - The supplied memory region must be available for the entire program (a `'static`
594    ///   lifetime).
595    /// - The supplied memory region must be exclusively available to the heap only, no aliasing.
596    /// - `size > 0`.
597    pub unsafe fn add_region(&self, region: HeapRegion) {
598        self.inner.with(|heap| unsafe { heap.add_region(region) })
599    }
600
601    /// Returns an estimate of the amount of bytes in use in all memory regions.
602    pub fn used(&self) -> usize {
603        self.inner.with(|heap| heap.used())
604    }
605
606    /// Return usage stats for the [EspHeap].
607    ///
608    /// Note:
609    /// [HeapStats] directly implements [Display], so this function can be
610    /// called from within `println!()` to pretty-print the usage of the
611    /// heap.
612    pub fn stats(&self) -> HeapStats {
613        self.inner.with(|heap| heap.stats())
614    }
615
616    /// Returns an estimate of the amount of bytes available.
617    pub fn free(&self) -> usize {
618        self.inner.with(|heap| heap.free())
619    }
620
621    /// The free heap satisfying the given requirements
622    pub fn free_caps(&self, capabilities: EnumSet<MemoryCapability>) -> usize {
623        self.inner.with(|heap| heap.free_caps(capabilities))
624    }
625
626    /// Allocate memory in a region satisfying the given requirements.
627    ///
628    /// # Safety
629    ///
630    /// This function is unsafe because undefined behavior can result
631    /// if the caller does not ensure that `layout` has non-zero size.
632    ///
633    /// The allocated block of memory may or may not be initialized.
634    pub unsafe fn alloc_caps(
635        &self,
636        capabilities: EnumSet<MemoryCapability>,
637        layout: Layout,
638    ) -> *mut u8 {
639        self.inner
640            .with(|heap| unsafe { heap.alloc_caps(capabilities, layout) })
641    }
642}
643
644unsafe impl GlobalAlloc for EspHeap {
645    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
646        unsafe { self.alloc_caps(EnumSet::empty(), layout) }
647    }
648
649    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
650        let Some(ptr) = NonNull::new(ptr) else {
651            return;
652        };
653
654        self.inner.with(|this| {
655            #[cfg(feature = "internal-heap-stats")]
656            let before = this.used();
657            let mut iter = this.heap.iter_mut();
658
659            while let Some(Some(region)) = iter.next() {
660                if unsafe { region.try_deallocate(ptr, layout) } {
661                    break;
662                }
663            }
664
665            #[cfg(feature = "internal-heap-stats")]
666            {
667                // We need to call `used()` because [linked_list_allocator::Heap] does internal
668                // size alignment so we cannot use the size provided by the
669                // layout.
670                this.internal_heap_stats.total_freed += before - this.used();
671            }
672        })
673    }
674}