Strlib provides a small, C11-based toolkit for handling string views, heap-allocated strings, and string builders in a consistent and ergonomic way. It exists to replace scattered, error-prone string handling in C with a unified, safer, and more convenient abstraction layer that still stays close to low-level control.
The library uses a three-layer ownership model which provides a way to separate string handling into increasing levels of ownership and responsibility, so you can move between performance, safety, and convenience without mixing concerns.
A non-owning reference:
- pointer + length
- does NOT allocate
- does NOT free
typedef struct {
const char *data;
size_t len;
} SV;Why it exists
This layer is for zero-cost string handling.
It allows you to:
- inspect strings without copying
- pass substrings efficiently
- avoid allocations entirely
A heap-allocated string that owns its memory.
typedef struct {
char *data;
size_t len;
} Str;Why it exists
This is the safe middle ground:
- owns memory
- can be passed around safely
- can be freed cleanly
- works with SV seamlessly, i.e.,
str_sv()borrows a Str as an SV for free, and most functions accept both viaNEW_SV(more on this later).
Key idea
Str is what you use when you want:
- persistence
- safety
- no manual buffer management
A growable buffer used to build strings efficiently.
typedef struct {
char *data;
size_t len;
size_t cap;
} SB;Why it exists
This solves a performance problem:
repeated string concatenation is expensive in C
So SB gives you:
- amortized O(1) appends
- no repeated realloc + copy chains
- efficient formatting/building
The construction pipeline
SB is also the only way to dynamically construct a Str. Once you're done building, sb_build() transfers ownership from the SB to a new Str and leaves the SB in a valid empty state - no copy, just a handoff. This makes the construction pipeline explicit:
SB (build) -> Str (own) -> SV (borrow)
Data flows in one direction. You never go backwards.
Strings are treated as raw byte sequences (UTF-8 encoded) unless explicitly stated otherwise, and any operation that understands Unicode codepoints will be clearly separated and named. All current operations are byte-level safe with UTF-8 already.
This library is designed around three core principles:
Ergonomics first
The API prioritizes ease of use and readability while staying close to low-level C. Common operations should feel natural, concise, and predictable without unnecessary boilerplate.
Explicit ownership
Memory ownership is always clear. Types and functions make it obvious whether data is borrowed, owned, or transferred, avoiding hidden lifetime assumptions and reducing the risk of misuse.
No hidden allocations
Allocations never happen implicitly. Any operation that allocates memory does so explicitly through clearly named functions, ensuring performance characteristics are always visible and under the user’s control.
The library requires C11 or above to work. It is enforced at compile time by compat.h.
GCC and Clang fully supported. MSVC is NOT supported and errors at compile. See Code
Makefile assumes Linux (hardcoded linux commands and directories). The library itself is platform-agnostic C11.
None. Only libc.
- GCC or Clang
makear(binutils)
makesudo make install VERSION=0.1.0Installs headers to /usr/local/include/strlib-<version>/, the static library to /usr/local/lib/libstrlib-<version>.a, and a pkg-config file to /usr/local/lib/pkgconfig/strlib.pc. Stable symlinks at /usr/local/include/strlib and /usr/local/lib/libstrlib.a always point to the latest installed version.
Override the install prefix if needed:
sudo make install VERSION=0.1.0 PREFIX=/usrsudo make uninstall VERSION=0.1.0Only removes the specified version. Symlinks are removed only if they point to that version.
make release VERSION=0.1.0Checks for a clean working tree, installs, tags the commit as v<version>, and pushes to origin.
With pkg-config:
gcc main.c -o app $(pkg-config --cflags --libs strlib)Without pkg-config:
gcc main.c -o app -I/usr/local/include/strlib -L/usr/local/lib -lstrlib| Module | Header | Purpose |
|---|---|---|
| SV | sv.h | Non-owning pointer + length view into existing string data. No allocation, no lifetime. |
| Str | str.h | Heap-allocated, owned string. Use when data needs to outlive its source or be passed around safely. |
| SB | sb.h | Growable buffer for constructing strings incrementally. Transfers ownership to a Str via sb_build(). |
strlib.h is a convenience umbrella header that conditionally includes sv.h, str.h, and sb.h in dependency order, allowing selective exclusion via NO_SV, NO_STR, and NO_SB compile-time flags.
typedef struct {
SV *data;
size_t len;
size_t cap;
} SVArray;A heap-allocated growable array of SVs. Returned by sv_split. Always call sva_free when done.
SVArray sva_new(void) — Creates an empty array. Does not allocate until first push.
void sva_push(SVArray *arr, SV sv) — Appends an SV to the array. Grows automatically.
SV sva_get(SVArray *arr, size_t i) — Returns the SV at index i. Aborts with an error message if i is out of bounds.
void sva_free(SVArray *arr) — Frees the backing array and zeroes the struct. Safe to call on an already-freed SVArray. Does not free the individual SVs — they are views and do not own memory.
Functions:
SV sv_from_cstr(const char *cstr) — Creates a view into an existing C string. Does not copy. cstr must outlive the view.
SV sv_from_parts(const char *data, size_t len) — Creates a view from a raw pointer and explicit length. No validation, no null-termination required.
bool sv_eq_sv(SV a, SV b) — Byte-exact equality check. Not locale or encoding aware.
int sv_cmp_sv(SV a, SV b) — Lexicographical comparison. Returns negative, zero, or positive. Shorter string loses on equal prefix.
bool sv_starts_with_sv(SV sv, SV prefix) — Returns true if sv begins with prefix.
bool sv_ends_with_sv(SV sv, SV suffix) — Returns true if sv ends with suffix.
size_t sv_find_sv(SV sv, SV needle) — Returns index of first match, or sv.len if not found. Empty needle matches at 0.
bool sv_contains_sv(SV sv, SV needle) — Returns true if needle occurs anywhere in sv.
SV sv_slice(SV sv, size_t start, size_t end) — Returns a sub-view. Out-of-range or inverted indices are clamped silently.
SV sv_trim_left(SV sv) — Returns view with leading whitespace removed.
SV sv_trim_right(SV sv) — Returns view with trailing whitespace removed.
SV sv_trim(SV sv) — Returns view with both leading and trailing whitespace removed.
SV sv_chop_by_delim(SV *sv, char delim) — Returns everything before the first delim and advances sv past it. If not found, returns the full view and leaves sv empty.
bool sv_is_empty(SV sv) — Returns true if sv has zero length.
bool sv_is_whitespace(SV sv) — Returns true if every byte is whitespace. Returns false on empty.
bool sv_is_alpha(SV sv) — Returns true if every byte is an alphabetic character. Returns false on empty.
bool sv_is_numeric(SV sv) — Returns true if every byte is a decimal digit. Returns false on empty.
bool sv_is_alphanumeric(SV sv) — Returns true if every byte is alphabetic or a decimal digit. Returns false on empty.
bool sv_is_upper(SV sv) — Returns true if every byte is an uppercase letter. Returns false on empty.
bool sv_is_lower(SV sv) — Returns true if every byte is a lowercase letter. Returns false on empty.
size_t sv_count_sv(SV sv, SV needle, bool overlapping) — Counts occurrences of needle in sv. If overlapping is false, matches do not overlap — sv_count("aaa", "aa", false) returns 1. If overlapping is true, every position is checked — sv_count("aaa", "aa", true) returns 2. Returns 0 if needle is empty or longer than sv.
bool sv_parse_int(SV sv, int *out) — Parses sv as a decimal integer into *out. Returns true on success, false if the input is not a valid integer or overflows the type. If out is NULL, validates without storing. All sv_parse_* variants follow this same contract.
bool sv_parse_long(SV sv, long *out) — Parses as long.
bool sv_parse_longlong(SV sv, long long *out) — Parses as long long.
bool sv_parse_int8(SV sv, int8_t *out) — Parses as int8_t.
bool sv_parse_int16(SV sv, int16_t *out) — Parses as int16_t.
bool sv_parse_int32(SV sv, int32_t *out) — Parses as int32_t.
bool sv_parse_int64(SV sv, int64_t *out) — Parses as int64_t.
bool sv_parse_uint8(SV sv, uint8_t *out) — Parses as uint8_t.
bool sv_parse_uint16(SV sv, uint16_t *out) — Parses as uint16_t.
bool sv_parse_uint32(SV sv, uint32_t *out) — Parses as uint32_t.
bool sv_parse_uint64(SV sv, uint64_t *out) — Parses as uint64_t.
bool sv_parse_float(SV sv, float *out) — Parses as float.
bool sv_parse_double(SV sv, double *out) — Parses as double.
SVArray sv_split_char(SV sv, char delim, size_t maxsplit) — Splits sv on every occurrence of delim. If maxsplit is non-zero, stops after that many splits and puts the remainder in the final element. Empty segments are preserved. Caller must call sva_free on the result.
SVArray sv_split_sv(SV sv, SV delim, size_t maxsplit) — Same as sv_split_char but splits on a multi-character delimiter. If delim is empty, returns sv unsplit as a single element.
Macros:
NEW_SV(s) — Converts SV, char *, or const char * into an SV at compile time via _Generic. No copy.
sv_eq(a, b), sv_cmp(a, b), sv_starts_with(a, b), sv_ends_with(a, b), sv_contains(a, b), sv_find(a, b) — Type-coercing wrappers around their _sv counterparts. Accept any mix of SV, char *, or const char * as arguments.
sv_count(a, b) — Counts non-overlapping occurrences of b in a. Accepts any mix of SV, char *, or const char *.
sv_count_overlapping(a, b) — Counts overlapping occurrences of b in a. Accepts any mix of SV, char *, or const char *.
sv_split(sv, delim, maxsplit) — Splits sv on delim, dispatching to sv_split_char or sv_split_sv based on the type of delim at compile time. Accepts char, SV, char *, or const char * as delimiter. Pass 0 for maxsplit to split everything.
SV_FMT, SV_ARGS(sv) — Use with printf-style functions to print an SV without null-termination:
printf(SV_FMT "\n", SV_ARGS(my_sv));Functions:
Str str_from_cstr(const char *cstr) — Allocates and copies a C string into a new Str. Caller owns the result.
Str str_from_parts(const char *data, size_t len) — Allocates and copies len bytes into a new Str. Always null-terminates.
Str str_from_sv(SV sv) — Allocates and copies an SV into a new Str.
Str str_clone(Str s) — Deep copies a Str. Caller owns the result.
void str_free(Str *s) — Frees the underlying buffer and zeroes the struct. Safe to call on an already-freed Str.
SV str_sv(Str s) — Borrows a Str as an SV. Free. The view is only valid for the lifetime of the Str.
const char *str_cstr(Str s) — Returns the underlying buffer as a null-terminated C string. Valid for the lifetime of the Str.
Str str_concat_sv(SV a, SV b) — Allocates a new Str containing a followed by b.
Macros:
NEW_STR(s) — Converts Str, SV, char *, or const char * into an owned Str via _Generic. Always allocates.
str_concat(a, b) — Concatenates any two string-like values into a new Str. Accepts any mix of SV, Str, char *, or const char *. Watch out for temporary Str arguments — they won't be freed automatically.
STR_AUTO — Marks a Str for automatic cleanup when it goes out of scope. GCC/Clang only.
Functions:
SB sb_new(void) — Creates an empty builder. Does not allocate until first append.
SB sb_with_cap(size_t initial_cap) — Creates a builder with a pre-allocated buffer. Use when final size is roughly known.
void sb_free(SB *sb) — Frees the buffer and zeroes the struct. Safe to call on an already-freed SB.
void sb_reset(SB *sb) — Resets length to zero without freeing. Use to reuse the allocation for a new string.
void sb_append_char(SB *sb, char c) — Appends a single character.
void sb_append_cstr(SB *sb, const char *cstr) — Appends a null-terminated C string.
void sb_append_sv(SB *sb, SV sv) — Appends a string view.
void sb_append_str(SB *sb, Str s) — Appends an owned string. Does not consume or free s.
void sb_append_fmt(SB *sb, const char *fmt, ...) — Appends a printf-style formatted string. Uses two-pass vsnprintf to avoid intermediate buffers.
SV sb_sv(const SB *sb) — Borrows the current contents as an SV. The view is invalidated by any subsequent append that triggers a realloc.
Str sb_build(SB *sb) — Transfers ownership of the buffer to a new Str, null-terminating it. Leaves the SB in a valid empty state. No copy.
Macros:
sb_append(sb, ...) — Appends up to 8 arguments of mixed types in one call. Accepts any combination of char, int, SV, Str, char *, and const char *. Dispatches per argument via _Generic at compile time. Maximum 8 arguments per call.
SB_AUTO — Marks an SB for automatic cleanup when it goes out of scope. GCC/Clang only.
compat.h is included automatically via sv.h and handles all compiler and standard detection. It is not intended to be included directly.
C11 is required and enforced at compile time. Compiling with -std=c99 or older will produce a hard error.
| Compiler | Status | Notes |
|---|---|---|
| GCC | Fully supported | |
| Clang | Fully supported | |
| MSVC | Not supported | Hard error at compile time |
| Unknown | Partial | Compiles but loses cleanup and typeof |
| Flag | What it enables | Lost on unknown compiler |
|---|---|---|
STRLIB_HAS_CLEANUP |
SB_AUTO, STR_AUTO |
Both become no-ops, no automatic cleanup |
STRLIB_HAS_TYPEOF |
Future use | Nothing currently |
STRLIB_HAS_GENERIC |
sb_append, NEW_SV, NEW_STR, all type-coercing macros |
Guaranteed by C11, cannot be lost |
STRLIB_AUTO(fn) — Expands to __attribute__((cleanup(fn))) on GCC/Clang, no-op otherwise. Emits a compile-time warning when unavailable.
STRLIB_UNUSED — Expands to __attribute__((unused)) on GCC/Clang to suppress unused variable warnings.
STRLIB_WARN(msg) — Emits a compile-time warning via _Pragma. No-op on unknown compilers.
sb_sv() returns a view into the builder's internal buffer. Any append that triggers a realloc invalidates it:
SV view = sb_sv(&sb);
sb_append_cstr(&sb, "more"); // may realloc
// view.data is now potentially danglingOnly use sb_sv() when you are done appending, or when you can guarantee no realloc will occur.
str_concat does not free its arguments. If you pass a temporary Str returned from a function, it leaks:
Str s = str_concat(make_str(), make_str()); // both temporaries leakAssign temporaries first and free them manually:
Str a = make_str();
Str b = make_str();
Str s = str_concat(a, b);
str_free(&a);
str_free(&b);The SVs inside an SVArray returned by sv_split are views into the original string's memory. Freeing or modifying the source string while still using the array will leave all the views dangling:
SVArray parts = sv_split(sv_from_cstr(get_temp_string()), ',', 0); // imagine get_temp_string() is a fn that returns temp string
// if get_temp_string()'s memory is gone, parts.data[i] are all dangling
sva_free(&parts);Make sure the source string outlives the SVArray.
On compilers without __attribute__((cleanup)), both macros expand to nothing and no warning is emitted at the point of use — only at the point compat.h is processed. Variables marked SB_AUTO or STR_AUTO will not be freed automatically. You will leak memory silently.
strlib follows Semantic Versioning. Version numbers are MAJOR.MINOR.PATCH:
MAJOR— breaking API changesMINOR— new features, backwards compatiblePATCH— bug fixes, backwards compatible
However, strlib does not strictly abide by the above convention and may change version in non-standard ways.
The current version is defined in strlib.h as STRLIB_VERSION.
strlib is currently at v0.1.0 and is not feature complete. The API will expand significantly before v1.0.0.
Planned areas (no fixed order or timeline):
- Allocator abstraction — pluggable allocator interface for arena, pool, and stack allocation
- Expanded SV API — split, count, parse primitives, ...
- Expanded Str API — slice, find, trim, case conversion, reverse, ...
- Expanded SB API — repeat, padding, ...
- Number conversions — string to integer/float and vice versa
- UTF-8 awareness — codepoint-aware variants of operations that require it, clearly separated from byte-level defaults
- Parser primitives — cursor-based API built on SV for writing hand-rolled parsers
- String interning — deduplication table for identifier-heavy use cases
- Tests — proper test suite before v1.0.0
compat.hexpansion — broader compiler support as needed
No guarantees on any of this until v1.0.0 is close.