Devlog #5: Error Handling Overhaul (Commits: 52515268fe-91402e3215)
18 changed files with 1153 additions and 547 deletions
I wasn’t happy with the old error handling. A flat enum doesn’t tell you what went wrong or how bad it is. So I tore it out and replaced it. I’m proud of this module because it uses modern C23 features everywhere you look, is fast, secure, and gives me the important debugging info in case something goes wrong.
Thread-safe error and diagnostic module (52515268fe)
This commit contains the initial implementation of the module, although with a few bugs I addressed in one of the two later commits.
Warning! This commit DOES NOT compile. I haven’t refactored the codebase to use the new error handling implemented in this commit.
Bit-packed error codes
The new zinn_err_t is a uint32_t with three fields packed into it:
[8 bits domain | 4 bits severity | 20 bits code]
- Domain identifies the module.
- Severity says how bad it is.
- Code is the specific error number.
Diagnostic stack
A thread_local fixed-depth LIFO stack (max 16 frames, 128 message chars each). Every frame records file, function, line, the packed error code, a human-readable message, and a unix timestamp.
Pushing a frame manually is tedious, so I wrote macros that inject them for you.
Higher-level macros build on this:
-
ZINN_RETURN_IF(condition, error): if true, push a frame and return the error -
ZINN_RETURN_IF_ERR(expression): if the expression fails, push and return -
ZINN_TRY(expression): same, but evaluates to the expression’s value on success -
ZINN_CHECK_ERROR(call, message, ...): push a frame if error, but keep going
zinn_diagnostics_dump_frames() dumps the whole stack to stderr with colored formatting, newest first.
Check the attachment to see the stack dump in a terminal. Contains all the info you’d ever need.
Scope guard
C doesn’t have RAII. But GCC has __attribute__((cleanup)). The scope guard module wraps it:
ZINN_DEFER_CLEANUP(function, pointer)
This declares a local guard that calls function(pointer) when the scope exits, whether by normal return or early return. You can disarm it with ZINN_SCOPE_GUARD_DISARM(name) if you transfer ownership.
The logger now uses this for mutex cleanup.
The old code scattered manual pthread_mutex_unlock() calls across every return path. One missed unlock would deadlock the whole process.
Refactoring the codebase (de25c634a0, 91402e3215)
The codebase was refactored to use the new error handling and diagnostic functions, macros and types.
A few examples:
-
Logger: returns
zinn_err_teverywhere; precondition checks useZINN_RETURN_IF; mutex handling uses scope guards -
Arena:
zinn_arena_createchanged from returning a pointer to returning an error code with an output parameter. Every error site now pushes a diagnostic frame with details -
Tests: adapted to the new API. Fixed several real bugs along the way: missing
fclose()calls that leaked file descriptors and a renamed internal function amongst a few others
The test count also dropped from 50 to 49 Logger tests because I removed a redundant test.
What’s next?
With the error handler now functional, it will be finally time for the (probably) final core mod-… oh wait. Nevermind that. Next I have to write a ton of unit tests for this new module. Lucky me!
Comments 0
No comments yet. Be the first!
Sign in to join the conversation.