Skip to content

Repository files navigation

Conway-IPC

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.

What it is

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" process fork()s one child process per cell. Each cell owns two pipe()s — commands in, responses out — and runs a select()-based loop that exchanges fixed-size binary Message structs 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.py drives the REST API to measure per-tick latency; one measured run is written up in BENCHMARK_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).

Repository layout

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
start.sh                 builds and launches backend and frontend together

How it works

Process model

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).

Tick flow

A generation advance (POST /api/tick → grid_tick in grid.c) works like this:

  1. The parent collects the current state of every cell — one blocking write/read pair per cell (grid_get_state).
  2. 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).
  3. The parent writes the new state back to every cell with MSG_UPDATE and 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).

HTTP API

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.

Building & running

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.

Backend

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).

Frontend

npm install
npm start          # ng serve on http://localhost:4200, proxying /api to :8081

Open 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.

Driving the API with curl

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/clear

Benchmark

With the server running:

pip install requests   # no requirements.txt is provided
python3 benchmark.py   # runs blinker/glider/random patterns against the API

The 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.

Status & caveats

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.c is 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 with sscanf, so /api/set accepts 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/select and BSD sockets and will not build on native Windows — use WSL.
  • No test suite. There are no automated tests. benchmark.py drives timing runs against the REST API, and its recorded output is in BENCHMARK_RESULTS.txt.

About

Conway's Game of Life as 400 concurrent Unix processes coordinated over pipes with select(), served by a hand-written HTTP/1.1 server on raw POSIX sockets, with an Angular 17 front end.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages