[core] Add RAMAllocator::make_unique for objects whose allocation may fail (#19245)

This commit is contained in:
J. Nick Koston
2026-09-14 09:56:50 +12:00
committed by GitHub
parent eb1ea4aefe
commit c51020dbaf
2 changed files with 92 additions and 0 deletions
+40
View File
@@ -5,6 +5,7 @@
#include <cassert>
#include <cmath>
#include <cstdarg>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
@@ -13,9 +14,11 @@
#include <iterator>
#include <limits>
#include <memory>
#include <new>
#include <span>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include <concepts>
#include <strings.h>
@@ -2123,6 +2126,10 @@ void delay_microseconds_safe(uint32_t us);
/// @name Memory management
///@{
template<typename T> struct RAMDeleter;
/// unique_ptr over RAMAllocator storage
template<typename T> using RAMUniquePtr = std::unique_ptr<T, RAMDeleter<T>>;
/** An STL allocator that uses SPI or internal RAM.
* Returns `nullptr` in case no memory is available.
*
@@ -2193,6 +2200,26 @@ template<class T> class RAMAllocator {
free(p); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc)
}
/// Value initialize one T; empty on exhaustion. new (std::nothrow) aborts on ESP-IDF instead.
/// Default flags prefer PSRAM; pass PREFER_INTERNAL to keep an object where plain new put it.
template<typename... Args> RAMUniquePtr<T> make_unique(Args &&...args) {
static_assert(alignof(T) <= alignof(std::max_align_t), "malloc storage cannot hold an over aligned type");
T *p = this->allocate(1);
if (p == nullptr)
return {};
// ::new so a class scoped operator new cannot hide the global placement form
return RAMUniquePtr<T>(::new (p) T(std::forward<Args>(args)...));
}
/// n elements left uninitialized, as std::make_unique_for_overwrite does; empty on exhaustion, overflow, and n == 0
RAMUniquePtr<T[]> make_unique_array_for_overwrite(size_t n) {
static_assert(std::is_trivially_default_constructible_v<T>, "elements are left unconstructed");
static_assert(alignof(T) <= alignof(std::max_align_t), "malloc storage cannot hold an over aligned type");
if (n == 0 || n > SIZE_MAX / sizeof(T))
return {};
return RAMUniquePtr<T[]>(this->allocate(n));
}
/**
* Return the total heap space available via this allocator
*/
@@ -2255,6 +2282,19 @@ template<class T> class RAMAllocator {
template<class T> using ExternalRAMAllocator = RAMAllocator<T>;
/// Destroys and frees RAMAllocator storage. Not convertible: free() needs the address malloc returned
template<typename T> struct RAMDeleter {
void operator()(T *p) const {
p->~T();
RAMAllocator<T>().deallocate(p, 1);
}
};
/// Array form: elements must be trivial, the count is not stored so only the storage is freed
template<typename T> struct RAMDeleter<T[]> {
static_assert(std::is_trivially_destructible_v<T>, "RAMUniquePtr<T[]> is for trivially destructible elements");
void operator()(T *p) const { RAMAllocator<T>().deallocate(p, 1); }
};
/**
* Functions to constrain the range of arithmetic values.
*/
+52
View File
@@ -367,4 +367,56 @@ TEST(FixedVectorTryInit, ReportsExhaustionAndStaysEmpty) {
EXPECT_EQ(v.size(), 1u);
}
// --- RAMAllocator::make_unique() ---
namespace {
struct Probe {
static inline int live = 0;
int a;
int b;
Probe(int a, int b) : a(a), b(b) { live++; }
~Probe() { live--; }
};
} // namespace
static_assert(sizeof(RAMUniquePtr<Probe>) == sizeof(Probe *), "the deleter must not add storage");
TEST(RAMAllocatorMakeUnique, ForwardsArgsAndDestroysOnce) {
auto p = RAMAllocator<Probe>().make_unique(3, 4);
ASSERT_NE(p, nullptr);
EXPECT_EQ(p->a, 3);
EXPECT_EQ(p->b, 4);
EXPECT_EQ(Probe::live, 1);
p.reset();
EXPECT_EQ(Probe::live, 0);
}
TEST(RAMAllocatorMakeUnique, ValueInitializesLikeMakeUnique) {
struct Plain {
uint32_t words[8];
};
// Dirty a block of the same size first so a recycled allocation is not zero by chance
auto dirty = RAMAllocator<uint8_t>().make_unique_array_for_overwrite(sizeof(Plain));
std::memset(dirty.get(), 0xFF, sizeof(Plain));
dirty.reset();
auto p = RAMAllocator<Plain>().make_unique();
ASSERT_NE(p, nullptr);
// Under ASan fresh blocks are filled with 0xbe, so this holds even when the dirtied block is not reused
EXPECT_TRUE(std::all_of(std::begin(p->words), std::end(p->words), [](uint32_t w) { return w == 0; }));
}
TEST(RAMAllocatorMakeUnique, ArrayFormRejectsOverflowAndZero) {
EXPECT_EQ(RAMAllocator<uint32_t>().make_unique_array_for_overwrite(SIZE_MAX / sizeof(uint32_t) + 1), nullptr);
EXPECT_EQ(RAMAllocator<uint32_t>().make_unique_array_for_overwrite(0), nullptr);
EXPECT_NE(RAMAllocator<uint32_t>().make_unique_array_for_overwrite(1), nullptr);
}
TEST(RAMAllocatorMakeUnique, ArrayFormAllocatesElements) {
RAMUniquePtr<uint8_t[]> buf = RAMAllocator<uint8_t>().make_unique_array_for_overwrite(256);
ASSERT_NE(buf, nullptr);
std::memset(buf.get(), 0xA5, 256);
EXPECT_EQ(buf[0], 0xA5);
EXPECT_EQ(buf[255], 0xA5);
}
} // namespace esphome::core::testing