refactor: Added utils for non-throwing memory allocation and scoped cleanup (#1832)

## Summary

Pared down version of #1418.

Following up on b5df6cb2b5. Added
lib/Memory/Memory.h with:
- `makeUniqueNoThrow<T>` a `nothrow` wrapper for `std::make_unique` that
return `nullptr` on OOM instead of calling `abort()` (the behavior of
bare `new` with `-fno-exceptions`)
- `ScopedCleanup` a helper to call a cleanup lambda on scope exit.

These utilities help to write code that handles OOM scenarios
gracefully, and consistently cleans up resources on scope exit.

JpegToBmpConverter.cpp has been converted to use these utilities. Other
files can be converted later.

This will simplify some of the SD card font resource management in
#1327.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
This commit is contained in:
Zach Nelson
2026-05-13 21:19:47 -05:00
committed by GitHub
parent 71518b3336
commit 8377ac9e31
3 changed files with 156 additions and 72 deletions
+43 -49
View File
@@ -4,10 +4,10 @@
#include <HalStorage.h>
#include <JPEGDEC.h>
#include <Logging.h>
#include <Memory.h>
#include <cstdio>
#include <cstring>
#include <new>
#include "BitmapHelpers.h"
@@ -211,26 +211,26 @@ struct BmpConvertCtx {
// Accumulates one MCU row (up to MAX_MCU_HEIGHT source rows × srcWidth pixels)
// Filled column-by-column as JPEGDEC callbacks arrive for the same MCU row
uint8_t* mcuBuf;
std::unique_ptr<uint8_t[]> mcuBuf;
// Y-axis area averaging accumulators (needsScaling only)
int currentOutY;
uint32_t nextOutY_srcStart; // 16.16 fixed-point boundary for the next output row
uint32_t* rowAccum;
uint32_t* rowCount;
std::unique_ptr<uint32_t[]> rowAccum;
std::unique_ptr<uint32_t[]> rowCount;
uint8_t* bmpRow;
std::unique_ptr<uint8_t[]> bmpRow;
AtkinsonDitherer* atkinsonDitherer;
FloydSteinbergDitherer* fsDitherer;
Atkinson1BitDitherer* atkinson1BitDitherer;
std::unique_ptr<AtkinsonDitherer> atkinsonDitherer;
std::unique_ptr<FloydSteinbergDitherer> fsDitherer;
std::unique_ptr<Atkinson1BitDitherer> atkinson1BitDitherer;
bool error;
};
// Write a fully-assembled output row (grayscale bytes, length outWidth) to BMP
static void writeOutputRow(BmpConvertCtx* ctx, const uint8_t* srcRow, int outY) {
memset(ctx->bmpRow, 0, ctx->bytesPerRow);
memset(ctx->bmpRow.get(), 0, ctx->bytesPerRow);
if (USE_8BIT_OUTPUT && !ctx->oneBit) {
for (int x = 0; x < ctx->outWidth; x++) {
@@ -262,12 +262,12 @@ static void writeOutputRow(BmpConvertCtx* ctx, const uint8_t* srcRow, int outY)
ctx->fsDitherer->nextRow();
}
ctx->bmpOut->write(ctx->bmpRow, ctx->bytesPerRow);
ctx->bmpOut->write(ctx->bmpRow.get(), ctx->bytesPerRow);
}
// Flush one scaled output row from Y-axis accumulators and advance currentOutY
static void flushScaledRow(BmpConvertCtx* ctx) {
memset(ctx->bmpRow, 0, ctx->bytesPerRow);
memset(ctx->bmpRow.get(), 0, ctx->bytesPerRow);
if (USE_8BIT_OUTPUT && !ctx->oneBit) {
for (int x = 0; x < ctx->outWidth; x++) {
@@ -301,7 +301,7 @@ static void flushScaledRow(BmpConvertCtx* ctx) {
ctx->fsDitherer->nextRow();
}
ctx->bmpOut->write(ctx->bmpRow, ctx->bytesPerRow);
ctx->bmpOut->write(ctx->bmpRow.get(), ctx->bytesPerRow);
ctx->currentOutY++;
}
@@ -324,7 +324,7 @@ int bmpDrawCallback(JPEGDRAW* pDraw) {
for (int r = 0; r < blockH && r < MAX_MCU_HEIGHT; r++) {
const int copyW = (blockX + validW <= ctx->srcWidth) ? validW : (ctx->srcWidth - blockX);
if (copyW <= 0) continue;
memcpy(ctx->mcuBuf + r * ctx->srcWidth + blockX, pixels + r * stride, copyW);
memcpy(ctx->mcuBuf.get() + r * ctx->srcWidth + blockX, pixels + r * stride, copyW);
}
// Wait for the last MCU column before processing any rows
@@ -334,7 +334,7 @@ int bmpDrawCallback(JPEGDRAW* pDraw) {
const int endRow = blockY + blockH;
for (int y = blockY; y < endRow && y < ctx->srcHeight; y++) {
const uint8_t* srcRow = ctx->mcuBuf + (y - blockY) * ctx->srcWidth;
const uint8_t* srcRow = ctx->mcuBuf.get() + (y - blockY) * ctx->srcWidth;
if (!ctx->needsScaling) {
// 1:1 — outWidth == srcWidth, write directly
@@ -364,8 +364,8 @@ int bmpDrawCallback(JPEGDRAW* pDraw) {
flushScaledRow(ctx);
ctx->nextOutY_srcStart = static_cast<uint32_t>(ctx->currentOutY + 1) * ctx->scaleY_fp;
if (srcY_fp >= ctx->nextOutY_srcStart) continue;
memset(ctx->rowAccum, 0, ctx->outWidth * sizeof(uint32_t));
memset(ctx->rowCount, 0, ctx->outWidth * sizeof(uint32_t));
memset(ctx->rowAccum.get(), 0, ctx->outWidth * sizeof(uint32_t));
memset(ctx->rowCount.get(), 0, ctx->outWidth * sizeof(uint32_t));
}
}
}
@@ -387,19 +387,20 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
s_jpegFile = &jpegFile;
JPEGDEC* jpeg = new (std::nothrow) JPEGDEC();
const auto jpeg = makeUniqueNoThrow<JPEGDEC>();
if (!jpeg) {
LOG_ERR("JPG", "Failed to allocate JPEG decoder");
LOG_ERR("JPG", "OOM: JPEG decoder");
return false;
}
int rc = jpeg->open("", bmpJpegOpen, bmpJpegClose, bmpJpegRead, bmpJpegSeek, bmpDrawCallback);
if (rc != 1) {
LOG_ERR("JPG", "JPEG open failed (err=%d)", jpeg->getLastError());
delete jpeg;
return false;
}
const ScopedCleanup cleanup{[&jpeg]() { jpeg->close(); }};
const int srcWidth = jpeg->getWidth();
const int srcHeight = jpeg->getHeight();
@@ -411,8 +412,6 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
if (srcWidth <= 0 || srcHeight <= 0 || srcWidth > MAX_IMAGE_WIDTH || srcHeight > MAX_IMAGE_HEIGHT) {
LOG_DBG("JPG", "Image too large or invalid (%dx%d), max supported: %dx%d", srcWidth, srcHeight, MAX_IMAGE_WIDTH,
MAX_IMAGE_HEIGHT);
jpeg->close();
delete jpeg;
return false;
}
@@ -472,54 +471,49 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
ctx.scaleY_fp = scaleY_fp;
ctx.error = false;
// RAII guard: frees all heap resources on any return path
struct Cleanup {
BmpConvertCtx& ctx;
JPEGDEC* jpeg;
~Cleanup() {
delete[] ctx.rowAccum;
delete[] ctx.rowCount;
delete ctx.atkinsonDitherer;
delete ctx.fsDitherer;
delete ctx.atkinson1BitDitherer;
free(ctx.mcuBuf);
free(ctx.bmpRow);
jpeg->close();
delete jpeg;
}
} cleanup{ctx, jpeg};
// MCU row buffer: MAX_MCU_HEIGHT rows × srcWidth columns of grayscale
ctx.mcuBuf = static_cast<uint8_t*>(malloc(MAX_MCU_HEIGHT * srcWidth));
ctx.mcuBuf = makeUniqueNoThrow<uint8_t[]>(MAX_MCU_HEIGHT * srcWidth);
if (!ctx.mcuBuf) {
LOG_ERR("JPG", "Failed to allocate MCU buffer (%d bytes)", MAX_MCU_HEIGHT * srcWidth);
LOG_ERR("JPG", "OOM: MCU buffer (%d bytes)", MAX_MCU_HEIGHT * srcWidth);
return false;
}
memset(ctx.mcuBuf, 0, MAX_MCU_HEIGHT * srcWidth);
memset(ctx.mcuBuf.get(), 0, MAX_MCU_HEIGHT * srcWidth);
ctx.bmpRow = static_cast<uint8_t*>(malloc(bytesPerRow));
ctx.bmpRow = makeUniqueNoThrow<uint8_t[]>(bytesPerRow);
if (!ctx.bmpRow) {
LOG_ERR("JPG", "Failed to allocate BMP row buffer");
LOG_ERR("JPG", "OOM: BMP row buffer");
return false;
}
if (needsScaling) {
ctx.rowAccum = new (std::nothrow) uint32_t[outWidth]();
ctx.rowCount = new (std::nothrow) uint32_t[outWidth]();
ctx.rowAccum = makeUniqueNoThrow<uint32_t[]>(outWidth);
ctx.rowCount = makeUniqueNoThrow<uint32_t[]>(outWidth);
if (!ctx.rowAccum || !ctx.rowCount) {
LOG_ERR("JPG", "Failed to allocate scaling buffers");
LOG_ERR("JPG", "OOM: scaling buffers");
return false;
}
ctx.nextOutY_srcStart = scaleY_fp;
}
if (oneBit) {
ctx.atkinson1BitDitherer = new (std::nothrow) Atkinson1BitDitherer(outWidth);
ctx.atkinson1BitDitherer = makeUniqueNoThrow<Atkinson1BitDitherer>(outWidth);
if (!ctx.atkinson1BitDitherer) {
LOG_ERR("JPG", "OOM: Atkinson1BitDitherer");
return false;
}
} else if (!USE_8BIT_OUTPUT) {
if (USE_ATKINSON) {
ctx.atkinsonDitherer = new (std::nothrow) AtkinsonDitherer(outWidth);
ctx.atkinsonDitherer = makeUniqueNoThrow<AtkinsonDitherer>(outWidth);
if (!ctx.atkinsonDitherer) {
LOG_ERR("JPG", "OOM: AtkinsonDitherer");
return false;
}
} else if (USE_FLOYD_STEINBERG) {
ctx.fsDitherer = new (std::nothrow) FloydSteinbergDitherer(outWidth);
ctx.fsDitherer = makeUniqueNoThrow<FloydSteinbergDitherer>(outWidth);
if (!ctx.fsDitherer) {
LOG_ERR("JPG", "OOM: FloydSteinbergDitherer");
return false;
}
}
}
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <cstddef>
#include <memory>
#include <new>
#include <type_traits>
#include <utility>
// Nothrow versions of std::make_unique. Return nullptr on allocation failure
// instead of calling abort() (the default when exceptions are disabled on ESP32).
//
// Single object:
// auto obj = makeUniqueNoThrow<PNG>();
// if (!obj) { LOG_ERR("TAG", "OOM"); return false; }
//
// Array:
// auto buf = makeUniqueNoThrow<uint8_t[]>(size);
// if (!buf) { LOG_ERR("TAG", "OOM"); return false; }
// buf[0] = 0xFF;
// someApi(buf.get(), size);
//
template <typename T, typename... Args>
requires(!std::is_array_v<T>)
std::unique_ptr<T> makeUniqueNoThrow(Args&&... args) {
return std::unique_ptr<T>(new (std::nothrow) T(std::forward<Args>(args)...));
}
template <typename T>
requires std::is_unbounded_array_v<T>
std::unique_ptr<T> makeUniqueNoThrow(size_t count) {
using Elem = std::remove_extent_t<T>;
return std::unique_ptr<T>(new (std::nothrow) Elem[count]());
}
// Helper struct to call a cleanup function on exit from any scope.
// Use with a lambda to avoid unnecessary allocations from std::function/std::bind:
// Example:
// auto jpeg = makeUniqueNoThrow<JPEGDEC>();
// ScopedCleanup cleanup{[&jpeg]{ jpeg->close(); }};
//
template <typename F>
struct [[nodiscard]] ScopedCleanup final {
const F fn;
explicit ScopedCleanup(F f) : fn{std::move(f)} {}
ScopedCleanup(const ScopedCleanup&) = delete;
ScopedCleanup& operator=(const ScopedCleanup&) = delete;
ScopedCleanup(ScopedCleanup&&) = delete;
ScopedCleanup& operator=(ScopedCleanup&&) = delete;
~ScopedCleanup() { fn(); }
};
template <typename F>
ScopedCleanup(F) -> ScopedCleanup<F>;