Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions include/silk/util/error.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
#pragma once

#include <cstdint>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

namespace silk
{

/**
* Rich error description that travels alongside an errno-style code returned by a function.
* An Error is a stack of frames: the top frame is the most recent, the bottom is the innermost
* failure that started the chain. All frame storage (headers + message bytes) lives in a single
* arena owned by the Error.
*
* Errors are normally produced through the RETURN_ERROR / CHECK_ERROR / CHECK_BOOL macros,
* which push a frame and capture the call-site file and line. Functions take an Error * error
* out-parameter and the caller declares the Error on its own stack:
*
* Error error;
* int r = doWork(&error);
* if (r) {
* logger->log(error.format());
* }
*
* Callers must hand in a clean Error. The macros only push: any pre-existing frames remain
* underneath the new top frame. Use clear between independent uses of the same Error
* (e.g., a loop dispatching independent operations).
*
* To walk the stack, follow the chain from top via next:
*
* for (const Error::Frame * frame = error.top(); frame; frame = error.next(frame)) {
* use(frame->code, frame->file, frame->line, frame->message());
* }
*
* Frame pointers stay valid until the next push/clear/move/destroy on the owning Error.
*/
class Error
{
public:
/**
* One frame stored in the arena. The message bytes follow inline immediately after the header
* and are always NUL-terminated (the byte one past the last message byte is '\0'), so callers
* may pass message().data() to C APIs expecting a NUL-terminated string.
*/
struct Frame
{
uint32_t nextOffset; // arena offset of the next-deeper frame, or SENTINEL at the bottom
uint32_t msgLen;
const char * file;
int code;
int line;

/** Inline message bytes following this header (NUL-terminated; the NUL is not counted). */
std::string_view message() const noexcept { return std::string_view(reinterpret_cast<const char *>(this + 1), msgLen); }
};
static_assert(sizeof(Frame) == 24);

/** Maximum length of a single frame's message in bytes; longer messages are truncated. */
static constexpr size_t MAX_MESSAGE_LEN = 512;

/** Construct an empty Error with no frames; no heap allocation until the first push. */
Error() noexcept = default;

// non-copyable
Error(const Error &) = delete;
Error & operator=(const Error &) = delete;

// moveable; source is left empty (no frames, freshly default-constructed state)
Error(Error && other) noexcept
: arena(std::move(other.arena))
, topOffset(std::exchange(other.topOffset, SENTINEL))
{
}
Error & operator=(Error && other) noexcept
{
arena = std::move(other.arena);
topOffset = std::exchange(other.topOffset, SENTINEL);
return *this;
}

/** True if no frames have been pushed yet. */
bool empty() const noexcept { return topOffset == SENTINEL; }

/** errno-style code of the top frame; 0 if empty. */
int code() const noexcept { return topOffset == SENTINEL ? 0 : frameAt(topOffset)->code; }

/** Pointer to the top of the stack (most recent frame); nullptr if empty. */
const Frame * top() const noexcept { return topOffset == SENTINEL ? nullptr : frameAt(topOffset); }

/** Pointer to the next-deeper frame; nullptr if frame is the innermost. */
const Frame * next(const Frame * frame) const noexcept { return frame->nextOffset == SENTINEL ? nullptr : frameAt(frame->nextOffset); }

/** Push one frame on top of the existing stack with a plain message. Returns code. */
int push(int code, const char * file, int line, std::string_view message) noexcept;

/** printf-style variant of push: vsnprintf's format + varargs into the frame's message. */
int pushf(int code, const char * file, int line, const char * format, ...) noexcept __attribute__((format(printf, 5, 6)));

/**
* Render the stack as one human-readable string. The top frame prints first; deeper frames are
* prefixed with "caused by: ". Each frame is <file>:<line>: <message> (errno=<code>[:
* <description>]). Best-effort: returns whatever was built so far if allocation fails partway.
*/
std::string format() const noexcept;

/** Discard all frames. */
void clear() noexcept
{
arena.clear();
topOffset = SENTINEL;
}

private:
/** Marker stored in topOffset / Frame::nextOffset to indicate "no frame". */
static constexpr uint32_t SENTINEL = UINT32_MAX;

/** Reserve space for a new top frame; returns a pointer to the message slot, or nullptr on OOM. */
char * reserve(int code, const char * file, int line, size_t msgLen) noexcept;

/** Interpret arena bytes at offset as a Frame. */
const Frame * frameAt(uint32_t offset) const noexcept { return reinterpret_cast<const Frame *>(arena.data() + offset); }

//
// State.
//

std::vector<uint8_t> arena;
uint32_t topOffset = SENTINEL;
};

} // namespace silk

/**
* Unconditional return-and-push: push a fresh frame on top of *error and return code. Captures
* the call-site file and line. Use when an error condition is detected directly (not propagated
* from a callee returning a code):
*
* if (input == nullptr) {
* RETURN_ERROR(EINVAL, error, "null input: idx=%d", idx);
* }
*/
#define RETURN_ERROR(code, error, msg, ...) return (error)->push##__VA_OPT__(f)((code), __FILE__, __LINE__, msg __VA_OPT__(, ) __VA_ARGS__)

/**
* Failure propagation: if r is non-zero, push a new frame on top of *error and return r.
* Works uniformly for callees that populate *error (the existing stack is preserved underneath)
* and for callees that do not (the frame becomes the leaf). Callers must hand in a clean Error;
* see the class doc.
*
* int r = volume->allocateLsn(&lsn);
* CHECK_ERROR(r, error, "could not allocate LSN");
*
* int r = store->getRowBlockReference(rbn, time, rowFunctions, &reference, error);
* CHECK_ERROR(r, error, "could not get row-block reference: pgId=%u", rbn.pgId);
*/
#define CHECK_ERROR(code, error, msg, ...) \
do \
{ \
if ((code) != 0) [[unlikely]] \
{ \
return (error)->push##__VA_OPT__(f)((code), __FILE__, __LINE__, msg __VA_OPT__(, ) __VA_ARGS__); \
} \
} while (0)

/**
* Boolean failure: if cond is false, push a fresh frame on top of *error and return code.
*
* NEVER embed a function call as cond -- capture into a bool b temp first:
* bool b = record.addRow(&context, recordRow);
* CHECK_BOOL(b, ENOSPC, error, "could not append row");
*/
#define CHECK_BOOL(cond, code, error, msg, ...) \
do \
{ \
if (!(cond)) [[unlikely]] \
{ \
return (error)->push##__VA_OPT__(f)((code), __FILE__, __LINE__, msg __VA_OPT__(, ) __VA_ARGS__); \
} \
} while (0)
123 changes: 123 additions & 0 deletions src/util/error.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#include <silk/util/error.h>

#include <silk/util/platform.h>

#include <algorithm>
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <memory>
#include <string>
#include <string_view>

namespace silk
{

// std::vector<uint8_t>::data comes from ::operator new, which on hosted implementations is
// aligned to __STDCPP_DEFAULT_NEW_ALIGNMENT__. We need that to be at least alignof(Frame) so that
// Frames placed at offset 0 are aligned; record-padding inside reserve keeps subsequent
// Frames aligned as well.
static_assert(__STDCPP_DEFAULT_NEW_ALIGNMENT__ >= alignof(Error::Frame), "arena base must satisfy Frame alignment");

char * Error::reserve(int code, const char * file, int line, size_t msgLen) noexcept
{
// Record size is the Frame header + message + 1 byte reserved for vsnprintf's NUL terminator
// (unused on plain-message paths). Pad to 8 bytes so consecutive Frames stay 8-aligned.
size_t recordSize = sizeof(Frame) + msgLen + 1;
size_t alignedRecordSize = alignUp<size_t>(recordSize, 8);

size_t newOffset = arena.size();
if (newOffset >= SENTINEL)
{
return nullptr;
}

try
{
arena.resize(newOffset + alignedRecordSize);
}
catch (...)
{
return nullptr;
}

Frame * frame = std::construct_at(
reinterpret_cast<Frame *>(arena.data() + newOffset), Frame{topOffset, static_cast<uint32_t>(msgLen), file, code, line});

topOffset = static_cast<uint32_t>(newOffset);
return reinterpret_cast<char *>(frame + 1);
}

int Error::push(int code, const char * file, int line, std::string_view message) noexcept
{
size_t msgLen = std::min(message.size(), MAX_MESSAGE_LEN);

char * dest = reserve(code, file, line, msgLen);
if (dest)
{
std::memcpy(dest, message.data(), msgLen);
dest[msgLen] = '\0';
}
return code;
}

int Error::pushf(int code, const char * file, int line, const char * format, ...) noexcept
{
char buffer[MAX_MESSAGE_LEN + 1];

std::va_list args;
va_start(args, format);
int written = std::vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);

if (written < 0)
{
written = 0;
}

return push(code, file, line, std::string_view(buffer, static_cast<size_t>(written)));
}

std::string Error::format() const noexcept
{
std::string result;
try
{
bool first = true;
for (const Frame * frame = top(); frame; frame = next(frame))
{
if (!first)
{
result += "\n caused by: ";
}
first = false;

if (frame->file)
{
result += frame->file;
result += ':';
result += std::to_string(frame->line);
result += ": ";
}

std::string_view message = frame->message();
result.append(message.data(), message.size());

result += " (errno=";
result += std::to_string(frame->code);
const char * description = strerrordesc_np(frame->code);
if (description)
{
result += ": ";
result += description;
}
result += ')';
}
}
catch (...)
{
}
return result;
}

} // namespace silk
Loading
Loading