Frontend Labs
⋯

16. Mini projects: a dynamic-array library + CSV parser, and a JSON parser

16.1 Mini project — a generic dynamic array (vector)

This is the pattern every C container library uses (Section 37's uthash, Section 19's intrusive lists, and any "vec.h"-style single-header library on GitHub): a macro that generates a typed struct + functions, because C has no real generics before you reach for void* + casts or _Generic.

// vec.h — a growable array, macro-generated per type
#ifndef VEC_H
#define VEC_H
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#define VEC_DEFINE(T, Name)                                                  \
typedef struct { T *data; size_t len, cap; } Name;                           \
                                                                               \
static inline void Name##_init(Name *v) { v->data = NULL; v->len = v->cap = 0; } \
                                                                               \
static inline int Name##_push(Name *v, T item) {                             \
    if (v->len == v->cap) {                                                  \
        if (v->cap > SIZE_MAX / 2) return 0;                                  \
        size_t new_cap = v->cap ? v->cap * 2 : 4;                            \
        if (new_cap > SIZE_MAX / sizeof(T)) return 0;                         \
        T *new_data = realloc(v->data, new_cap * sizeof(T));                 \
        if (!new_data) return 0;                                             \
        v->data = new_data;                                                  \
        v->cap = new_cap;                                                    \
    }                                                                        \
    v->data[v->len++] = item;                                                \
    return 1;                                                                \
}                                                                             \
	                                                                               \
static inline void Name##_free(Name *v) { free(v->data); v->data = NULL; v->len = v->cap = 0; }

#endif
// main.c
#include <stdio.h>
#include "vec.h"

VEC_DEFINE(int, IntVec)

int main(void) {
    IntVec v;
    IntVec_init(&v);
    for (int i = 0; i < 10; i++) {
        if (!IntVec_push(&v, i * i)) { perror("IntVec_push"); IntVec_free(&v); return 1; }
    }
    for (size_t i = 0; i < v.len; i++) printf("%d ", v.data[i]);
    printf("\n");
    IntVec_free(&v);
    return 0;
}

Doubling the capacity on growth (cap * 2) gives amortized O(1) push — the classic result every dynamic-array implementation (C++ std::vector, Go slices, Python lists) relies on: most pushes are O(1), occasional resizes are O(n), and the total cost over n pushes is O(n).

16.2 Mini project — a CSV parser using the vector above

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "vec.h"

VEC_DEFINE(char *, StrVec)

static int split_line(const char *line, char delim, StrVec *out) {
    const char *start = line;
    for (const char *p = line; ; p++) {
        if (*p == delim || *p == '\0' || *p == '\n') {
            size_t len = (size_t)(p - start);
            char *field = malloc(len + 1);
            if (!field) return 0;
            memcpy(field, start, len);
            field[len] = '\0';
            if (!StrVec_push(out, field)) { free(field); return 0; }
            start = p + 1;
            if (*p == '\0' || *p == '\n') break;
        }
    }
    return 1;
}

int main(void) {
    char line[1024];
    while (fgets(line, sizeof line, stdin)) {
        StrVec fields;
        StrVec_init(&fields);
        if (!split_line(line, ',', &fields)) {
            fprintf(stderr, "out of memory\n");
            for (size_t i = 0; i < fields.len; i++) free(fields.data[i]);
            StrVec_free(&fields);
            return 1;
        }

        for (size_t i = 0; i < fields.len; i++) {
            printf("[%zu]=%s ", i, fields.data[i]);
            free(fields.data[i]);   // each field was malloc'd in split_line
        }
        printf("\n");
        StrVec_free(&fields);
    }
    return 0;
}

This is a teaching CSV parser — it doesn't handle quoted fields with embedded commas. Real CSV parsing needs a small state machine; that's exactly the technique the JSON parser below and Section 36's cJSON walkthrough use for a harder grammar.

16.3 Mini project — a minimal JSON parser (recursive descent)

This is the single most valuable exercise in the whole guide for "reading real C" — Section 36 walks through cJSON's actual source right after this, and you'll recognize every function.

// json.h
#ifndef JSON_H
#define JSON_H
#include <stddef.h>   // size_t

typedef enum { JSON_NULL, JSON_BOOL, JSON_NUMBER, JSON_STRING, JSON_ARRAY, JSON_OBJECT } JsonType;

typedef struct JsonValue JsonValue;
typedef struct { char *key; JsonValue *value; } JsonMember;

struct JsonValue {
    JsonType type;
    union {
        int    boolean;
        double number;
        char  *string;
        struct { JsonValue **items; size_t count; } array;
        struct { JsonMember *members; size_t count; } object;
    };
};

JsonValue *json_parse(const char *text);
void       json_free(JsonValue *v);

#endif
// json.c — a compact recursive-descent parser
#include "json.h"
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

typedef struct { const char *p; } Parser;

static void skip_ws(Parser *ps) { while (isspace((unsigned char)*ps->p)) ps->p++; }

static JsonValue *parse_value(Parser *ps);

static JsonValue *new_value(JsonType t) {
    JsonValue *v = calloc(1, sizeof(JsonValue));
    if (!v) return NULL;
    v->type = t;
    return v;
}

static char *parse_raw_string(Parser *ps) {
    ps->p++; // skip opening quote
    const char *start = ps->p;
    while (*ps->p && *ps->p != '"') ps->p++;   // no escape handling — kept minimal on purpose
    size_t len = (size_t)(ps->p - start);
    char *s = malloc(len + 1);
    if (!s) return NULL;
    memcpy(s, start, len);
    s[len] = '\0';
    if (*ps->p == '"') ps->p++;
    return s;
}

static JsonValue *parse_string(Parser *ps) {
    JsonValue *v = new_value(JSON_STRING);
    if (!v) return NULL;
    v->string = parse_raw_string(ps);
    if (!v->string) { free(v); return NULL; }
    return v;
}

static JsonValue *parse_number(Parser *ps) {
    char *end;
    double n = strtod(ps->p, &end);
    ps->p = end;
    JsonValue *v = new_value(JSON_NUMBER);
    if (!v) return NULL;
    v->number = n;
    return v;
}

static JsonValue *parse_array(Parser *ps) {
    JsonValue *v = new_value(JSON_ARRAY);
    if (!v) return NULL;
    ps->p++; skip_ws(ps);   // skip '['
    if (*ps->p == ']') { ps->p++; return v; }

    size_t cap = 4;
    v->array.items = malloc(cap * sizeof(JsonValue *));
    if (!v->array.items) { free(v); return NULL; }
    while (1) {
        skip_ws(ps);
        if (v->array.count == cap) {
            cap *= 2;
            JsonValue **new_items = realloc(v->array.items, cap * sizeof(JsonValue *));
            if (!new_items) { json_free(v); return NULL; }
            v->array.items = new_items;
        }
        JsonValue *item = parse_value(ps);
        if (!item) { json_free(v); return NULL; }
        v->array.items[v->array.count++] = item;
        skip_ws(ps);
        if (*ps->p == ',') { ps->p++; continue; }
        break;
    }
    skip_ws(ps);
    if (*ps->p == ']') ps->p++;
    return v;
}

static JsonValue *parse_object(Parser *ps) {
    JsonValue *v = new_value(JSON_OBJECT);
    if (!v) return NULL;
    ps->p++; skip_ws(ps);   // skip '{'
    if (*ps->p == '}') { ps->p++; return v; }

    size_t cap = 4;
    v->object.members = malloc(cap * sizeof(JsonMember));
    if (!v->object.members) { free(v); return NULL; }
    while (1) {
        skip_ws(ps);
        char *key = parse_raw_string(ps);
        if (!key) { json_free(v); return NULL; }
        skip_ws(ps);
        if (*ps->p == ':') ps->p++;
        skip_ws(ps);
        JsonValue *val = parse_value(ps);
        if (!val) { free(key); json_free(v); return NULL; }

        if (v->object.count == cap) {
            cap *= 2;
            JsonMember *new_members = realloc(v->object.members, cap * sizeof(JsonMember));
            if (!new_members) { free(key); json_free(val); json_free(v); return NULL; }
            v->object.members = new_members;
        }
        v->object.members[v->object.count].key = key;
        v->object.members[v->object.count].value = val;
        v->object.count++;

        skip_ws(ps);
        if (*ps->p == ',') { ps->p++; continue; }
        break;
    }
    skip_ws(ps);
    if (*ps->p == '}') ps->p++;
    return v;
}

static JsonValue *parse_value(Parser *ps) {
    skip_ws(ps);
    switch (*ps->p) {
        case '"': return parse_string(ps);
        case '[': return parse_array(ps);
        case '{': return parse_object(ps);
        case 't': ps->p += 4; { JsonValue *v = new_value(JSON_BOOL); if (v) v->boolean = 1; return v; }
        case 'f': ps->p += 5; { JsonValue *v = new_value(JSON_BOOL); if (v) v->boolean = 0; return v; }
        case 'n': ps->p += 4; return new_value(JSON_NULL);
        default:  return parse_number(ps);
    }
}

JsonValue *json_parse(const char *text) {
    Parser ps = { .p = text };
    return parse_value(&ps);
}

void json_free(JsonValue *v) {
    if (!v) return;
    switch (v->type) {
        case JSON_STRING:
            free(v->string);
            break;
        case JSON_ARRAY:
            for (size_t i = 0; i < v->array.count; i++) json_free(v->array.items[i]);
            free(v->array.items);
            break;
        case JSON_OBJECT:
            for (size_t i = 0; i < v->object.count; i++) {
                free(v->object.members[i].key);
                json_free(v->object.members[i].value);
            }
            free(v->object.members);
            break;
        default: break;
    }
    free(v);
}

Recursive descent works because JSON's grammar is itself recursive (value can contain arrays and objects which contain more values) — each grammar rule becomes one function, and each function calls the ones for the rules nested inside it. This is not a toy technique: it's the same broad structure used by many hand-written parsers, including cJSON's (Section 36) and SQLite's SQL tokenizer/parser (Section 33). This compact example is intentionally incomplete: it omits escape sequences, exponent notation, literal validation, depth limits, and full syntax errors so the recursive-descent shape stays visible.


Generated from real-life-c-guide.md · 8 Oct 2026 16:36