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.