#![doc = document_features::document_features!(feature_label = r#"<span class="stab portability"><code>{feature}</code></span>"#)]
#![no_std]
#![cfg_attr(feature = "nightly", feature(allocator_api))]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/46717278")]
mod allocators;
mod heap;
mod macros;
#[cfg(feature = "compat")]
mod malloc;
use core::{
alloc::{GlobalAlloc, Layout},
fmt::Display,
ptr::{self, NonNull},
};
pub mod export {
pub use enumset;
}
pub use allocators::*;
use enumset::{EnumSet, EnumSetType};
use esp_sync::NonReentrantMutex;
use crate::heap::Heap;
#[cfg_attr(feature = "global-allocator", global_allocator)]
pub static HEAP: EspHeap = EspHeap::empty();
#[cfg(feature = "alloc-hooks")]
unsafe extern "Rust" {
fn _esp_alloc_alloc(heap: &EspHeap, caps: EnumSet<MemoryCapability>, ptr: usize, size: usize);
fn _esp_alloc_dealloc(heap: &EspHeap, ptr: usize, size: usize);
}
const BAR_WIDTH: usize = 35;
fn write_bar(f: &mut core::fmt::Formatter<'_>, usage_percent: usize) -> core::fmt::Result {
let used_blocks = BAR_WIDTH * usage_percent / 100;
(0..used_blocks).try_for_each(|_| write!(f, "█"))?;
(used_blocks..BAR_WIDTH).try_for_each(|_| write!(f, "░"))
}
#[cfg(feature = "defmt")]
fn write_bar_defmt(fmt: defmt::Formatter, usage_percent: usize) {
let used_blocks = BAR_WIDTH * usage_percent / 100;
(0..used_blocks).for_each(|_| defmt::write!(fmt, "█"));
(used_blocks..BAR_WIDTH).for_each(|_| defmt::write!(fmt, "░"));
}
#[derive(EnumSetType, Debug)]
pub enum MemoryCapability {
Internal,
External,
}
#[derive(Debug)]
pub struct RegionStats {
pub size: usize,
pub used: usize,
pub free: usize,
pub capabilities: EnumSet<MemoryCapability>,
}
impl Display for RegionStats {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let usage_percent = self.used * 100 / self.size;
if self.capabilities.contains(MemoryCapability::Internal) {
write!(f, "Internal")?;
} else if self.capabilities.contains(MemoryCapability::External) {
write!(f, "External")?;
} else {
write!(f, "Unknown")?;
}
write!(f, " | ")?;
write_bar(f, usage_percent)?;
write!(
f,
" | Used: {}% (Used {} of {}, free: {})",
usage_percent, self.used, self.size, self.free
)
}
}
#[cfg(feature = "defmt")]
#[allow(clippy::if_same_then_else)]
impl defmt::Format for RegionStats {
fn format(&self, fmt: defmt::Formatter<'_>) {
let usage_percent = self.used * 100 / self.size;
if self.capabilities.contains(MemoryCapability::Internal) {
defmt::write!(fmt, "Internal");
} else if self.capabilities.contains(MemoryCapability::External) {
defmt::write!(fmt, "External");
} else {
defmt::write!(fmt, "Unknown");
}
defmt::write!(fmt, " | ");
write_bar_defmt(fmt, usage_percent);
defmt::write!(
fmt,
" | Used: {}% (Used {} of {}, free: {})",
usage_percent,
self.used,
self.size,
self.free
);
}
}
pub struct HeapRegion {
heap: Heap,
capabilities: EnumSet<MemoryCapability>,
}
impl HeapRegion {
pub unsafe fn new(
heap_bottom: *mut u8,
size: usize,
capabilities: EnumSet<MemoryCapability>,
) -> Self {
Self {
heap: unsafe { Heap::new(heap_bottom, size) },
capabilities,
}
}
pub fn stats(&self) -> RegionStats {
RegionStats {
size: self.size(),
used: self.used(),
free: self.free(),
capabilities: self.capabilities,
}
}
fn size(&self) -> usize {
self.heap.size()
}
fn used(&self) -> usize {
self.heap.used()
}
fn free(&self) -> usize {
self.heap.free()
}
fn allocate(&mut self, layout: Layout) -> Option<NonNull<u8>> {
self.heap.allocate(layout)
}
unsafe fn try_deallocate(&mut self, ptr: NonNull<u8>, layout: Layout) -> bool {
unsafe { self.heap.try_deallocate(ptr, layout) }
}
}
#[derive(Debug)]
pub struct HeapStats {
pub region_stats: [Option<RegionStats>; 3],
pub size: usize,
pub current_usage: usize,
#[cfg(feature = "internal-heap-stats")]
pub max_usage: usize,
#[cfg(feature = "internal-heap-stats")]
pub total_allocated: u64,
#[cfg(feature = "internal-heap-stats")]
pub total_freed: u64,
}
impl Display for HeapStats {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
writeln!(f, "HEAP INFO")?;
writeln!(f, "Size: {}", self.size)?;
writeln!(f, "Current usage: {}", self.current_usage)?;
#[cfg(feature = "internal-heap-stats")]
{
writeln!(f, "Max usage: {}", self.max_usage)?;
writeln!(f, "Total freed: {}", self.total_freed)?;
writeln!(f, "Total allocated: {}", self.total_allocated)?;
}
writeln!(f, "Memory Layout: ")?;
for region in self.region_stats.iter() {
if let Some(region) = region.as_ref() {
region.fmt(f)?;
writeln!(f)?;
}
}
Ok(())
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for HeapStats {
fn format(&self, fmt: defmt::Formatter<'_>) {
defmt::write!(fmt, "HEAP INFO\n");
defmt::write!(fmt, "Size: {}\n", self.size);
defmt::write!(fmt, "Current usage: {}\n", self.current_usage);
#[cfg(feature = "internal-heap-stats")]
{
defmt::write!(fmt, "Max usage: {}\n", self.max_usage);
defmt::write!(fmt, "Total freed: {}\n", self.total_freed);
defmt::write!(fmt, "Total allocated: {}\n", self.total_allocated);
}
defmt::write!(fmt, "Memory Layout:\n");
for region in self.region_stats.iter() {
if let Some(region) = region.as_ref() {
defmt::write!(fmt, "{}\n", region);
}
}
}
}
#[cfg(feature = "internal-heap-stats")]
struct InternalHeapStats {
max_usage: usize,
total_allocated: u64,
total_freed: u64,
}
struct EspHeapInner {
heap: [Option<HeapRegion>; 3],
#[cfg(feature = "internal-heap-stats")]
internal_heap_stats: InternalHeapStats,
}
impl EspHeapInner {
pub const fn empty() -> Self {
EspHeapInner {
heap: [const { None }; 3],
#[cfg(feature = "internal-heap-stats")]
internal_heap_stats: InternalHeapStats {
max_usage: 0,
total_allocated: 0,
total_freed: 0,
},
}
}
pub unsafe fn add_region(&mut self, region: HeapRegion) {
let free = self
.heap
.iter()
.enumerate()
.find(|v| v.1.is_none())
.map(|v| v.0);
if let Some(free) = free {
self.heap[free] = Some(region);
} else {
panic!(
"Exceeded the maximum of {} heap memory regions",
self.heap.len()
);
}
}
pub fn used(&self) -> usize {
let mut used = 0;
for region in self.heap.iter() {
if let Some(region) = region.as_ref() {
used += region.heap.used();
}
}
used
}
pub fn stats(&self) -> HeapStats {
let mut region_stats: [Option<RegionStats>; 3] = [const { None }; 3];
let mut used = 0;
let mut free = 0;
for (id, region) in self.heap.iter().enumerate() {
if let Some(region) = region.as_ref() {
let stats = region.stats();
free += stats.free;
used += stats.used;
region_stats[id] = Some(region.stats());
}
}
cfg_if::cfg_if! {
if #[cfg(feature = "internal-heap-stats")] {
HeapStats {
region_stats,
size: free + used,
current_usage: used,
max_usage: self.internal_heap_stats.max_usage,
total_allocated: self.internal_heap_stats.total_allocated,
total_freed: self.internal_heap_stats.total_freed,
}
} else {
HeapStats {
region_stats,
size: free + used,
current_usage: used,
}
}
}
}
pub fn free(&self) -> usize {
self.free_caps(EnumSet::empty())
}
pub fn free_caps(&self, capabilities: EnumSet<MemoryCapability>) -> usize {
let mut free = 0;
for region in self.heap.iter().filter(|region| {
if region.is_some() {
region
.as_ref()
.unwrap()
.capabilities
.is_superset(capabilities)
} else {
false
}
}) {
if let Some(region) = region.as_ref() {
free += region.heap.free();
}
}
free
}
unsafe fn alloc_caps(
&mut self,
capabilities: EnumSet<MemoryCapability>,
layout: Layout,
) -> *mut u8 {
#[cfg(feature = "internal-heap-stats")]
let before = self.used();
let mut iter = self
.heap
.iter_mut()
.filter_map(|region| region.as_mut())
.filter(|region| region.capabilities.is_superset(capabilities));
let allocation = loop {
let Some(region) = iter.next() else {
return ptr::null_mut();
};
if let Some(res) = region.allocate(layout) {
break res;
}
};
#[cfg(feature = "internal-heap-stats")]
{
let used = self.used();
self.internal_heap_stats.total_allocated = self
.internal_heap_stats
.total_allocated
.saturating_add((used - before) as u64);
self.internal_heap_stats.max_usage =
core::cmp::max(self.internal_heap_stats.max_usage, used);
}
allocation.as_ptr()
}
}
pub struct EspHeap {
inner: NonReentrantMutex<EspHeapInner>,
}
impl EspHeap {
pub const fn empty() -> Self {
EspHeap {
inner: NonReentrantMutex::new(EspHeapInner::empty()),
}
}
pub unsafe fn add_region(&self, region: HeapRegion) {
self.inner.with(|heap| unsafe { heap.add_region(region) })
}
pub fn used(&self) -> usize {
self.inner.with(|heap| heap.used())
}
pub fn stats(&self) -> HeapStats {
self.inner.with(|heap| heap.stats())
}
pub fn free(&self) -> usize {
self.inner.with(|heap| heap.free())
}
pub fn free_caps(&self, capabilities: EnumSet<MemoryCapability>) -> usize {
self.inner.with(|heap| heap.free_caps(capabilities))
}
pub unsafe fn alloc_caps(
&self,
capabilities: EnumSet<MemoryCapability>,
layout: Layout,
) -> *mut u8 {
let ptr = self
.inner
.with(|heap| unsafe { heap.alloc_caps(capabilities, layout) });
#[cfg(feature = "alloc-hooks")]
unsafe {
_esp_alloc_alloc(self, capabilities, ptr.addr(), layout.size());
}
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
#[cfg(feature = "alloc-hooks")]
unsafe {
_esp_alloc_dealloc(self, ptr.addr(), layout.size());
}
let Some(ptr) = NonNull::new(ptr) else {
return;
};
self.inner.with(|this| {
#[cfg(feature = "internal-heap-stats")]
let before = this.used();
let mut iter = this.heap.iter_mut();
while let Some(Some(region)) = iter.next() {
if unsafe { region.try_deallocate(ptr, layout) } {
break;
}
}
#[cfg(feature = "internal-heap-stats")]
{
this.internal_heap_stats.total_freed = this
.internal_heap_stats
.total_freed
.saturating_add((before - this.used()) as u64);
}
})
}
}
unsafe impl GlobalAlloc for EspHeap {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
unsafe { self.alloc_caps(EnumSet::empty(), layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { self.dealloc(ptr, layout) }
}
}