Conway's Game of Life where every grid cell is its own Unix process — 400 processes on the default 20×20 grid — coordinated over pipes by a single parent process and exposed through a hand-written HTTP server built directly on POSIX sockets.
A deliberately over-engineered Game of Life whose goal is to exercise Unix systems programming, not to run Life quickly. It has three parts:
- C backend (
backend/): a parent "grid manager" processfork()s one child process per cell. Each cell owns twopipe()s — commands in, responses out — and runs aselect()-based loop that exchanges fixed-size binaryMessagestructs with the parent. The same parent process also serves a small REST API from a hand-written HTTP/1.1 server (socket/setsockopt/bind/listen/accept, request parsing and response assembly by hand, no HTTP library) on TCP port 8081. - Angular 17 frontend (
src/): a standalone-component UI with a drawing canvas on the left and a live simulation view on the right, reaching the backend through the dev-server proxy (proxy.conf.json). - Benchmark tooling:
benchmark.pydrives the REST API to measure per-tick latency; one measured run is written up inBENCHMARK_RESULTS.txt.
This is an educational systems-programming project. Any array-based Life implementation is orders of magnitude faster; the point is to make operating-system concepts concrete and observable: process creation and teardown (fork, waitpid), pipe-based IPC with structured messages, select() loops, coordinator-style synchronization, and a from-scratch HTTP server. It also turned into a small profiling case study — the included benchmark shows that pipe IPC, not the Life computation, dominates each tick (see below).
backend/
Makefile gcc build (-Wall -Wextra -O2); targets: all/clean/run/debug
include/cell.h CellState, Message, MessageType, CellContext, MAX_NEIGHBORS
include/grid.h Grid/GridCell, MAX_GRID_SIZE (50)
include/server.h ServerContext, PORT (8081), BUFFER_SIZE (4096)
src/cell.c child cell process: select() loop, message handling, Conway rule
src/grid.c fork/pipe spawn, central tick, state collect/distribute, shutdown
src/server.c socket HTTP server and request routing
src/main.c entry point, argument parsing, SIGINT/SIGTERM handling
src/ Angular frontend (standalone components)
app/app.component.ts layout, controls, auto-run timing
app/draw-canvas.component.ts 20×20 pattern editor on an HTML canvas
app/simulation-view.component.ts live grid renderer
app/conway.service.ts HttpClient wrapper over /api
proxy.conf.json dev-server proxy: /api -> http://localhost:8081
benchmark.py REST-API latency benchmark (needs the `requests` package)
BENCHMARK_RESULTS.txt recorded benchmark write-up
ARCHITECTURE.md design notes (its diagram says port 8080; the code uses 8081)
start.sh builds and launches backend and frontend together
conway_server opens its listening socket on port 8081, then calls grid_spawn_cells (backend/src/grid.c): for each of the width × height cells it creates two pipes (parent_to_child, child_to_parent) and fork()s. Each child runs cell_process_main (backend/src/cell.c), blocking in select() for a fixed-size Message (backend/include/cell.h) on its parent pipe and replying on the other. The message types the running system uses are MSG_STATE_QUERY / MSG_STATE_RESPONSE, MSG_UPDATE, and MSG_SHUTDOWN. On teardown the parent broadcasts MSG_SHUTDOWN, closes both pipe ends, and reaps every child with waitpid (grid_destroy).
A generation advance (POST /api/tick → grid_tick in grid.c) works like this:
- The parent collects the current state of every cell — one blocking write/read pair per cell (
grid_get_state). - The parent computes the next generation itself, over the bounding box of live cells expanded by one (an empty grid returns immediately; this skips dead regions).
- The parent writes the new state back to every cell with
MSG_UPDATEand returns the incremented generation number over HTTP.
An honest architectural note: cell.c also contains a per-cell rule engine (MSG_TICK, cell_query_neighbors, cell_compute_next_state), but that path never runs. Nothing ever sends MSG_TICK, and direct cell-to-cell neighbor pipes are never wired up — grid_setup_neighbor_pipes is a no-op stub that is never even called, and every cell's neighbor_count stays 0. In the running program the cell processes are isolated state holders and the Conway rules execute centrally in the parent (grid_tick).
| Method | Path | Effect |
|---|---|---|
| GET | /api/state |
JSON: width, height, generation, and the list of live {x,y} cells |
| POST | /api/set |
Set one cell; body must be exactly {"x":5,"y":5,"alive":1} |
| POST | /api/tick |
Advance one generation; returns the new generation |
| POST | /api/clear |
Set every cell dead |
CORS headers (Access-Control-Allow-Origin: *, methods GET, POST, OPTIONS) are attached to every response and OPTIONS preflights are answered, so the Angular dev server can call the API directly.
Requirements: a POSIX system (Linux, macOS, or WSL) with gcc and make for the backend; Node.js and npm for the frontend; Python 3 with the requests package for the benchmark.
make -C backend # builds backend/bin/conway_server
./backend/bin/conway_server # default 20x20 grid = 400 cell processes
./backend/bin/conway_server 30 30 # custom width/height, each in 1..50 (MAX_GRID_SIZE)npm run build-backend and npm run start-backend wrap the first two commands. The port (8081) is hardcoded in backend/include/server.h. Additional Makefile targets: clean, debug (adds -g -DDEBUG), and run (build then launch).
npm install
npm start # ng serve on http://localhost:4200, proxying /api to :8081Open http://localhost:4200, draw a pattern on the left canvas, click "Load Pattern to Grid", then step one generation at a time or auto-run with the 50–1000 ms delay slider. The UI's grid is fixed at 20×20.
./start.sh builds the backend if it is missing, installs frontend dependencies if they are missing, launches the server, and starts npm start. (Its banner prints "port 8080", but the server always listens on 8081.)
curl http://localhost:8081/api/state
curl -X POST http://localhost:8081/api/set -H "Content-Type: application/json" -d '{"x":5,"y":5,"alive":1}'
curl -X POST http://localhost:8081/api/tick
curl -X POST http://localhost:8081/api/clearWith the server running:
pip install requests # no requirements.txt is provided
python3 benchmark.py # runs blinker/glider/random patterns against the APIThe recorded run in BENCHMARK_RESULTS.txt (20×20 grid, blinker) reports roughly 28–29 ms per tick, of which about 95% (~27 ms) is spent querying the 400 processes over pipes. The bounding-box optimization shrinks the rule computation about 26× (400 cells → ~15 for a blinker) but improves total tick time only ~2% — Amdahl's Law: IPC, not computation, is the bottleneck.
A working demo with deliberate, known limitations:
- Computation is centralized. Conway's rules run in the parent, not in the cell processes; the per-cell tick path in
cell.cis unreachable and neighbor pipes are never connected. Cells are process-per-cell state holders, not autonomous actors. - IPC is sequential. The parent talks to each of the 400 cells one at a time (a blocking write, then a blocking read), which is where nearly all tick time goes.
- The HTTP server is minimal. Single-threaded and blocking — one connection handled to completion at a time; a single
read()of at most 4 KB per request; the request line and JSON body are parsed withsscanf, so/api/setaccepts only the exact key order shown above. No TLS, no authentication,Access-Control-Allow-Origin: *. Local demo use only. - Fixed limits. Grid size is capped at 50×50 (
MAX_GRID_SIZE); the port 8081 is hardcoded; the grid is not toroidal (edges do not wrap); there is no state persistence; the Angular UI is hardcoded to a 20×20 view regardless of the backend grid size. - Unix only. It relies on
fork/pipe/selectand BSD sockets and will not build on native Windows — use WSL. - Housekeeping.
ARCHITECTURE.mdandstart.shmention port 8080 while the code uses 8081;package.jsondeclares the MIT license but the repository contains noLICENSEfile.