mirror of
https://github.com/esphome/esphome.git
synced 2026-09-17 18:18:43 +00:00
[core] Add RAMAllocator::make_unique for objects whose allocation may fail (#19245)
This commit is contained in:
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user