Ch20.10: Herbceptions: the Only Error Handling That Exists Here
1. The Same Feature, Implemented the Other Way
A herbception is two words on the return path and a failure bit: the error value is
{domain pointer, code}, and the calling convention says whether it is present
(3.16). There is no object to allocate,
no stack to walk, no table to register and no personality routine, because nothing is
being unwound — the function returns, exactly like a function that returns an error
code.
Everything that made legacy exceptions impossible freestanding is therefore absent from the mechanism by construction. What is left is one question: the error domain has to know what its codes mean, and that knowledge lives in a library. So how does that library stay freestanding?
2. Measured: One Symbol Instead of Eleven
The same throw-and-catch program as 20.9, written with the mechanism this book uses:
#include <herbceptions/error>
void fail(int x) throws { if (x < 0) { throw throws ::std::errc::invalid_argument; } }
int main() { try { fail(-1); } catch throws (::std::error) { return 0; } return 1; }
$ g++ -std=c++26 -fherbceptions -O2 -c herb.cc && nm -u herb.o
U __cxa_error_domain_posix
[1 undefined symbol]
$ size legacy.o herb.o
text data bss dec hex filename
452 0 0 452 1c4 legacy.o
230 0 0 230 e6 herb.o
One symbol, and it is a data pointer to the error domain singleton —
not a function, not a runtime, not a table that a loader has to find. The object file is
also half the size, and it has no .eh_frame entries for the frames it cannot
unwind.
And note what the object file does not need: malloc,
__cxa_allocate_exception, _Unwind_Resume,
__gxx_personality_v0, or any RTTI. The eleven-line dependency of
20.9 is genuinely gone, not hidden.
3. The Ban Flag Does Not Touch Them
The first thing people ask is whether the build flags from 19.7 break herbceptions. They do not, because herbceptions are not exceptions:
$ g++ -std=c++26 -fherbceptions -fno-exceptions -O2 hb.cc -o hb -lherbceptions
[exit 0]
-fno-exceptions removes the legacy mechanism, and the herbception mechanism
keeps working, because it never used it. A freestanding build can and should have
-fno-exceptions -fno-rtti in it while using throws everywhere,
which is exactly the combination the rest of this book uses.
4. The One Thing That Is Different: No Message Tables
The remaining dependency is the domain, and its job is to turn a code into text. Text is the one thing that has to be paid for in flash: a table of error strings per domain, plus the formatting code that copies them out. On a board where the entire binary is 64 KiB, that is a real cost for a string nobody will ever read.
So the runtime has a build knob. LIBHERBCEPTIONS_NO_MESSAGES (passed to the
translation unit as __HERBCEPTIONS_NO_MESSAGES; the freestanding build option
LIBHERBCEPTIONS_FREESTANDING defaults it on) makes every domain report
only [domain](code). Here is the same program, once with the
hosted runtime and once with a runtime built with that macro, printing name, message,
name+message and the error itself:
--- hosted runtime ---
name posix
message (12)Not enough space
name_message [posix](12)Not enough space
e [posix](12)Not enough space
--- runtime built with __HERBCEPTIONS_NO_MESSAGES ---
name posix
message (12)
name_message [posix](12)
e [posix](12)
Same code, same error, same domain. What disappeared is the text. And it is not a stub that formats an empty string: the message table is never touched, which is where the savings come from. Built from the same source, the two runtimes:
$ size -A libhb_msg.so | grep -E '^\.(text|rodata)|^Total'
.text 8771 4352
.rodata 3427 16384
Total 17964
$ size -A libhb_nomsg.so | grep -E '^\.(text|rodata)|^Total'
.text 5587 4288
.rodata 291 12288
Total 11164
6,800 bytes of a 17,964-byte runtime, and 91% of its read-only data, for one domain — the POSIX one. Add the parse, HTTP, cmath and Win32 domains and the combined table is the largest thing in the library. On x86-64 those numbers are noise; on the targets this chapter is about they decide whether the feature fits.
5. And You Can Put Your Own Text Back
"No messages" is not a downgrade you have to live with forever, because a domain is
overridable (19.1). A
kernel that wants descriptive text for the twenty codes it actually reports can replace the
POSIX domain with its own implementation, compiled into its own binary, with its own
strings and its own do_query_information. The library gives you a default that
costs nothing; you decide whether to pay for words.
| Legacy exceptions (20.9) | Herbceptions | |
|---|---|---|
| Linker symbols needed | 11 | 1, a data pointer |
| Heap at failure time | Yes | No |
| Stack walk | Yes | No |
| Frame tables, personality routine | Yes | No |
| RTTI | Yes | No |
| Works on a board, in a kernel, on a GPU | No | Yes |
| Message text | Allocated at throw time | A build option; overridable per domain |
That table is the whole reason this book teaches throws instead of
throw. The error handling is the same feature on both rows; only the
implementation changed, and the implementation was the problem.