It's easy to use the same str_*() functions to implement dynamic
memory allocation. I think I could have done a bit different naming,
like maybe:
extern struct string *str_alloc(unsigned int len);
extern void str_append(struct string *str, const char *cstr);
#define static_string(name, size) ..
#define sstr_append(str, cstr) str_append(&(str).string, cstr)
I would hardly call 8 extra bytes on stack heavier. Also if this was
used everywhere I wouldn't be surprised if it made the code faster,
because it would remove a lot of overflow checking code so more code
will fit into L1 cache.