Last active
March 20, 2026 14:23
-
-
Save MikeTeddyOmondi/11cc9182e00dd7eaa382c1de40963439 to your computer and use it in GitHub Desktop.
Tinker with Arena Allocations
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * 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 */ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * 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; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * 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; | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
.http file for CURL requests