Skip to content

Instantly share code, notes, and snippets.

@MikeTeddyOmondi
Last active March 20, 2026 14:23
Show Gist options
  • Select an option

  • Save MikeTeddyOmondi/11cc9182e00dd7eaa382c1de40963439 to your computer and use it in GitHub Desktop.

Select an option

Save MikeTeddyOmondi/11cc9182e00dd7eaa382c1de40963439 to your computer and use it in GitHub Desktop.
Tinker with Arena Allocations
/*
* arena.h — Single-header arena allocator (STB-style)
*
* USAGE
* -----
* In exactly ONE .c file, before including this header:
*
* #define ARENA_IMPLEMENTATION
* #include "arena.h"
*
* In all other files (or multiple places in main.c) just:
*
* #include "arena.h"
*
* OPTIONS (define before ARENA_IMPLEMENTATION)
* #define ARENA_ASSERT(x) custom assert (default: <assert.h>)
* #define ARENA_MALLOC(n) custom allocator (default: malloc)
* #define ARENA_FREE(p) custom free (default: free)
*/
#ifndef ARENA_H
#define ARENA_H
#include <stddef.h> /* size_t */
#include <stdarg.h> /* va_list */
/* ------------------------------------------------------------------ */
/* Public types */
/* ------------------------------------------------------------------ */
typedef struct ArenaChunk ArenaChunk;
struct ArenaChunk {
ArenaChunk *next;
size_t cap;
size_t pos;
char buf[]; /* flexible array — data lives here */
};
typedef struct {
ArenaChunk *head; /* current (newest) chunk */
size_t chunk_size; /* default size for new chunks */
size_t total_allocd; /* bytes handed out (stats) */
} Arena;
/* Lightweight non-owning string slice — no NUL needed inside */
typedef struct {
const char *ptr;
size_t len;
} Str;
/* Saved position for rollback */
typedef struct {
ArenaChunk *chunk;
size_t pos;
} ArenaMark;
/* ------------------------------------------------------------------ */
/* Public API */
/* ------------------------------------------------------------------ */
/* Lifecycle */
Arena arena_create(size_t chunk_size);
void arena_destroy(Arena *a);
void arena_reset(Arena *a); /* free all chunks except first */
/* Allocation */
void *arena_alloc(Arena *a, size_t size);
void *arena_alloc_zero(Arena *a, size_t size);
/* Save / restore position (scratch-buffer pattern) */
ArenaMark arena_save(Arena *a);
void arena_restore(Arena *a, ArenaMark mark);
/* String helpers */
char *arena_strdup(Arena *a, const char *s);
char *arena_strdup_n(Arena *a, const char *s, size_t n);
char *arena_sprintf(Arena *a, const char *fmt, ...);
char *arena_concat(Arena *a, const char *s1, const char *s2);
Str str_from(const char *cstr);
Str str_slice(Str s, size_t start, size_t end);
int str_eq(Str a, Str b);
/* Stats / debug */
size_t arena_bytes_used(const Arena *a);
#endif /* ARENA_H */
/* ================================================================== */
/* IMPLEMENTATION — compiled only where ARENA_IMPLEMENTATION defined */
/* ================================================================== */
#ifdef ARENA_IMPLEMENTATION
#include <string.h>
#include <stdio.h>
#ifndef ARENA_ASSERT
# include <assert.h>
# define ARENA_ASSERT(x) assert(x)
#endif
#ifndef ARENA_MALLOC
# include <stdlib.h>
# define ARENA_MALLOC(n) malloc(n)
# define ARENA_FREE(p) free(p)
#endif
#define ARENA_ALIGN (sizeof(void *)) /* natural pointer alignment */
#define ARENA_DEFAULT_CHUNK (1024 * 64) /* 64 KB default chunk */
/* ------------------------------------------------------------------ */
/* Internal helpers */
/* ------------------------------------------------------------------ */
static size_t arena__align_up(size_t n) {
return (n + ARENA_ALIGN - 1) & ~(ARENA_ALIGN - 1);
}
static ArenaChunk *arena__new_chunk(size_t cap) {
ArenaChunk *c = ARENA_MALLOC(sizeof(ArenaChunk) + cap);
ARENA_ASSERT(c && "arena: out of memory");
c->next = NULL;
c->cap = cap;
c->pos = 0;
return c;
}
/* ------------------------------------------------------------------ */
/* Lifecycle */
/* ------------------------------------------------------------------ */
Arena arena_create(size_t chunk_size) {
if (chunk_size == 0) chunk_size = ARENA_DEFAULT_CHUNK;
Arena a;
a.chunk_size = chunk_size;
a.total_allocd = 0;
a.head = arena__new_chunk(chunk_size);
return a;
}
void arena_destroy(Arena *a) {
ArenaChunk *c = a->head;
while (c) {
ArenaChunk *next = c->next;
ARENA_FREE(c);
c = next;
}
a->head = NULL;
a->total_allocd = 0;
}
/* Keep first chunk, free the rest, reset positions */
void arena_reset(Arena *a) {
/* free all but last (oldest) chunk */
while (a->head && a->head->next) {
ArenaChunk *old = a->head;
a->head = a->head->next;
ARENA_FREE(old);
}
if (a->head) a->head->pos = 0;
a->total_allocd = 0;
}
/* ------------------------------------------------------------------ */
/* Allocation */
/* ------------------------------------------------------------------ */
void *arena_alloc(Arena *a, size_t size) {
size = arena__align_up(size);
/* Try to fit in current chunk */
if (a->head->pos + size <= a->head->cap) {
void *ptr = a->head->buf + a->head->pos;
a->head->pos += size;
a->total_allocd += size;
return ptr;
}
/* Need a new chunk — at least as large as requested */
size_t new_cap = a->chunk_size > size ? a->chunk_size : size * 2;
ArenaChunk *c = arena__new_chunk(new_cap);
c->next = a->head; /* prepend — newest first */
a->head = c;
void *ptr = c->buf;
c->pos = size;
a->total_allocd += size;
return ptr;
}
void *arena_alloc_zero(Arena *a, size_t size) {
void *ptr = arena_alloc(a, size);
memset(ptr, 0, size);
return ptr;
}
/* ------------------------------------------------------------------ */
/* Save / restore */
/* ------------------------------------------------------------------ */
ArenaMark arena_save(Arena *a) {
return (ArenaMark){ .chunk = a->head, .pos = a->head->pos };
}
void arena_restore(Arena *a, ArenaMark mark) {
/* Free any chunks allocated after the mark */
while (a->head != mark.chunk) {
ArenaChunk *old = a->head;
a->head = a->head->next;
ARENA_FREE(old);
}
a->head->pos = mark.pos;
}
/* ------------------------------------------------------------------ */
/* String helpers */
/* ------------------------------------------------------------------ */
char *arena_strdup(Arena *a, const char *s) {
ARENA_ASSERT(s);
size_t len = strlen(s) + 1;
char *dst = arena_alloc(a, len);
memcpy(dst, s, len);
return dst;
}
char *arena_strdup_n(Arena *a, const char *s, size_t n) {
ARENA_ASSERT(s);
char *dst = arena_alloc(a, n + 1);
memcpy(dst, s, n);
dst[n] = '\0';
return dst;
}
char *arena_sprintf(Arena *a, const char *fmt, ...) {
va_list ap;
/* First pass: measure */
va_start(ap, fmt);
int len = vsnprintf(NULL, 0, fmt, ap);
va_end(ap);
ARENA_ASSERT(len >= 0);
char *dst = arena_alloc(a, (size_t)len + 1);
/* Second pass: write */
va_start(ap, fmt);
vsnprintf(dst, (size_t)len + 1, fmt, ap);
va_end(ap);
return dst;
}
char *arena_concat(Arena *a, const char *s1, const char *s2) {
size_t l1 = strlen(s1), l2 = strlen(s2);
char *dst = arena_alloc(a, l1 + l2 + 1);
memcpy(dst, s1, l1);
memcpy(dst + l1, s2, l2);
dst[l1 + l2] = '\0';
return dst;
}
/* ------------------------------------------------------------------ */
/* Str (string slice) helpers */
/* ------------------------------------------------------------------ */
Str str_from(const char *cstr) {
return (Str){ .ptr = cstr, .len = strlen(cstr) };
}
Str str_slice(Str s, size_t start, size_t end) {
ARENA_ASSERT(start <= end && end <= s.len);
return (Str){ .ptr = s.ptr + start, .len = end - start };
}
int str_eq(Str a, Str b) {
return a.len == b.len && memcmp(a.ptr, b.ptr, a.len) == 0;
}
/* ------------------------------------------------------------------ */
/* Stats */
/* ------------------------------------------------------------------ */
size_t arena_bytes_used(const Arena *a) {
return a->total_allocd;
}
#endif /* ARENA_IMPLEMENTATION */
/*
* demo.c — Demonstrates arena.h in a realistic program:
* a tiny HTTP-like request parser that builds a config
* from key=value lines, then formats a response.
*
* Compile:
* gcc -Wall -Wextra -o demo demo.c
*/
/* ---- Bootstrap the implementation ONCE here ---- */
#define ARENA_IMPLEMENTATION
#include "arena.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ================================================================== */
/* Domain types — all strings point into arenas, never heap-alloc'd */
/* ================================================================== */
#define MAX_HEADERS 32
typedef struct {
Str key;
Str value;
} Header;
typedef struct {
Str method;
Str path;
Header headers[MAX_HEADERS];
int header_count;
Str body;
} Request;
typedef struct {
int status;
char *status_text; /* lives in perm arena */
char *body; /* lives in perm arena */
} Response;
/* ================================================================== */
/* Parser — uses SCRATCH arena for temp work, PERM for results */
/* ================================================================== */
/*
* parse_request()
*
* raw — the raw input bytes (not modified)
* scratch — temporary arena; we'll use save/restore around this call
* perm — permanent arena; parsed strings committed here
*/
static Request *parse_request(const char *raw, Arena *scratch, Arena *perm) {
Request *req = arena_alloc_zero(perm, sizeof(Request));
/* ---- Work on a mutable copy in scratch so we can tokenise ---- */
char *work = arena_strdup(scratch, raw);
/* First line: METHOD /path HTTP/1.1 */
char *line = work;
char *rest = NULL;
char *nl = strchr(line, '\n');
if (!nl) return NULL;
*nl = '\0';
rest = nl + 1;
char *sp1 = strchr(line, ' ');
if (!sp1) return NULL;
*sp1 = '\0';
char *sp2 = strchr(sp1 + 1, ' ');
if (sp2) *sp2 = '\0';
/* Commit method + path into perm arena */
req->method = str_from(arena_strdup(perm, line));
req->path = str_from(arena_strdup(perm, sp1 + 1));
/* ---- Parse headers: Key: Value\n ---- */
line = rest;
while (*line && *line != '\n' && *line != '\r') {
nl = strchr(line, '\n');
if (!nl) break;
*nl = '\0';
rest = nl + 1;
char *colon = strchr(line, ':');
if (!colon) { line = rest; continue; }
*colon = '\0';
/* Trim leading space from value */
const char *val = colon + 1;
while (*val == ' ') val++;
/* Trim trailing \r */
size_t vlen = strlen(val);
if (vlen && val[vlen - 1] == '\r') vlen--;
if (req->header_count < MAX_HEADERS) {
Header *h = &req->headers[req->header_count++];
/* key and value committed to perm */
h->key = str_from(arena_strdup(perm, line));
h->value = str_from(arena_strdup_n(perm, val, vlen));
}
line = rest;
}
/* ---- Body (everything after blank line) ---- */
if (*line == '\n') line++;
else if (*line == '\r' && *(line+1) == '\n') line += 2;
if (*line) req->body = str_from(arena_strdup(perm, line));
return req;
}
/* ================================================================== */
/* Business logic — build a Response in perm arena */
/* ================================================================== */
static Str request_header(const Request *req, const char *key) {
Str k = str_from(key);
for (int i = 0; i < req->header_count; i++) {
if (str_eq(req->headers[i].key, k))
return req->headers[i].value;
}
return (Str){ NULL, 0 };
}
static Response *handle_request(const Request *req,
Arena *scratch,
Arena *perm) {
Response *res = arena_alloc_zero(perm, sizeof(Response));
Str host = request_header(req, "Host");
Str ua = request_header(req, "User-Agent");
/* Use scratch arena for intermediate formatted strings */
char *host_str = host.ptr
? arena_strdup_n(scratch, host.ptr, host.len)
: arena_strdup(scratch, "(unknown)");
char *ua_str = ua.ptr
? arena_strdup_n(scratch, ua.ptr, ua.len)
: arena_strdup(scratch, "(unknown)");
/* Build path display string in scratch */
char *path_cstr = arena_strdup_n(scratch, req->path.ptr, req->path.len);
/* Determine status */
Str get = str_from("GET");
if (!str_eq(req->method, get)) {
res->status = 405;
res->status_text = arena_strdup(perm, "Method Not Allowed");
res->body = arena_sprintf(perm,
"405 Method Not Allowed\nOnly GET is supported.\n");
return res;
}
res->status = 200;
res->status_text = arena_strdup(perm, "OK");
/* Final body committed to perm */
res->body = arena_sprintf(perm,
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"\r\n"
"Hello from arena land!\n"
" Path : %s\n"
" Host : %s\n"
" User-Agent : %s\n",
path_cstr, host_str, ua_str
);
/* scratch work is discarded automatically when we reset it below */
(void)scratch;
return res;
}
/* ================================================================== */
/* Simulate processing N requests in a loop */
/* ================================================================== */
static const char *FAKE_REQUESTS[] = {
"GET /api/v1/farms HTTP/1.1\r\n"
"Host: locci.cloud\r\n"
"User-Agent: LocciScheduler/1.0\r\n"
"Accept: application/json\r\n"
"\r\n",
"POST /api/v1/jobs HTTP/1.1\r\n"
"Host: locci.cloud\r\n"
"User-Agent: curl/8.5\r\n"
"Content-Length: 42\r\n"
"\r\n"
"{\"cron\":\"0 * * * *\",\"target\":\"irrigate\"}",
"GET /healthz HTTP/1.1\r\n"
"Host: mt0.dev\r\n"
"User-Agent: k8s-probe/1.0\r\n"
"\r\n",
};
int main(void) {
/*
* Two arenas:
* perm — lives for the duration of the program (or request lifetime)
* scratch — reset after each request; zero-cost cleanup
*/
Arena perm = arena_create(1024 * 256); /* 256 KB permanent */
Arena scratch = arena_create(1024 * 16); /* 16 KB scratch */
int n = (int)(sizeof(FAKE_REQUESTS) / sizeof(FAKE_REQUESTS[0]));
for (int i = 0; i < n; i++) {
printf("━━━ Request %d ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n", i + 1);
/*
* Save scratch position BEFORE any per-request work.
* After the request, restore it — O(1) "free" of all temp strings.
*/
ArenaMark tick = arena_save(&scratch);
Request *req = parse_request(FAKE_REQUESTS[i], &scratch, &perm);
if (!req) { fprintf(stderr, "parse error\n"); continue; }
Response *res = handle_request(req, &scratch, &perm);
printf("Status : %d %s\n", res->status, res->status_text);
printf("Body :\n%s\n", res->body);
/* Blow away ALL scratch strings from this request — one assignment */
arena_restore(&scratch, tick);
printf("Perm bytes used so far : %zu\n", arena_bytes_used(&perm));
printf("Scratch bytes after reset: %zu\n", arena_bytes_used(&scratch));
printf("\n");
}
/* Single free for everything in perm — no per-string free() needed */
arena_destroy(&perm);
arena_destroy(&scratch);
return 0;
}
/*
* http.c — Minimal arena-backed HTTP/1.1 server
*
* Arena patterns demonstrated:
* perm arena — route table + server config (never reset)
* scratch arena — per-request allocation, reset after each request
* thread-local — one scratch arena per thread, zero lock contention
* arena_save/restore — rollback on parse error, free all per-request
* memory in O(1) at end of connection
*
* Compile:
* gcc -Wall -Wextra -pthread -o http_server http.c
*
* Run:
* ./http_server 8080
*
* Test:
* curl http://localhost:8080/
* curl http://localhost:8080/health
* curl -X POST http://localhost:8080/echo -d "hello"
* curl http://localhost:8080/stats
*/
#define ARENA_IMPLEMENTATION
#include "arena.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <stdatomic.h>
/* ================================================================== */
/* Config */
/* ================================================================== */
#define DEFAULT_PORT 8080
#define BACKLOG 128
#define RECV_BUF_SIZE (8 * 1024) /* 8 KB read buffer per request */
#define SCRATCH_SIZE (64 * 1024) /* 64 KB per-thread scratch arena */
#define PERM_SIZE (256 * 1024) /* 256 KB permanent arena */
#define MAX_HEADERS 32
#define MAX_ROUTES 64
/* ================================================================== */
/* Types */
/* ================================================================== */
typedef struct {
Str key;
Str value;
} Header;
typedef struct {
Str method;
Str path;
Str version;
Header headers[MAX_HEADERS];
int header_count;
Str body;
} Request;
typedef struct {
int status;
const char *status_text;
const char *content_type;
char *body; /* arena-allocated, may be NULL */
size_t body_len;
} Response;
/* Handler: receives parsed request + scratch arena, returns a Response.
* All strings in the Response must be allocated from scratch. */
typedef Response (*HandlerFn)(const Request *req, Arena *scratch);
typedef struct {
const char *method; /* "GET", "POST", "*" = any method */
const char *path; /* exact path, "*" = catch-all */
HandlerFn handler;
} Route;
typedef struct {
Route routes[MAX_ROUTES];
int route_count;
Arena *perm; /* routes are interned here */
} Router;
/* Passed through to each connection thread */
typedef struct {
int fd;
Router *router;
char client_ip[INET_ADDRSTRLEN];
} ConnCtx;
/* ================================================================== */
/* Global stats (atomic so threads can update safely) */
/* ================================================================== */
static atomic_size_t g_requests_total = 0;
static atomic_size_t g_requests_200 = 0;
static atomic_size_t g_requests_404 = 0;
static atomic_size_t g_requests_other = 0;
/* ================================================================== */
/* Thread-local scratch arena — one per thread, no locking needed */
/* ================================================================== */
static __thread Arena t_scratch;
static __thread int t_scratch_init = 0;
static Arena *get_scratch(void) {
if (!t_scratch_init) {
t_scratch = arena_create(SCRATCH_SIZE);
t_scratch_init = 1;
}
return &t_scratch;
}
/* ================================================================== */
/* Response helpers */
/* ================================================================== */
static Response make_response(int status, const char *status_text,
const char *content_type, char *body) {
return (Response){
.status = status,
.status_text = status_text,
.content_type = content_type,
.body = body,
.body_len = body ? strlen(body) : 0,
};
}
/*
* Serialise a Response to the fd.
* The header line is built in scratch — no static buffers, any size.
*/
static void write_response(int fd, const Response *res, Arena *scratch) {
char *header = arena_sprintf(scratch,
"HTTP/1.1 %d %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %zu\r\n"
"Connection: close\r\n"
"\r\n",
res->status,
res->status_text ? res->status_text : "OK",
res->content_type ? res->content_type : "text/plain",
res->body_len
);
write(fd, header, strlen(header));
if (res->body && res->body_len > 0)
write(fd, res->body, res->body_len);
}
/* ================================================================== */
/* Request parser */
/* */
/* All strings in the returned Request are Str slices into scratch. */
/* No copies into perm — everything disappears when scratch resets. */
/* ================================================================== */
static Request *parse_request(const char *raw, Arena *scratch) {
/*
* Save the scratch position before we touch it.
* On failure we roll back — the caller's mark is unaffected.
*/
ArenaMark mark = arena_save(scratch);
Request *req = arena_alloc_zero(scratch, sizeof(Request));
/* Mutable working copy so we can NUL-terminate in-place */
char *work = arena_strdup(scratch, raw);
char *rest;
/* ---- Request line: METHOD SP path SP HTTP/version CRLF ---- */
char *nl = strchr(work, '\n');
if (!nl) goto fail;
*nl = '\0';
rest = nl + 1;
/* trim trailing \r */
if (nl > work && *(nl-1) == '\r') *(nl-1) = '\0';
char *sp1 = strchr(work, ' ');
if (!sp1) goto fail;
*sp1 = '\0';
char *sp2 = strchr(sp1 + 1, ' ');
if (sp2) {
*sp2 = '\0';
req->version = str_from(sp2 + 1);
}
req->method = str_from(work);
req->path = str_from(sp1 + 1);
/* ---- Headers: Key: Value CRLF ... blank line ---- */
char *line = rest;
while (*line) {
/* blank line = end of headers */
if (*line == '\r' && *(line+1) == '\n') { line += 2; break; }
if (*line == '\n') { line += 1; break; }
nl = strchr(line, '\n');
if (!nl) break;
*nl = '\0';
rest = nl + 1;
size_t llen = strlen(line);
if (llen && line[llen-1] == '\r') line[--llen] = '\0';
char *colon = strchr(line, ':');
if (!colon) { line = rest; continue; }
*colon = '\0';
const char *val = colon + 1;
while (*val == ' ') val++;
if (req->header_count < MAX_HEADERS) {
Header *h = &req->headers[req->header_count++];
h->key = str_from(line);
h->value = str_from(val);
}
line = rest;
}
/* ---- Body (whatever remains after blank line) ---- */
if (*line) req->body = str_from(line);
return req;
fail:
arena_restore(scratch, mark);
return NULL;
}
/* ================================================================== */
/* Router */
/* ================================================================== */
/*
* Routes are stored in perm arena so they survive for the server lifetime.
* Lookup is O(n) linear scan — fine for small route tables.
*/
static void router_add(Router *r, const char *method, const char *path,
HandlerFn handler) {
if (r->route_count >= MAX_ROUTES) return;
Route *route = &r->routes[r->route_count++];
route->method = arena_strdup(r->perm, method);
route->path = arena_strdup(r->perm, path);
route->handler = handler;
}
static HandlerFn router_match(const Router *r, Str method, Str path) {
for (int i = 0; i < r->route_count; i++) {
const Route *rt = &r->routes[i];
int m = strcmp(rt->method, "*") == 0 || str_eq(method, str_from(rt->method));
int p = strcmp(rt->path, "*") == 0 || str_eq(path, str_from(rt->path));
if (m && p) return rt->handler;
}
return NULL;
}
/* ================================================================== */
/* Route handlers */
/* */
/* Each handler allocates its response body in scratch. */
/* The body is written to the socket then scratch is reset — gone. */
/* ================================================================== */
static Response handle_index(const Request *req, Arena *scratch) {
(void)req;
char *body = arena_strdup(scratch,
"arena-http server\n"
"\n"
"routes:\n"
" GET / this page\n"
" GET /health JSON health check\n"
" * /echo echo method, path, headers, body\n"
" GET /stats request counters\n"
);
return make_response(200, "OK", "text/plain", body);
}
static Response handle_health(const Request *req, Arena *scratch) {
(void)req;
char *body = arena_strdup(scratch, "{\"status\":\"ok\"}\n");
return make_response(200, "OK", "application/json", body);
}
static Response handle_echo(const Request *req, Arena *scratch) {
/*
* Build the response body incrementally with arena_sprintf.
* Each call allocates a new string in scratch — no fixed-size buffer.
*/
char *body = arena_sprintf(scratch,
"method : %.*s\n"
"path : %.*s\n"
"version : %.*s\n"
"headers : %d\n",
(int)req->method.len, req->method.ptr,
(int)req->path.len, req->path.ptr,
(int)req->version.len, req->version.ptr ? req->version.ptr : "",
req->header_count
);
/* Append each header — arena_concat keeps everything in scratch */
for (int i = 0; i < req->header_count; i++) {
const Header *h = &req->headers[i];
char *row = arena_sprintf(scratch, " %.*s: %.*s\n",
(int)h->key.len, h->key.ptr,
(int)h->value.len, h->value.ptr);
body = arena_concat(scratch, body, row);
}
if (req->body.ptr && req->body.len > 0) {
char *bline = arena_sprintf(scratch, "body : %.*s\n",
(int)req->body.len, req->body.ptr);
body = arena_concat(scratch, body, bline);
}
return make_response(200, "OK", "text/plain", body);
}
static Response handle_stats(const Request *req, Arena *scratch) {
(void)req;
size_t total = atomic_load(&g_requests_total);
size_t r200 = atomic_load(&g_requests_200);
size_t r404 = atomic_load(&g_requests_404);
size_t other = atomic_load(&g_requests_other);
char *body = arena_sprintf(scratch,
"{\n"
" \"requests_total\" : %zu,\n"
" \"requests_200\" : %zu,\n"
" \"requests_404\" : %zu,\n"
" \"requests_other\" : %zu\n"
"}\n",
total, r200, r404, other
);
return make_response(200, "OK", "application/json", body);
}
static Response handle_not_found(const Request *req, Arena *scratch) {
(void)req;
return make_response(404, "Not Found", "text/plain",
arena_strdup(scratch, "404 Not Found\n"));
}
static Response handle_method_not_allowed(const Request *req, Arena *scratch) {
(void)req;
return make_response(405, "Method Not Allowed", "text/plain",
arena_strdup(scratch, "405 Method Not Allowed\n"));
}
/* ================================================================== */
/* Connection handler — runs in a detached thread */
/* ================================================================== */
static void *conn_thread(void *arg) {
ConnCtx *ctx = arg;
Arena *scratch = get_scratch(); /* thread-local: no mutex needed */
/*
* Save scratch BEFORE any per-request work.
* arena_restore at the end = O(1) free of everything allocated
* for this request: read buffer, parsed request, response body.
*/
ArenaMark tick = arena_save(scratch);
/* Read the request into scratch — no fixed global buffer */
char *buf = arena_alloc(scratch, RECV_BUF_SIZE);
ssize_t n = recv(ctx->fd, buf, RECV_BUF_SIZE - 1, 0);
Response res;
if (n <= 0) {
goto done;
}
buf[n] = '\0';
{
Request *req = parse_request(buf, scratch);
if (!req) {
res = make_response(400, "Bad Request", "text/plain",
arena_strdup(scratch, "400 Bad Request\n"));
write_response(ctx->fd, &res, scratch);
goto log_done;
}
/*
* Route lookup:
* 1. Exact method + path match → call handler
* 2. Path matches but different method → 405
* 3. No path match → 404
*/
HandlerFn handler = router_match(ctx->router, req->method, req->path);
if (handler) {
res = handler(req, scratch);
} else {
Str any = str_from("*");
HandlerFn any_method = router_match(ctx->router, any, req->path);
res = any_method
? handle_method_not_allowed(req, scratch)
: handle_not_found(req, scratch);
}
write_response(ctx->fd, &res, scratch);
log_done:
/* Update stats */
atomic_fetch_add(&g_requests_total, 1);
if (res.status == 200) atomic_fetch_add(&g_requests_200, 1);
else if (res.status == 404) atomic_fetch_add(&g_requests_404, 1);
else atomic_fetch_add(&g_requests_other, 1);
printf("[%s] %.*s %.*s → %d (scratch used: %zu B)\n",
ctx->client_ip,
req ? (int)req->method.len : 1, req ? req->method.ptr : "?",
req ? (int)req->path.len : 1, req ? req->path.ptr : "?",
res.status,
arena_bytes_used(scratch));
}
done:
close(ctx->fd);
/*
* One restore call frees:
* - the 8 KB read buffer
* - the mutable working copy of the raw request
* - all parsed Str slices and the Request struct
* - the formatted response body
* - the serialised HTTP header line
*
* Zero individual free() calls. No leaks possible.
*/
arena_restore(scratch, tick);
free(ctx); /* ctx was malloc'd so the thread can own it */
return NULL;
}
/* ================================================================== */
/* main */
/* ================================================================== */
int main(int argc, char **argv) {
int port = argc > 1 ? atoi(argv[1]) : DEFAULT_PORT;
/*
* perm arena — lives for the server's lifetime.
* Route strings are interned here once at startup.
*/
Arena perm = arena_create(PERM_SIZE);
Router router = { .route_count = 0, .perm = &perm };
router_add(&router, "GET", "/", handle_index);
router_add(&router, "GET", "/health", handle_health);
router_add(&router, "*", "/echo", handle_echo);
router_add(&router, "GET", "/stats", handle_stats);
/* TCP socket */
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) { perror("socket"); return 1; }
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons((uint16_t)port),
.sin_addr.s_addr = INADDR_ANY,
};
if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind"); return 1;
}
if (listen(server_fd, BACKLOG) < 0) { perror("listen"); return 1; }
signal(SIGPIPE, SIG_IGN); /* ignore broken-pipe on disconnected clients */
printf("arena-http listening on :%d\n", port);
printf("perm arena : %d KB\n", PERM_SIZE / 1024);
printf("scratch/thr : %d KB (thread-local, reset each request)\n",
SCRATCH_SIZE / 1024);
printf("perm used at startup: %zu B (route table)\n\n",
arena_bytes_used(&perm));
while (1) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
if (fd < 0) { perror("accept"); continue; }
/*
* ConnCtx is malloc'd — the thread takes full ownership and free's it.
* Everything else (buffers, parsed data) goes into the thread-local
* scratch arena and is freed via arena_restore.
*/
ConnCtx *ctx = malloc(sizeof(ConnCtx));
if (!ctx) { close(fd); continue; }
ctx->fd = fd;
ctx->router = &router;
inet_ntop(AF_INET, &client_addr.sin_addr,
ctx->client_ip, sizeof(ctx->client_ip));
pthread_t tid;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, conn_thread, ctx);
pthread_attr_destroy(&attr);
}
/* Unreachable under normal operation */
arena_destroy(&perm);
close(server_fd);
return 0;
}
@MikeTeddyOmondi
Copy link
Author

.http file for CURL requests

# arena-http test requests
# Run server first: ./http_server 8080

### Index
curl -s http://localhost:8080/

### Health check
curl -s http://localhost:8080/health

### Echo GET with headers
curl -s http://localhost:8080/echo \
  -H "X-Request-Id: abc123" \
  -H "Accept: application/json"

### Echo POST with body
curl -s -X POST http://localhost:8080/echo \
  -H "Content-Type: text/plain" \
  -d "hello arena"

### Echo POST with JSON body
curl -s -X POST http://localhost:8080/echo \
  -H "Content-Type: application/json" \
  -d '{"farm":"locci","action":"irrigate"}'

### Stats
curl -s http://localhost:8080/stats

### 404 — unknown path
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/unknown

### 405 — wrong method on known path
curl -s -o /dev/null -w "%{http_code}" -X DELETE http://localhost:8080/health

### Verbose — see full request/response headers
curl -v http://localhost:8080/health

### Load test — 12 threads, 400 connections, 30s against /health
rewrk -t12 -c100 -d30s -pct -h http://localhost:8080/health

### Load test — hit /echo with POST body
rewrk -t12 -c400 -d30s -m POST --body "hello arena" http://localhost:8080/echo

### After load test — check stats
curl -s http://localhost:8080/stats

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment