|
1 | | -# c_minilib_error |
| 1 | +# ⚡ cdk_error |
2 | 2 |
|
3 | | -**`c_minilib_error`** is a minimal and fast C library for structured error reporting. It supports lightweight stack tracing, optional formatted messages, and a circular ring buffer backend—all designed with embedded and low-overhead systems in mind. |
| 3 | +`cdk_error` is a small, modern C library that brings structured error handling into plain C code. |
| 4 | +It works much like `errno`, but adds richer context: not just an error code, but also a message and a lightweight backtrace showing where the error has been passed along. All of this comes without any heap allocations, making it safe and fast even for embedded or low-overhead systems. |
4 | 5 |
|
5 | | -## ✨ Features |
| 6 | +What is nice about `cdk_errno` is that on fast path it is as fast as normal `errno`, so if no error occured in the function there is no penalty hit. The difference show only on slow path so when error occurs: |
| 7 | +``` |
| 8 | +❯ ./build/example/bench |
| 9 | +5-lvl errno-trace avg: 32.8 ns |
| 10 | +5-lvl fmt errno-trace avg: (disabled by CDK_ERROR_OPTIMIZE) |
| 11 | +5-lvl int avg: 5.7 ns |
| 12 | +``` |
| 13 | + |
| 14 | +With `CDK_ERROR_OPTIMIZE` enabled `cdk_error` is like 6-8 times slower than normal errno. This performence hit is perfectly accaptable in most cases, because there will be plenty of other app critical processes wich will slow you more than this 32ns overhead. |
| 15 | + |
| 16 | +--- |
| 17 | + |
| 18 | +## 📦 Getting started |
| 19 | + |
| 20 | +The library is header-only. To use it, copy the single header file into **your own project or library**. Each project should keep its own copy to avoid sharing one global error state across unrelated code. |
| 21 | + |
| 22 | +Create a small wrapper header and add one `.c` file that defines the thread-local variables: |
| 23 | + |
| 24 | +```c |
| 25 | +// myerror.h |
| 26 | +#ifndef MYERROR_H |
| 27 | +#define MYERROR_H |
| 28 | + |
| 29 | +#define CDK_ERROR_FSTR_MAX 512 |
| 30 | +#define CDK_ERROR_BTRACE_MAX 32 |
| 31 | +#include "cdk_error.h" |
| 32 | + |
| 33 | +#endif |
| 34 | +``` |
| 35 | + |
| 36 | +```c |
| 37 | +// myerror.c |
| 38 | +#include "myerror.h" |
| 39 | + |
| 40 | +_Thread_local cdk_error_t cdk_errno = NULL; |
| 41 | +_Thread_local struct cdk_Error cdk_hidden_errno = {0}; |
| 42 | +``` |
| 43 | +
|
| 44 | +Every other file in your project just includes `myerror.h`. |
| 45 | +The `.c` file is required, because it provides the actual definitions of the thread-local globals; without it you’d only have declarations, and the linker would complain. |
| 46 | +
|
| 47 | +🔧 If you’d like to change the prefix (for example, from `cdk_` to `my_`), there’s a helper script: |
| 48 | +
|
| 49 | +```bash |
| 50 | +python3 tools/change_prefix.py \ |
| 51 | + --inf include/cdk_error.h \ |
| 52 | + --out my_error.h \ |
| 53 | + --old cdk --new my |
| 54 | +``` |
| 55 | + |
| 56 | +This produces a copy of the header with your own prefix, ready to drop into a project. |
| 57 | + |
| 58 | +--- |
| 59 | + |
| 60 | +## ❓ Why copy instead of link? |
| 61 | + |
| 62 | +Unlike traditional libraries, `cdk_error` is designed to be embedded into each project separately. The reason is simple: every library or program should have its **own private error state**. |
6 | 63 |
|
7 | | -- 📌 **Structured error objects**: Includes code, message, file, function, and line |
8 | | -- 🧵 **Inline call trace tracking**: Record stack frames using `cme_return()` |
9 | | -- 🧠 **Formatted errors**: Optional `printf`-style formatting |
10 | | -- ♻️ **Allocation-free runtime**: Uses a circular buffer, no dynamic allocations in hot paths |
11 | | -- 🧪 **Dump utilities**: Export errors to file or string buffer |
| 64 | +If multiple components linked against the same global `cdk_errno`, their errors could overwrite each other, leading to confusing or incorrect traces. By copying the header into your project (and optionally renaming the prefix), you guarantee isolation: errors raised inside your library stay inside your library, and don’t clash with others. |
12 | 65 |
|
13 | | -## 🔧 Usage |
| 66 | +--- |
| 67 | + |
| 68 | +## 💡 Usage |
| 69 | + |
| 70 | +Here’s the simplest way to use the errno-style API: |
14 | 71 |
|
15 | 72 | ```c |
16 | | -#include "c_minilib_error.h" |
| 73 | +#include <stdio.h> |
| 74 | +#include "myerror.h" |
17 | 75 |
|
18 | | -cme_error_t deep_error(void) { |
19 | | - return cme_errorf(42, "Sensor read failure: value=%d", -1); |
| 76 | +const char *nested_failing_func(void) { |
| 77 | + if (1) { |
| 78 | + cdk_errno = cdk_errnos(ENOBUFS, "My error"); |
| 79 | + return NULL; |
| 80 | + } |
| 81 | + return "All good"; |
20 | 82 | } |
21 | 83 |
|
22 | | -cme_error_t wrapped_error(void) { |
23 | | - return cme_return(deep_error()); |
| 84 | +int failing_func(void) { |
| 85 | + const char *s = nested_failing_func(); |
| 86 | + if (!s) { |
| 87 | + return cdk_ereturn(-1); |
| 88 | + } |
| 89 | + return 13; |
24 | 90 | } |
25 | 91 |
|
26 | | -int main(void) { |
27 | | - cme_init(); |
28 | | - |
29 | | - cme_error_t err = wrapped_error(); |
30 | | - if (err) { |
31 | | - char buf[1024]; |
32 | | - if (cme_error_dump_to_str(err, sizeof(buf), buf) == 0) |
33 | | - fprintf(stderr, "%s", buf); |
34 | | - cme_error_dump_to_file(err, "trace.log"); |
| 92 | +void api_entry(void) { |
| 93 | + int res = failing_func(); |
| 94 | + if (res < 0) { |
| 95 | + cdk_ewrap(); |
| 96 | + return; |
35 | 97 | } |
| 98 | +} |
36 | 99 |
|
37 | | - cme_destroy(); |
| 100 | +int main(void) { |
| 101 | + char buf[1024]; |
| 102 | + cdk_errno = 0; |
| 103 | + |
| 104 | + api_entry(); |
| 105 | + if (cdk_errno) { |
| 106 | + cdk_edumps(sizeof(buf), buf); |
| 107 | + printf("%s", buf); |
| 108 | + return -1; |
| 109 | + } |
38 | 110 | return 0; |
39 | 111 | } |
40 | 112 | ``` |
41 | 113 |
|
42 | | -## 🧱 API Overview |
| 114 | +The pattern is straightforward: when something fails, set `cdk_errno` with a code or message, return an error value, and wrap it if you need to add another frame. At the top level, dump the error to a buffer or file to see the full trace. |
| 115 | +
|
| 116 | +--- |
| 117 | +
|
| 118 | +### ⚡ Performance |
| 119 | +
|
| 120 | +One of the nice things about `cdk_errno` is that on the **fast path** it behaves just like plain `errno`: if no error occurs in a function, there’s no extra overhead at all. |
| 121 | +The difference only shows up on the **slow path**, when an error is actually created and propagated: |
| 122 | +
|
| 123 | +``` |
| 124 | +❯ ./build/example/bench |
| 125 | +5-lvl errno-trace avg: 32.8 ns |
| 126 | +5-lvl fmt errno-trace avg: (disabled by CDK_ERROR_OPTIMIZE) |
| 127 | +5-lvl int avg: 5.7 ns |
| 128 | +``` |
| 129 | +
|
| 130 | +With `CDK_ERROR_OPTIMIZE` enabled (which removes formatted errors), the library is about **6–8× slower than a plain `errno` write** when an error occurs. |
| 131 | +That sounds big, but remember: it’s \~32 nanoseconds to build a full 5-level trace. In practice this overhead is negligible compared to real application work (I/O, syscalls, allocations, etc.). |
43 | 132 |
|
44 | | -| Function | Description | |
45 | | -| -------------------------- | ------------------------------------- | |
46 | | -| `cme_init()` | Allocate internal ring buffer | |
47 | | -| `cme_destroy()` | Free ring buffer | |
48 | | -| `cme_error()` | Create literal string error | |
49 | | -| `cme_errorf()` | Create formatted error | |
50 | | -| `cme_return(err)` | Propagate error and add current frame | |
51 | | -| `cme_error_dump_to_str()` | Dump trace to a buffer | |
52 | | -| `cme_error_dump_to_file()` | Dump trace to a file | |
| 133 | +In other words: you get structured errors and backtraces “for free” in the common case, and only pay a small price when something actually goes wrong. |
53 | 134 |
|
54 | | -## 🛠️ Building |
| 135 | +--- |
55 | 136 |
|
56 | | -Using [Meson](https://mesonbuild.com/): |
| 137 | +
|
| 138 | +## 🛠️ Building examples and tests |
| 139 | +
|
| 140 | +This project uses [Meson](https://mesonbuild.com/) for its build system. To build with examples and tests enabled, run: |
57 | 141 |
|
58 | 142 | ```sh |
59 | | -meson setup build |
| 143 | +meson setup build -Dtests=true -Dexamples=true |
60 | 144 | meson compile -C build |
61 | 145 | ``` |
62 | 146 |
|
63 | | -## ✅ Running Tests |
| 147 | +Examples will be placed in `build/example/` and test binaries in `build/test/`. You can run them directly: |
| 148 | + |
| 149 | +```sh |
| 150 | +./build/example/example_1 |
| 151 | +./build/example/example_2 |
| 152 | +``` |
| 153 | + |
| 154 | +and execute the full test suite with: |
64 | 155 |
|
65 | 156 | ```sh |
66 | 157 | meson test -C build |
67 | 158 | ``` |
68 | 159 |
|
69 | | -Unit tests are implemented using [Unity](https://www.throwtheswitch.org/unity). |
| 160 | +🧪 Tests are written using the Unity framework, pulled in automatically as a Meson subproject. |
70 | 161 |
|
71 | | -## ⚙️ Dev Tools |
| 162 | +--- |
72 | 163 |
|
73 | | -Automate with [Invoke](https://www.pyinvoke.org/): |
| 164 | +## ⚙️ Development workflow |
| 165 | + |
| 166 | +For convenience, there’s an [Invoke](https://www.pyinvoke.org/) setup that automates common tasks: |
74 | 167 |
|
75 | 168 | ```sh |
76 | | -inv install # Setup tools |
77 | | -inv build # Compile project |
78 | | -inv test # Run test suite |
79 | | -inv format # Apply clang-format |
80 | | -inv lint # Run clang-tidy checks |
81 | | -inv clean # Clean build artifacts |
| 169 | +inv install # install meson, ninja, clang-format, clang-tidy, etc. |
| 170 | +inv build # configure and compile (add --debug, --tests, --examples) |
| 171 | +inv test # run test suite |
| 172 | +inv format # apply clang-format |
| 173 | +inv lint # run clang-tidy checks |
| 174 | +inv clean # remove build artifacts |
82 | 175 | ``` |
83 | 176 |
|
| 177 | +This makes it easy to keep the environment consistent and catch issues early. |
| 178 | + |
| 179 | +--- |
| 180 | + |
84 | 181 | ## 📄 License |
85 | 182 |
|
86 | | -Licensed under the [MIT License](LICENSE) © 2025 Jakub Buczynski (KubaTaba1uga). |
| 183 | +Released under the [MIT License](LICENSE) © 2025 Jakub Buczynski (KubaTaba1uga). |
87 | 184 |
|
0 commit comments