Skip to content

Latest commit

 

History

155 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dstr - Dynamic String Library for C

Overview

dstr is a dynamic string library for C (C11+). It exposes a char* interface backed by a hidden allocation header that stores length and capacity. Strings are heap-allocated and may be reallocated automatically during operations.

Requires gcc or clang. Supported architectures: x86, x86-64, ARM, ARM64.

Benchmarks

dstr is benchmarked head-to-head against sds (Redis's string library) in RECAP.md: speed and memory numbers, methodology, and the reasoning behind each result. The benchmark sources live in bench/.

Build

The library ships as two files: dstr.h and dstr.c. Compile and link using the provided Makefile:

# Build both static and shared libraries
make

# Build static library only
make libstatic

# Build shared library only
make libshared

This produces libdstr.a and libdstr.so. Link against whichever suits your project:

cc -std=c11 your_file.c libdstr.a -o your_program

Configuration

dstr_options.h is included by dstr.c and is the single place to configure the library. Create it in the same directory as dstr.c before building. Available options:

Custom allocator — override the default malloc, realloc, and free:

// dstr_options.h
#define DSTR_MALLOC(sz)        my_malloc(sz)
#define DSTR_REALLOC(ptr, sz)  my_realloc(ptr, sz)
#define DSTR_FREE(ptr)         my_free(ptr)

If not defined, these fall back to the standard allocators.

Growth strategy - controls how dstrcat, dstrappend, and dstrpush (the two-argument, non-custom forms) grow the buffer when the current capacity is not enough:

// dstr_options.h
#define DSTR_GROWTH_ENABLED 1   /* 1 = amortized growth, 0 = allocate exactly what is needed */
#define DSTR_GROWTH_PERCENT 50  /* extra capacity added on growth, as a percent of the current capacity */

With DSTR_GROWTH_ENABLED set to 1 (the default), a growth only happens when the requested size exceeds the current capacity, and it adds DSTR_GROWTH_PERCENT percent of headroom on top (50 means the new capacity is roughly 1.5x the old one), so repeated small appends do not reallocate on every call. Setting it to 0 disables the headroom: every growth allocates exactly what is needed, trading more frequent reallocations for a tighter memory footprint. Either way, capacity is left untouched whenever it is already sufficient. The _custom variants of dstrcat, dstrappend, and dstrpush, as well as the cap argument of dstrnew, always honor the caller-supplied capacity and are not affected by these options.

Types

dstr is a char*. The internal headers (dstrhd8, dstrhd16, dstrhd32, dstrhd64) are implementation details and not part of the public API. The header type used for a given string is selected automatically based on the required capacity.

Memory layout

[dstrhd{8|16|32|64}][char buffer]

The dstr pointer points to the buffer. The header immediately precedes it in memory and stores len, cap, and a type byte used to identify the header variant at runtime. The type byte is always at s[-1].

Header sizes grow with string capacity:

Type Max capacity
dstrhd8 255
dstrhd16 65,535
dstrhd32 4,294,967,295
dstrhd64 2^64 - 1

API

Create

dstr s = dstrnew("hello");
dstr s = dstrnew("hello", 64); // pre-allocate 64 bytes of capacity

If a capacity is provided and is smaller than the string length, it is automatically increased to fit. When no capacity is given, the allocation matches the string length plus null terminator.

Length and capacity

size_t len = dstrlen(s);
size_t cap = dstrcap(s);

Both return size_t.

Concatenate (dstr + char*)

s = dstrcat(s, "world");
s = dstrcat(s, "world", 128); // hint a capacity of 128 bytes

The first argument must be a dstr. The second is a const char*. Use dstrappend when the second argument is also a dstr.

Append (dstr + dstr)

s = dstrappend(s, s2);
s = dstrappend(s, s2, 128); // hint a capacity of 128 bytes

Both arguments must be dstr. Passing a char* as the second argument is not supported.

Reserve capacity

s = dstrreserve(s, 256);

Grows the allocation to at least new_cap. No-op if the current capacity is already sufficient. The returned pointer must be reassigned as the buffer may move during reallocation. If the header type changes due to the new size, a new allocation is made and the old one is freed.

Duplicate

dstr copy = dstrdup(s);

Clear

dstrclear(s);

Resets len to zero and writes a null terminator without freeing the allocation. Previous content beyond the null terminator remains in memory.

Zero

dstrzero(s);

Resets len to zero and zeroes the entire buffer with memset. Use this when the previous content must not remain readable in memory.

Compare

if (dstrequal(s1, s2)) { ... }

Returns true if the strings are equal, false otherwise. Both arguments must be dstr. For dstr vs char* comparisons use strcmp directly.

Case conversion

dstrtolower(s);
dstrtoupper(s);

Converts ASCII letters in place. Bytes outside 'A'-'Z'/'a'-'z', including non-ASCII bytes, are left untouched. Length and capacity are unchanged; no reallocation occurs. No-op if s is NULL.

Find substring

ssize_t index = dstrfind(s, "needle");

Searches for the first occurrence of a null-terminated C string (needle) inside the dynamic string s. Returns the zero-based index of the first match, or -1 if the substring is not found. It safely returns -1 if either argument is NULL, if the needle is an empty string, or if the needle is longer than the string itself.

Range (substring)

dstrrange(s, 0, 2);   // keep only the first 3 characters
dstrrange(s, -3, -1); // keep the last 3 characters
dstrrange(s, 2, -1);  // keep from index 2 to the last character

Keeps only the [start, end] slice of s, in place (end is inclusive). Negative indices count from the end of the string, i.e. -1 is the last character. Out-of-range indices are clamped instead of erroring, and a start past end or past the end of the string yields an empty dstr. No allocation happens: the existing buffer is shifted with memmove and truncated in place. No-op if s is NULL or empty.

Trim

dstrtrim(s);          // strip leading/trailing ASCII whitespace
dstrtrim(s, "xy");    // strip leading/trailing 'x'/'y' bytes instead

Strips leading and trailing bytes from s, in place. The one-argument form strips ASCII whitespace (' ', '\t', '\n', '\v', '\f', '\r'); the two-argument form strips any byte found in the null-terminated cutset instead. No allocation happens: the kept slice is shifted with memmove and truncated in place. No-op if s is NULL or empty, or (two-argument form) if cutset is NULL or empty.

Insert

s = dstrinsert(s, 5, ", ");  // insert at index 5
s = dstrinsert(s, 0, ">> "); // prepend
s = dstrinsert(s, -1, "!");  // insert before the last character

Inserts the null-terminated string s2 into s1 at idx, in place. Negative indices count from the end of the string, as in dstrrange. Out-of-range indices are clamped instead of erroring. May reallocate s1; always use the returned pointer.

Split

size_t count;
dstr* parts = dstrsplit(s, ",", &count);
for (size_t i = 0; i < count; i++)
    puts(parts[i]);
dstrsplitfree(parts, count);

Splits s on every occurrence of delim, returning a newly allocated array of dstr. Adjacent or leading/trailing delimiters produce empty ("") elements. Returns NULL if s/delim is NULL, delim is empty, or allocation fails. The array and every element must be released together with dstrsplitfree.

Free

dstrfree(s);

Automatic cleanup

dstrauto declares a dstr with __attribute__((cleanup)).
The string is freed automatically when it goes out of scope:

dstrauto dstr s = dstrnew("hello");

Shortcut macro

$() can be enabled by changing DSTR_SHORTCUT_ENABLE in dstr.h. It works as an alias for dstrnew:

// dstr_options.h
#define DSTR_SHORTCUT
#include "dstr.h"

dstr s = $("hello");
dstr s = $("hello", 64);

Notes

  • Strings are always null-terminated.
  • All functions that return dstr may return a reallocated pointer; always reassign.
  • Capacity arguments are hints and are silently increased if insufficient.

License

BSD-2-Clause

About

Dynamic string library for C (C11+): char*-compatible API, hidden length/capacity header, automatic reallocation, and optional cleanup on scope exit

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Contributors

Languages