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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>;
|
||||
Reference in New Issue
Block a user