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 (its diagram says port 8080; the code uses 8081)
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/tickgrid_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. (Its banner prints "port 8080", but the server always listens on 8081.)

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.
  • Housekeeping. ARCHITECTURE.md and start.sh mention port 8080 while the code uses 8081; package.json declares the MIT license but the repository contains no LICENSE file.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages