akerr_reserve_status_range() and akerr_register_status_name() returned private int enumerations, which was the one place in the library where a failure was not an akerr_ErrorContext *. They now return one like everything else: NULL on success, and on refusal an error whose status is a real code in the library's reserved band, so it can be CATCH-ed, HANDLE-d, PASS-ed, or left to propagate into a stack trace. Both are marked AKERR_NOIGNORE, so discarding the result warns at compile time. AKERR_STATUS_RANGE_OK and AKERR_STATUS_NAME_OK are gone; the remaining seven codes move into the AKERR_* offset span and get registered names. AKERR_LAST_LIBRARY_STATUS replaces AKERR_BADEXC as the top of that span in the reserved-band static assert and the exhaustiveness sweep. The refusal detail that used to go straight to akerr_log_method now travels in the error message, so a caller that handles the error decides whether it is reported. The two-argument akerr_name_for_status() set path is the exception: it returns a name and cannot raise, so it logs and releases. akerr_init() likewise has no caller to raise into, so failing to reserve its own band or name its own codes is logged and fatal -- that can only happen on a misconfigured build, and continuing would degrade every later stack trace to "Unknown Error". Move the 1.0.0 upgrade notice out of README.md into UPGRADING.md and rewrite its return-code tables in terms of the statuses now raised. Tests: ctest 29/29, coverage 97.5% line / 64.5% branch, mutation 77.5% (was 77.6%; the new survivors are the fatal init path, which needs a library built with an undersized name table -- TODO item 7). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
356 lines
16 KiB
C
356 lines
16 KiB
C
#ifndef _AKERR_H_
|
|
#define _AKERR_H_
|
|
|
|
#if (defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1) || (!defined(AKERR_USE_STDLIB))
|
|
#include <stdlib.h>
|
|
#include <stdbool.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <limits.h>
|
|
#endif
|
|
|
|
// FIXME: This is huge now. It used to be 1000 bytes, then I wanted to report errors
|
|
// related to filesystem paths, which made it grow beyond PATH_MAX, then I started
|
|
// reporting messages including 2 file paths (PATH_MAX * 2), so now to make the compiler warnings
|
|
// shut up, it's enormous (PATH_MAX*3).
|
|
#define AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH 12384
|
|
|
|
#define AKERR_MAX_ERROR_NAME_LENGTH 64
|
|
#define AKERR_MAX_ERROR_FNAME_LENGTH PATH_MAX
|
|
#define AKERR_MAX_ERROR_FUNCTION_LENGTH 128
|
|
#define AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH (AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH + AKERR_MAX_ERROR_NAME_LENGTH + AKERR_MAX_ERROR_FNAME_LENGTH + AKERR_MAX_ERROR_FUNCTION_LENGTH + 16)
|
|
|
|
#define AKERR_LAST_ERRNO_VALUE AKERR_LAST_ERRNO_VALUE_SED
|
|
|
|
#define AKERR_NULLPOINTER (AKERR_LAST_ERRNO_VALUE + 1) /** A pointer had a NULL value where such was not permissible */
|
|
#define AKERR_OUTOFBOUNDS (AKERR_LAST_ERRNO_VALUE + 2) /** Attempt to access a datastructure outside of bounds */
|
|
#define AKERR_API (AKERR_LAST_ERRNO_VALUE + 3) /** An otherwise unspecified API contract has been violated */
|
|
#define AKERR_ATTRIBUTE (AKERR_LAST_ERRNO_VALUE + 4) /** Relates to accessing of attributes on objects */
|
|
#define AKERR_TYPE (AKERR_LAST_ERRNO_VALUE + 5) /** An object had the incorrect type */
|
|
#define AKERR_KEY (AKERR_LAST_ERRNO_VALUE + 6) /** A key was either invalid for or not present in a map */
|
|
#define AKERR_INDEX (AKERR_LAST_ERRNO_VALUE + 8) /** An error occurred when attempting to index an indexable datastructure (other than out of bounds) */
|
|
#define AKERR_FORMAT (AKERR_LAST_ERRNO_VALUE + 9) /** An error occurred in the formatting of an object (usually a string) */
|
|
#define AKERR_IO (AKERR_LAST_ERRNO_VALUE + 10) /** An unspecified IO error occurred. */
|
|
#define AKERR_VALUE (AKERR_LAST_ERRNO_VALUE + 11) /** A provided value was invalid */
|
|
#define AKERR_RELATIONSHIP (AKERR_LAST_ERRNO_VALUE + 12) /** An error occurred in establishing, maintaining or severing a relationship between two objects */
|
|
#define AKERR_EOF (AKERR_LAST_ERRNO_VALUE + 13) /** The end of a stream or file has been encountered */
|
|
#define AKERR_CIRCULAR_REFERENCE (AKERR_LAST_ERRNO_VALUE + 14) /** Indicates that a circular reference has been found in a linked list */
|
|
#define AKERR_ITERATOR_BREAK (AKERR_LAST_ERRNO_VALUE + 15) /** Used to prematurely end an iteration cycle (such as when searching a graph and the desired node has been found) */
|
|
#define AKERR_NOT_IMPLEMENTED (AKERR_LAST_ERRNO_VALUE + 16) /** A method was called that is defined but not currently implemented */
|
|
#define AKERR_BADEXC (AKERR_LAST_ERRNO_VALUE + 17) /** The libakerr library was given an akerr_ErrorContext to parse that did not come from AKERR_ARRAY_ERROR (likely an uninitialized pointer) */
|
|
|
|
/*
|
|
* Registry failures. These are ordinary status codes, not a private return
|
|
* enumeration: akerr_reserve_status_range() and akerr_register_status_name()
|
|
* return akerr_ErrorContext * like everything else in this library, so a refused
|
|
* reservation can be CATCH-ed, HANDLE-d, PASS-ed, or left unhandled to produce a
|
|
* stack trace and stop the program. Success returns NULL.
|
|
*/
|
|
#define AKERR_STATUS_RANGE_OVERLAP (AKERR_LAST_ERRNO_VALUE + 18) /** Some part of the range is already owned by someone else */
|
|
#define AKERR_STATUS_RANGE_FULL (AKERR_LAST_ERRNO_VALUE + 19) /** No reservation slots remain (see AKERR_MAX_RESERVED_STATUS_RANGES) */
|
|
#define AKERR_STATUS_RANGE_INVALID (AKERR_LAST_ERRNO_VALUE + 20) /** Bad count, bad owner string, or the range overflows int */
|
|
#define AKERR_STATUS_NAME_UNRESERVED (AKERR_LAST_ERRNO_VALUE + 21) /** No owner has reserved a range containing this status */
|
|
#define AKERR_STATUS_NAME_FOREIGN (AKERR_LAST_ERRNO_VALUE + 22) /** The status lies in a range reserved by a different owner */
|
|
#define AKERR_STATUS_NAME_FULL (AKERR_LAST_ERRNO_VALUE + 23) /** The name registry is full (raise AKERR_STATUS_NAME_SLOTS) */
|
|
#define AKERR_STATUS_NAME_INVALID (AKERR_LAST_ERRNO_VALUE + 24) /** NULL/empty/over-long owner, or a NULL name */
|
|
|
|
/* The last status the library defines for itself. Everything from
|
|
* AKERR_LAST_ERRNO_VALUE + 1 through here must have a registered name. */
|
|
#define AKERR_LAST_LIBRARY_STATUS AKERR_STATUS_NAME_INVALID
|
|
|
|
/*
|
|
* Status values 0 through 255 are reserved by libakerror at akerr_init() time:
|
|
* the host's errno values plus the AKERR_* codes above. Consumers allocate from
|
|
* 256 upwards. Reserving any part of this band fails with
|
|
* AKERR_STATUS_RANGE_OVERLAP naming AKERR_LIBRARY_OWNER.
|
|
*/
|
|
#define AKERR_LIBRARY_OWNER "libakerror"
|
|
#define AKERR_RESERVED_STATUS_COUNT 256
|
|
#define AKERR_FIRST_CONSUMER_STATUS AKERR_RESERVED_STATUS_COUNT
|
|
|
|
/*
|
|
* The library reserves status values 0 through 255 for itself (see akerr_init),
|
|
* which must contain every AKERR_* code above. AKERR_LAST_ERRNO_VALUE is
|
|
* derived from the host's errno list at build time, so on a platform with an
|
|
* unusually large errno space these codes could escape the band and collide
|
|
* with consumer codes allocated at 256. Fail the build instead.
|
|
*/
|
|
typedef char akerr_assert_codes_within_reserved_band[(AKERR_LAST_LIBRARY_STATUS < 256) ? 1 : -1];
|
|
|
|
#define AKERR_MAX_ARRAY_ERROR 128
|
|
|
|
|
|
typedef struct
|
|
{
|
|
char message[AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH];
|
|
int arrayid;
|
|
int status;
|
|
bool handled;
|
|
int refcount;
|
|
char fname[AKERR_MAX_ERROR_FNAME_LENGTH];
|
|
char function[AKERR_MAX_ERROR_FNAME_LENGTH];
|
|
int lineno;
|
|
bool reported;
|
|
char stacktracebuf[AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH];
|
|
char *stacktracebufptr;
|
|
} akerr_ErrorContext;
|
|
|
|
#define AKERR_NOIGNORE __attribute__((warn_unused_result))
|
|
|
|
typedef void (*akerr_ErrorUnhandledErrorHandler)(akerr_ErrorContext *errctx);
|
|
typedef void (*akerr_ErrorLogFunction)(const char *f, ...);
|
|
|
|
extern akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR];
|
|
extern akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
|
|
extern akerr_ErrorLogFunction akerr_log_method;
|
|
extern akerr_ErrorContext *__akerr_last_ignored;
|
|
|
|
akerr_ErrorContext AKERR_NOIGNORE *akerr_release_error(akerr_ErrorContext *ptr);
|
|
akerr_ErrorContext AKERR_NOIGNORE *akerr_next_error();
|
|
/*
|
|
* Look up (name == NULL) or register (name != NULL) the display name for a
|
|
* status. Registration succeeds only if some owner has reserved a range
|
|
* containing `status`; prefer akerr_register_status_name(), which also checks
|
|
* that the range belongs to you and raises an error saying why a registration
|
|
* was refused. This entry point cannot return an error context, so a refusal
|
|
* here is reported through akerr_log_method and reads back as "Unknown Error".
|
|
* Never returns NULL -- an unregistered status reads back as "Unknown Error".
|
|
*/
|
|
char *akerr_name_for_status(int status, char *name);
|
|
|
|
/*
|
|
* Register a display name for a status you own. `owner` must match the owner
|
|
* string passed to akerr_reserve_status_range() for the range containing
|
|
* `status`. Returns NULL on success, or an error context whose status is one of
|
|
* the AKERR_STATUS_NAME_* codes above -- CATCH it, HANDLE it, or let it
|
|
* propagate.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akerr_register_status_name(const char *owner, int status, const char *name);
|
|
|
|
/*
|
|
* Claim `count` status values starting at `first_status` for `owner`. Repeating
|
|
* an identical reservation for the same owner is a no-op; any other collision is
|
|
* refused. Returns NULL on success, or an error context whose status is one of
|
|
* the AKERR_STATUS_RANGE_* codes above. Treat any error as an initialization
|
|
* failure: either handle it or let it propagate out of your init function.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akerr_reserve_status_range(int first_status, int count, const char *owner);
|
|
void akerr_init();
|
|
void akerr_default_handler_unhandled_error(akerr_ErrorContext *ptr);
|
|
void akerr_default_logger(const char *f, ...);
|
|
int akerr_valid_error_address(akerr_ErrorContext *ptr);
|
|
/* defined in src/errno.c which is built dynamically at build time from system errno definitions */
|
|
void akerr_init_errno(void);
|
|
/*
|
|
* Internal. Names a status in the library's own reserved band on behalf of
|
|
* akerr_init() and the generated errno table, which have no caller to raise
|
|
* into: a failure here is logged and terminates the program. Not part of the
|
|
* consumer API -- use akerr_register_status_name(), which raises instead.
|
|
*/
|
|
void __akerr_name_library_status(int status, const char *name);
|
|
|
|
#define LOG_ERROR_WITH_MESSAGE(__err_context, __err_message) \
|
|
akerr_log_method("%s%s:%s:%d: %s %d (%s): %s", (char *)&__err_context->stacktracebuf, (char *)__FILE__, (char *)__func__, __LINE__, __err_message, __err_context->status, akerr_name_for_status(__err_context->status, NULL), __err_context->message); \
|
|
|
|
#define LOG_ERROR(__err_context) \
|
|
LOG_ERROR_WITH_MESSAGE(__err_context, "");
|
|
|
|
#define RELEASE_ERROR(__err_context) \
|
|
if ( __err_context != NULL ) { \
|
|
__err_context = akerr_release_error(__err_context); \
|
|
}
|
|
|
|
#define PREPARE_ERROR(__err_context) \
|
|
akerr_init(); \
|
|
akerr_ErrorContext __attribute__ ((unused)) *__err_context = NULL;
|
|
|
|
#define ENSURE_ERROR_READY(__err_context) \
|
|
if ( __err_context == NULL ) { \
|
|
__err_context = akerr_next_error(); \
|
|
if ( __err_context == NULL ) { \
|
|
akerr_log_method("%s:%s:%d: Unable to pull an error context from the array!", __FILE__, (char *)__func__, __LINE__); \
|
|
exit(1); \
|
|
} \
|
|
__err_context->refcount += 1; \
|
|
}
|
|
|
|
/*
|
|
* Append a formatted line to the error's stack-trace buffer, bounded by the
|
|
* space that remains so a deep propagation chain cannot write past the end of
|
|
* stacktracebuf. snprintf reports the length it *would* have written, which on
|
|
* truncation exceeds what it actually wrote, so the cursor advance is clamped
|
|
* to the remaining space.
|
|
*/
|
|
#define AKERR_STACKTRACE_APPEND(__err_context, ...) \
|
|
do { \
|
|
char *__akerr_stb = (char *)__err_context->stacktracebuf; \
|
|
size_t __akerr_used = (size_t)(__err_context->stacktracebufptr - __akerr_stb); \
|
|
if ( __akerr_used < AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH ) { \
|
|
size_t __akerr_rem = AKERR_MAX_ERROR_STACKTRACE_BUF_LENGTH - __akerr_used; \
|
|
int __akerr_n = snprintf(__err_context->stacktracebufptr, __akerr_rem, __VA_ARGS__); \
|
|
if ( __akerr_n < 0 ) { \
|
|
__akerr_n = 0; \
|
|
} \
|
|
__err_context->stacktracebufptr += ((size_t)__akerr_n < __akerr_rem) \
|
|
? (size_t)__akerr_n : (__akerr_rem - 1); \
|
|
} \
|
|
} while ( 0 )
|
|
|
|
/*
|
|
* Failure and success methods for functions that return akerr_ErrorContext *
|
|
*/
|
|
|
|
#define FAIL_ZERO_RETURN(__err_context, __x, __err, __message, ...) \
|
|
if ( __x == 0 ) { \
|
|
FAIL(__err_context, __err, __message, ##__VA_ARGS__); \
|
|
return __err_context; \
|
|
}
|
|
|
|
#define FAIL_NONZERO_RETURN(__err_context, __x, __err, __message, ...) \
|
|
if ( __x != 0 ) { \
|
|
FAIL(__err_context, __err, __message, ##__VA_ARGS__); \
|
|
return __err_context; \
|
|
}
|
|
|
|
#define FAIL_RETURN(__err_context, __err, __message, ...) \
|
|
FAIL(__err_context, __err, __message, ##__VA_ARGS__); \
|
|
return __err_context;
|
|
|
|
#define SUCCEED_RETURN(__err_context) \
|
|
RELEASE_ERROR(__err_context); \
|
|
return NULL;
|
|
|
|
/*
|
|
* Failure and success methods for use inside of ATTEMPT() blocks
|
|
*/
|
|
|
|
#define FAIL_ZERO_BREAK(__err_context, __x, __err, __message, ...) \
|
|
if ( __x == 0 ) { \
|
|
FAIL(__err_context, __err, __message, ##__VA_ARGS__); \
|
|
break; \
|
|
}
|
|
|
|
#define FAIL_NONZERO_BREAK(__err_context, __x, __err, __message, ...) \
|
|
if ( __x != 0 ) { \
|
|
FAIL(__err_context, __err, __message, ##__VA_ARGS__); \
|
|
break; \
|
|
}
|
|
|
|
#define FAIL_BREAK(__err_context, __err_, __message, ...) \
|
|
FAIL(__err_context, __err_, __message, ##__VA_ARGS__); \
|
|
break;
|
|
|
|
#define SUCCEED_BREAK(__err_context) \
|
|
SUCCEED(__err_context); \
|
|
break;
|
|
|
|
/*
|
|
* General failure and success methods
|
|
*/
|
|
|
|
#define FAIL(__err_context, __err, __message, ...) \
|
|
ENSURE_ERROR_READY(__err_context); \
|
|
__err_context->status = __err; \
|
|
snprintf((char *)__err_context->fname, AKERR_MAX_ERROR_FNAME_LENGTH, "%s", __FILE__); \
|
|
snprintf((char *)__err_context->function, AKERR_MAX_ERROR_FUNCTION_LENGTH, "%s", __func__); \
|
|
__err_context->lineno = __LINE__; \
|
|
snprintf((char *)__err_context->message, AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH, __message, ## __VA_ARGS__); \
|
|
AKERR_STACKTRACE_APPEND(__err_context, "%s:%s:%d: %d (%s) : %s\n", (char *)__err_context->fname, (char *)__err_context->function, __err_context->lineno, __err_context->status, akerr_name_for_status(__err_context->status, NULL), (__err_context->message == NULL ? "" : __err_context->message));
|
|
|
|
|
|
#define SUCCEED(__err_context) \
|
|
ENSURE_ERROR_READY(__err_context); \
|
|
__err_context->status = 0;
|
|
|
|
/*
|
|
* Defines for the ATTEMPT/CATCH/CLEANUP/PROCESS/HANDLE/FINISH process
|
|
*/
|
|
|
|
#define ATTEMPT \
|
|
switch ( 0 ) { \
|
|
case 0: \
|
|
|
|
#define VALID(__err_context, __stmt) \
|
|
__stmt; \
|
|
if ( akerr_valid_error_address(__err_context) == 0 ) { \
|
|
__err_context = NULL; \
|
|
FAIL(__err_context, AKERR_BADEXC, "Received (akerr_ErrorContext *) from an invalid memory region. (Did the method finish without calling SUCCEED_RETURN?)"); \
|
|
}
|
|
|
|
#define DETECT(__err_context, __stmt) \
|
|
VALID(__err_context, __stmt); \
|
|
if ( __err_context != NULL ) { \
|
|
if ( __err_context->status != 0 ) { \
|
|
AKERR_STACKTRACE_APPEND(__err_context, "%s:%s:%d\n", (char *)__FILE__, (char *)__func__, __LINE__); \
|
|
break; \
|
|
} \
|
|
}
|
|
|
|
#define CATCH(__err_context, __stmt) \
|
|
DETECT(__err_context, __err_context = __stmt);
|
|
|
|
#define PASS(__err_context, __stmt) \
|
|
switch ( 0 ) { \
|
|
case 0: \
|
|
DETECT(__err_context, __err_context = __stmt); \
|
|
} \
|
|
FINISH_LOGIC(__err_context, true);
|
|
|
|
#define IGNORE(__stmt) \
|
|
__akerr_last_ignored = __stmt; \
|
|
if ( __akerr_last_ignored != NULL ) { \
|
|
LOG_ERROR_WITH_MESSAGE(__akerr_last_ignored, "** IGNORED ERROR **"); \
|
|
}
|
|
|
|
#define CLEANUP \
|
|
};
|
|
|
|
#define PROCESS(__err_context) \
|
|
if ( __err_context != NULL ) { \
|
|
switch ( __err_context->status ) { \
|
|
case 0: \
|
|
__err_context->handled = true;
|
|
|
|
#define HANDLE(__err_context, __err_status) \
|
|
break; \
|
|
case __err_status: \
|
|
__err_context->stacktracebufptr = (char *)&__err_context->stacktracebuf; \
|
|
__err_context->handled = true;
|
|
|
|
#define HANDLE_GROUP(__err_context, __err_status) \
|
|
case __err_status: \
|
|
__err_context->stacktracebufptr = (char *)&__err_context->stacktracebuf; \
|
|
__err_context->handled = true;
|
|
|
|
#define HANDLE_DEFAULT(__err_context) \
|
|
break; \
|
|
default: \
|
|
__err_context->stacktracebufptr = (char *)&__err_context->stacktracebuf; \
|
|
__err_context->handled = true;
|
|
|
|
#define FINISH_LOGIC(__err_context, __pass_up) \
|
|
if ( __err_context != NULL ) { \
|
|
if ( __err_context->handled == false && __pass_up == true ) { \
|
|
return __err_context; \
|
|
} \
|
|
} \
|
|
|
|
#define FINISH(__err_context, __pass_up) \
|
|
}; \
|
|
}; \
|
|
FINISH_LOGIC(__err_context, __pass_up) \
|
|
RELEASE_ERROR(__err_context);
|
|
|
|
#define FINISH_NORETURN(__err_context) \
|
|
}; \
|
|
}; \
|
|
if ( __err_context != NULL ) { \
|
|
if ( __err_context->handled == false ) { \
|
|
LOG_ERROR_WITH_MESSAGE(__err_context, "Unhandled Error"); \
|
|
akerr_handler_unhandled_error(__err_context); \
|
|
} \
|
|
} \
|
|
RELEASE_ERROR(__err_context);
|
|
|
|
#endif // _AKERR_H_
|