9. String Management

Content gets dynamically appended onto a new string in memory, before being saved as content to a weewiki page.

<<string_management>>=
<<string_constants>>
<<string_struct>>
<<string_funcs>>

An interface needs to be built to handle this. Strings need to dynamically grow when needed (shrinking not needed at the moment), and it needs to be trivial to append stuff to the end of the string as well. Something simple and fast. Just something that re-allocs stuff under the hood.

The main struct consists of a string pointer, it's current size, and the total capacity.

<<string_struct>>=
typedef struct {
   char *str;
   size_t sz;
   size_t cap;
} wwstring;

When the string is initialized, everything is zeroed out.

<<string_funcs>>=
static void wwstring_init(wwstring *s)
{
    s->str = NULL;
    s->sz = 0;
    s->cap = 1;
}

The internal string grows by getting stuff appended to it. Arguments supplied must be the content to appended (duh), as well as the size of the content.

Before copying (memmove-ing?) the content over, the string must be checked if it has enough capacity for it. If not, the capacity will keep doubling until it is greater than the needed capacity. Either that, or the capacity reaches some built-in max. At which point, and error is returned.

Speaking of max capacity, that's a constant defined! A value of 131072 (2^17) should be plenty while still being modest on modern hardware.

<<string_constants>>=
#define WWSTRING_MAX 131072

An initialized string starts at 0, which means that no memory has actually been allocated yet. The initial allocation starts at 8, and doubles from there.

None of the sizes/capacities include the null terminator, so malloc/realloc makes up for this by allocating capacity + 1.

<<string_resize>>=
while (s->cap < new_sz) {
    if (s->cap == 0) {
        s->cap = 8;
        s->str = malloc(s->cap + 1);
    } else {
        s->cap *= 2;
        s->str = realloc(s->str, s->cap + 1);
    }
}

Since there is a hard coded max, some work needs to be done to ensure that the new string does go over the size. If this does happen, only part of the string gets appended.

<<check_limits>>=
if (new_sz > WWSTRING_MAX) {
    sz = WWSTRING_MAX - s->sz;
    new_sz = WWSTRING_MAX;
}

String copying is done with memmove, which is a little bit more straight forward than memcpy and is less prone to undefined behavior.

The string starts copying over stuff where the previous size sz is. (I initially wasn't entirely sure this was corrrect, so I had to double check with a test; When sz is 0, str[0] is correctly at the beginning of the string.)

<<copy_string_over>>=
memmove(&s->str[s->sz], txt, sz);
s->str[new_sz] = '\0'; /* don't forget the null terminator */

At the end of it all, update the size of string to be new_sz.

<<update_size>>=
s->sz = new_sz;
<<string_funcs>>=
static void wwstring_append(wwstring *s,
                            const char *txt,
                            size_t sz)
{
    size_t new_sz;

    if (s->sz >= WWSTRING_MAX) return;

    new_sz = s->sz + sz;

<<check_limits>>

    if (new_sz > s->cap) {
<<string_resize>>
    }

<<copy_string_over>>

<<update_size>>
}

Freeing a string is done with wwstring_free. This function will ignore cap and sz, and only check and see if the string itself is NULL or not. The string gets reinitialized as a precautionary measure. Doing this should help to avoid things like double frees and out-of-bounds errors.

<<string_funcs>>=
static void wwstring_free(wwstring *s)
{
    if (s->str != NULL) free(s->str);
}



prev | home | next