---
title: 'Systems Programming with C'
source: 'https://academia.sh/en/courses/c-systems-programming'
language: en
updated: '2026-09-26T03:02:44+00:00'
license: 'CC BY-SA 4.0'
lessons: 15
outcomes:
    - 'Uses the operating system interface directly through file descriptors, processes and signals'
    - 'Tells whether a change is a copy, shared, a transfer or unobserved, and measures it'
    - 'Shares data by a defined path through pipes, shared memory and threads'
    - 'Implements linked structures, ring buffers and hash tables with an ownership contract'
    - 'Explains which change the build, library, test and debugging infrastructure misses'
---

# Systems Programming with C

In systems programming, the side that makes a change and the side that will read it often do not hold the same copy: the course measures every mechanism with a single question — who sees this change; only the writer's copy, everyone holding the same object, a reader on the other end of an intermediary, or nobody at all? File descriptors, processes, signals, threads, data structures, and build and test infrastructure all pass through the same frame.

## System Interfaces

File descriptors and the shared file offset, creating and waiting for processes, signal handling, inter-process communication through pipes, shared memory and sockets, and sharing under a lock in POSIX threads — each paired with the question of who sees a change.

### File Descriptors

Source: https://academia.sh/en/courses/c-systems-programming/file-descriptors

Whether a pointer refers to the same memory location is measured by equality; whether a file descriptor shares the same open file description is measured by offset behavior instead — the descriptor itself is an integer and says nothing on its own.

The previous course asked who would free a block: when a pointer referred to a
record, which side owed the obligation to release it. This course asks a different
question. When one side changes something, does the other side see it? `fork` copies
memory, `dup` shares an open file, a thread shares the address space, a pipe carries
bytes across once. Each mechanism answers this question differently, and throughout
the course the answer has four fixed names: **copy**, **shared**, **handoff**,
**unobserved**. This lesson shows all four in a single run, then looks a little
closer at the first of them — file descriptors.

#### Who Sees It: A Full Run of the Core

The measurement is built from two parts. `observe.h` and `observe.c` are a small unit
that records a change's name, its class, and whether the other side saw it. The
counts live only inside `observe.c`; no lesson touches them from the outside.

```c
// observe.h — a record that classifies whom a change reaches
#ifndef OBSERVE_H
#define OBSERVE_H

enum Class { COPY, SHARED, HANDOFF, UNOBSERVED };

/* Records one change: seen is whether the side that did not make the
   change can read it (1 or 0). */
void observe(const char *change, enum Class cls, int seen);

/* Prints the count per class and the reached / not reached totals. */
void observe_summary(void);

#endif
```

```c
// observe.c — the counts live only in this unit
#include <stdio.h>
#include "observe.h"

static const char *const names[] = { "copy", "shared", "handoff", "unobserved" };
static int counts[4];
static int reached = 0;
static int not_reached = 0;

void observe(const char *change, enum Class cls, int seen) {
    /* shared and handoff reach, copy and unobserved do not */
    int expected = (cls == SHARED || cls == HANDOFF);
    counts[cls]++;
    if (seen) {
        reached++;
    } else {
        not_reached++;
    }
    printf("%-34s %-8s %s%s\n", change, seen ? "saw" : "missed",
           names[cls], seen == expected ? "" : "  MISMATCH");
}

void observe_summary(void) {
    printf("classes: copy %d, shared %d, handoff %d, unobserved %d\n",
           counts[COPY], counts[SHARED], counts[HANDOFF], counts[UNOBSERVED]);
    printf("reached %d, not reached %d\n", reached, not_reached);
}
```

The third unit, `who_sees.c`, measures each of fifteen small changes with these two
functions. This lesson runs the **whole** core; the remaining fourteen lessons will
each take only their own slice, not rewrite this file.

```c
// who_sees.c — fifteen changes, one question for each: who sees it
#define _POSIX_C_SOURCE 200809L
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include "observe.h"

/* 1. File descriptor: a descriptor duplicated with dup shares the offset */
static void descriptor(void) {
    int a = open("who_sees.txt", O_CREAT | O_RDWR | O_TRUNC, 0600);
    if (write(a, "abcdef", 6) != 6) {
        return;
    }
    int b = dup(a);
    lseek(a, 2, SEEK_SET);
    observe("offset shared through dup", SHARED, lseek(b, 0, SEEK_CUR) == 2);
    close(b);
    close(a);
    unlink("who_sees.txt");
}

/* 2. Process: the variable the child writes does not change in the parent */
static void process(void) {
    int value = 0;
    fflush(stdout);
    pid_t p = fork();
    if (p == 0) {
        value = 1;
        _exit(value);
    }
    int status = 0;
    waitpid(p, &status, 0);
    observe("child's variable after fork", COPY, value == 1);
}

/* 3. Signal: the flag the handler raises reaches the main flow once */
static volatile sig_atomic_t flag = 0;
static void handler(int s) {
    (void)s;
    flag = 1;
}
static void signals(void) {
    struct sigaction sa;
    memset(&sa, 0, sizeof sa);
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGUSR1, &sa, NULL);
    raise(SIGUSR1);
    observe("flag raised by the handler", HANDOFF, flag == 1);
}

/* 4. Pipe: the value the child writes is carried to the parent once */
static void pipes(void) {
    int ends[2];
    if (pipe(ends) != 0) {
        return;
    }
    fflush(stdout);
    pid_t p = fork();
    if (p == 0) {
        int value = 42;
        close(ends[0]);
        ssize_t n = write(ends[1], &value, sizeof value);
        _exit(n == (ssize_t)sizeof value ? 0 : 1);
    }
    close(ends[1]);
    int received = 0;
    ssize_t n = read(ends[0], &received, sizeof received);
    waitpid(p, NULL, 0);
    close(ends[0]);
    observe("value written to the pipe", HANDOFF, n == (ssize_t)sizeof received && received == 42);
}

/* 5. Thread: a counter set under the lock reaches the main thread */
static int counter = 0;
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static void *increment(void *arg) {
    (void)arg;
    pthread_mutex_lock(&lock);
    counter = 7;
    pthread_mutex_unlock(&lock);
    return NULL;
}
static void thread(void) {
    pthread_t t;
    pthread_create(&t, NULL, increment, NULL);
    pthread_join(t, NULL);
    pthread_mutex_lock(&lock);
    int seen = (counter == 7);
    pthread_mutex_unlock(&lock);
    observe("counter written by the thread", SHARED, seen);
}

/* 6. Linked structure: a change made through a second pointer shows in the list */
struct Node {
    int value;
    struct Node *next;
};
static void linked(void) {
    struct Node c = { 3, NULL };
    struct Node b = { 2, &c };
    struct Node a = { 1, &b };
    struct Node *alias = &b;
    alias->value = 20;
    int total = 0;
    for (struct Node *d = &a; d != NULL; d = d->next) {
        total += d->value;
    }
    observe("node changed through an alias", SHARED, total == 24);
}

/* 7. Ring buffer: an item put in reaches the taking side once */
enum { CAPACITY = 4 };
struct Ring {
    int items[CAPACITY];
    int head;
    int count;
};
static int ring_put(struct Ring *r, int item) {
    if (r->count == CAPACITY) {
        return 0;
    }
    r->items[(r->head + r->count) % CAPACITY] = item;
    r->count++;
    return 1;
}
static int ring_take(struct Ring *r, int *item) {
    if (r->count == 0) {
        return 0;
    }
    *item = r->items[r->head];
    r->head = (r->head + 1) % CAPACITY;
    r->count--;
    return 1;
}
static void ring(void) {
    struct Ring r = { { 0 }, 0, 0 };
    int taken = 0;
    ring_put(&r, 5);
    int first = ring_take(&r, &taken) && taken == 5;
    int second = ring_take(&r, &taken);
    observe("item put in the ring", HANDOFF, first && !second);
}

/* 8. Hash table: the table keeps its own copy of the key */
struct Entry {
    char key[16];
    int value;
};
static void hashing(void) {
    char buffer[16] = "apple";
    struct Entry e;
    strcpy(e.key, buffer);
    e.value = 1;
    strcpy(buffer, "pear");
    observe("key changed after insertion", COPY, strcmp(e.key, "pear") == 0);
}

/* 9. Object-like design: two users holding the same object */
struct Account {
    int balance;
};
static void account_deposit(struct Account *a, int amount) {
    a->balance += amount;
}
static int account_balance(const struct Account *a) {
    return a->balance;
}
static void object(void) {
    struct Account account = { 0 };
    struct Account *first = &account;
    const struct Account *second = &account;
    account_deposit(first, 50);
    observe("deposit made by one user", SHARED, account_balance(second) == 50);
}

/* 10. Cleanup: a buffer that is never flushed vanishes with _exit */
static void cleanup(void) {
    int ends[2];
    if (pipe(ends) != 0) {
        return;
    }
    fflush(stdout);
    pid_t p = fork();
    if (p == 0) {
        close(ends[0]);
        FILE *stream = fdopen(ends[1], "w");
        if (stream == NULL) {
            _exit(1);
        }
        setvbuf(stream, NULL, _IOFBF, 256);
        fputs("line left in the buffer\n", stream);
        _exit(0);
    }
    close(ends[1]);
    char incoming[64];
    ssize_t n = read(ends[0], incoming, sizeof incoming);
    waitpid(p, NULL, 0);
    close(ends[0]);
    observe("buffer lost on exit, not flushed", UNOBSERVED, n > 0);
}

/* 11. Build system (model): a unit with no listed dependency is not rebuilt */
struct Unit {
    int depends_on_header;
    int built_version;
};
static void build_unit(struct Unit *u, int header_version, int header_time, int object_time) {
    if (u->depends_on_header && header_time > object_time) {
        u->built_version = header_version;
    }
}
static void build(void) {
    struct Unit unit = { 0, 1 };
    build_unit(&unit, 2, 20, 10);
    observe("header with no listed dependency", COPY, unit.built_version == 2);
}

/* 12. Library (model): static linking takes a copy of the code */
struct Library {
    int version;
};
struct Program {
    int embedded_version;
    const struct Library *linked;
};
static void library(void) {
    struct Library lib = { 1 };
    struct Program static_build = { lib.version, NULL };
    lib.version = 2;
    observe("update to a static library", COPY, static_build.embedded_version == 2);
}

/* 13. Test: a change made by a test in its own process does not reach the next */
static int shared_state = 0;
static void testing(void) {
    fflush(stdout);
    pid_t p = fork();
    if (p == 0) {
        shared_state = 9;
        _exit(0);
    }
    waitpid(p, NULL, 0);
    observe("change made by an isolated test", COPY, shared_state == 9);
}

/* 14. Debugger (model): the value between two stops */
static void debugger(void) {
    int values[3] = { 1, 2, 3 };
    int seen_values[2];
    seen_values[0] = values[0]; /* first stop */
    seen_values[1] = values[2]; /* second stop */
    observe("value between two stops", UNOBSERVED,
            seen_values[0] == 2 || seen_values[1] == 2);
}

/* 15. Optimization (model): a write that no read ever saw */
struct Step {
    char op;
    int value;
};
static void optimization(void) {
    struct Step flow[] = { { 'w', 1 }, { 'w', 2 }, { 'r', 0 } };
    int readers = 0;
    int last_write = -1;
    for (int i = 0; i < 3; i++) {
        if (flow[i].op == 'w') {
            last_write = i;
        } else if (last_write == 0) {
            readers++;
        }
    }
    observe("write overwritten before any read", UNOBSERVED, readers > 0);
}

int main(void) {
    descriptor();
    process();
    signals();
    pipes();
    thread();
    linked();
    ring();
    hashing();
    object();
    cleanup();
    build();
    library();
    testing();
    debugger();
    optimization();
    observe_summary();
    return 0;
}
```

```text
offset shared through dup          saw      shared
child's variable after fork        missed   copy
flag raised by the handler         saw      handoff
value written to the pipe          saw      handoff
counter written by the thread      saw      shared
node changed through an alias      saw      shared
item put in the ring               saw      handoff
key changed after insertion        missed   copy
deposit made by one user           saw      shared
buffer lost on exit, not flushed   missed   unobserved
header with no listed dependency   missed   copy
update to a static library         missed   copy
change made by an isolated test    missed   copy
value between two stops            missed   unobserved
write overwritten before any read  missed   unobserved
classes: copy 5, shared 4, handoff 3, unobserved 3
reached 7, not reached 8
```

**SI1.** None of the fifteen lines carries a `MISMATCH` mark: every observation
matches its expected class. Five changes are copy, four shared, three handoff, three
unobserved — and seven reach the other side while eight do not. This fifteen-line
list is a map for the rest of the course: each lesson takes one line from here and
goes deeper. This lesson's share is the first line: `offset shared through dup`.

#### The Offset Shared Through dup

**SI2.** The `descriptor` function opens a file with the descriptor `a`, writes six
bytes, then derives a second descriptor — `b` — with `dup(a)`. The offset is moved
through `a`; the question measured is whether the offset read through `b` also moved.
The output says `saw`, meaning it did. The reason is not in the descriptor numbers but
in the three-layer structure the operating system keeps: every process keeps its own
descriptor table, every descriptor points at an **open file description**, and every
open file description carries an **offset**. Every time `open` is called it creates a
new open file description — and with it a new offset. `dup`, on the other hand, does
not open a new description; it hands back a second descriptor pointing at the
existing one. `a` and `b` are different integers, but they point at the same
description; when one moves the offset, the other reads that same offset.

Had we opened the same file with `open` twice instead — `a` once, then the same path
again as `c` — the situation would differ. Two separate open file descriptions would
be created, each keeping its own offset; moving the offset through `a` would leave
`c` at zero. Sharing a description is not established by `open` but by deriving a
descriptor that **points at** the same description: `dup`, `dup2`, or descriptors
inherited during `fork`. `who_sees.c` shows this distinction with `dup`; the next
lesson carries the same inheritance forward with `fork`, but there the subject is not
a shared offset, it is copied memory.

#### The Bounding Measurement

A descriptor is only a small integer; it says nothing on its own. Whether two
descriptors look at the same description or at two separate ones can never be told
by looking at their numeric values. The only reliable measurement is the behavior of
the **offset** the description carries: if the offset changed through one is also
read through the other, the description is shared; if not, there are two separate
descriptions. That is why `who_sees.c` reads the offset value with
`lseek(b, 0, SEEK_CUR)` and never prints the descriptor number anywhere — the
descriptor number depends on the target and the run, the offset behavior does not.

#### Summary

- The four classes that stay fixed throughout the course — copy, shared, handoff,
  unobserved — all answer one question: does the side that did not make the change
  see it.
- The core's fifteen observations split into five copy, four shared, three handoff,
  three unobserved; seven reach the other side, eight do not.
- A descriptor derived with `dup` does not open a new open file description; it opens
  a second path to the existing one, so it shares the offset.
- A separate `open` call always creates a separate open file description, and
  therefore a separate offset — even when it looks at the same file.
- This lesson's class is **shared**: because the two descriptors hold the same
  description, when one changes it, the other sees it too.

#### Next Step

The next lesson asks the same question of `fork`. `dup` made a description shared;
`fork` does not share memory, it copies it — the child's variable stays in its own
copy, the parent never sees it.

### Process Management

Source: https://academia.sh/en/courses/c-systems-programming/process-management

fork splits a process in two and hands the child a copy of the memory at that moment; once the copy is made, the two sides are independent of each other — no change reaches the other side in either direction.

The previous lesson showed that a descriptor derived with `dup` shares the same open
file description: when one moved the offset, the other saw it. `fork` does the
opposite. The moment it is called it hands the child a copy of the process's memory,
and once that copy is made the two sides are completely independent of each other.
One of the course's four classes, **copy**, describes exactly this: the change stays
only on the side that made it, the other side never sees it. This lesson measures
that independence in both directions, across two separate `fork` calls and two
different data types: first two plain integers, then through the macros that read
the exit status, and finally through a small structure that models standard output's
buffer.

#### Measuring the Copy in Both Directions

The measurement units stay the same: `observe.h` and `observe.c` are identical to
the previous lesson's. The new part in this file is `process_measurement.c`, which
uses two `fork` calls back to back: the first measures two plain integers in both
directions, the second measures a small structure that models standard output's
buffer the same way.

```c
// observe.h — a record that classifies whom a change reaches
#ifndef OBSERVE_H
#define OBSERVE_H

enum Class { COPY, SHARED, HANDOFF, UNOBSERVED };

/* Records one change: seen is whether the side that did not make the
   change can read it (1 or 0). */
void observe(const char *change, enum Class cls, int seen);

/* Prints the count per class and the reached / not reached totals. */
void observe_summary(void);

#endif
```

```c
// observe.c — the counts live only in this unit
#include <stdio.h>
#include "observe.h"

static const char *const names[] = { "copy", "shared", "handoff", "unobserved" };
static int counts[4];
static int reached = 0;
static int not_reached = 0;

void observe(const char *change, enum Class cls, int seen) {
    /* shared and handoff reach, copy and unobserved do not */
    int expected = (cls == SHARED || cls == HANDOFF);
    counts[cls]++;
    if (seen) {
        reached++;
    } else {
        not_reached++;
    }
    printf("%-34s %-8s %s%s\n", change, seen ? "saw" : "missed",
           names[cls], seen == expected ? "" : "  MISMATCH");
}

void observe_summary(void) {
    printf("classes: copy %d, shared %d, handoff %d, unobserved %d\n",
           counts[COPY], counts[SHARED], counts[HANDOFF], counts[UNOBSERVED]);
    printf("reached %d, not reached %d\n", reached, not_reached);
}
```

```c
// process_measurement.c — two separate forks, two separate data types, the same copy rule
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include "observe.h"

/* Model: not a real stdio buffer, a small structure that follows the same copy rule */
typedef struct {
    char data[32];
    int length;
} ModelBuffer;

static void model_write(ModelBuffer *b, const char *s) {
    int n = 0;
    while (s[n] != '\0' && b->length < 31) {
        b->data[b->length] = s[n];
        b->length++;
        n++;
    }
}

int main(void) {
    int value_a = 0;
    int value_b = 0;

    fflush(stdout);
    pid_t p = fork();
    if (p == 0) {
        value_a = 1;      /* changes only in the child's own copy */
        _exit(value_b);   /* exits without ever seeing the parent's later change */
    }

    value_b = 7;           /* after fork, changes only in the parent's copy */
    int status = 0;
    waitpid(p, &status, 0);

    observe("child's variable after fork", COPY, value_a == 1);
    observe("parent's variable after fork", COPY, WEXITSTATUS(status) == 7);
    printf("child's normal exit (WIFEXITED): %s\n", WIFEXITED(status) ? "yes" : "no");

    /* second fork: does the model buffer follow the same rule too */
    ModelBuffer buffer;
    memset(&buffer, 0, sizeof buffer);
    model_write(&buffer, "previous");   /* model of content accumulated before fork, not yet flushed */

    fflush(stdout);
    pid_t p2 = fork();
    if (p2 == 0) {
        model_write(&buffer, "child");
        _exit(buffer.length);
    }
    model_write(&buffer, "parent");
    int status2 = 0;
    waitpid(p2, &status2, 0);

    observe("did the child's addition to the buffer reach the parent", COPY,
           WEXITSTATUS(status2) == buffer.length);

    observe_summary();
    return 0;
}
```

```text
child's variable after fork        missed   copy
parent's variable after fork       missed   copy
child's normal exit (WIFEXITED): yes
did the child's addition to the buffer reach the parent missed   copy
classes: copy 3, shared 0, handoff 0, unobserved 0
reached 0, not reached 3
```

**SI3.** Both of the first two lines say `missed`. The child sets `value_a` to `1` in
its own copy; after `waitpid`, the parent looks at its own `value_a` and still finds
`0` — the child's write never reached the parent's copy. Symmetrically, the parent
sets its own `value_b` to `7` after `fork` returns; the child exits with
`_exit(value_b)` without ever seeing that line, and since `value_b` in its own copy
is still `0`, its exit code is also `0`. When the parent reads that with
`WEXITSTATUS` and finds `0`, not `7`, it confirms that its own change never reached
the child at all. Both fall into the same class: **copy**. `fork` copies once, and
after that the two sides are unaware of each other — which side writes first and
which writes second makes no difference here, because there is no shared record,
there are two independent copies.

#### Measuring the fflush Rule

**SI4.** The third line carries this independence from an integer into a multi-line
buffer. `ModelBuffer` stands in for a real `stdio` buffer: before `fork` it
accumulates the text `"previous"`, just like an unflushed output line waiting to be
written to the terminal. When `fork` is called this structure is copied along with
the rest of memory too — the child adds `"child"` to its own copy and reports its
length through the exit code, the parent adds `"parent"` to its own copy. Because
`"child"` and `"parent"` are different lengths, the two sides' final lengths never
come out equal, and the measurement reads this as `missed`: the child's addition to
its own buffer never reached the parent's buffer. The rule that holds in every lesson
of the course is exactly what this measurement shows: the `fflush(stdout)` call
before `fork` clears whatever unflushed content sits inside the real `stdio` buffer,
so there is nothing left to copy. Skip that line, and content that has not yet been
written — like `"previous"` — sits in both the parent's and the child's copy
together; if each flushes its own buffer at its own close, the same line appears
twice — once from the parent, once from the child, because now both carry a separate
copy belonging to the same class, **copy**. `process_measurement.c` closes off this
risk twice: `fflush(stdout)` is called right before each of the two `fork` calls, and
the `observe` calls themselves only run on the parent side after `waitpid`; the child
never prints anything, it exits silently with `_exit`.

#### Exit Status Macros

**SI5.** The `status` that `waitpid` fills in is a single integer, but it encodes two
separate questions: how the process ended, and with what value. `WEXITSTATUS(status)`
only answers the second question — it returns the value the child gave to `_exit` or
`return` — but this macro is only meaningful if the first question is affirmative,
that is, if the child really did exit the normal way. `WIFEXITED(status)` answers the
first question: it confirms that the child ended by its own choice, not through a
signal. This lesson finds `WIFEXITED` true after both `fork` calls, because both
children exit with `_exit`; the distinction stays abstract until the upcoming
lesson on signals shows the macro's counterpart, a process that ends through a
signal. The point here is this: relying on `WEXITSTATUS` without a `WIFEXITED` check
can be misleading, because the `status` field of a process that ended through a
signal carries the same bits for a different purpose — this lesson does not run
that case, but records here why the macro is not sufficient on its own.

The value `WEXITSTATUS` returns is limited to eight bits, because POSIX guarantees
carrying the exit code at that width; both measurements inside
`process_measurement.c` were chosen to stay under that limit — `value_b` is seven,
the model buffer's length is under thirty-one. A program that gives `_exit` a
three-digit or negative value and then reads the exit code expecting that value can
produce a misleading result because of this eight-bit truncation; this lesson does
not measure that case separately, but the reason the `observe` calls choose small
fixed values is exactly to avoid that limit.

`WIFEXITED`'s counterpart is `WIFSIGNALED`: this macro returns true if the child
ended because of a signal, and in that case `WTERMSIG(status)` reads which signal
ended it. This lesson does not run that macro, following the rule that forbids
printing a signal number or name — it would neither produce a printable value nor
add anything to this lesson's measurement axis, because a process that ended through
a signal has no such thing as an exit code, only the signal that ended it. A program
that relies on `WEXITSTATUS` after `waitpid` without checking both `WIFEXITED` and
`WIFSIGNALED` ends up reading an undefined value as if it were an exit code, for a
child that ended through a signal; so the distinction between the two is not merely
a style preference, it is a precondition for reading correctly.

#### The exec Family

**SI6.** After `fork`, the child process is usually followed by a second step: a call
from the `exec` family. `fork` copies memory but does not change the program code —
the child is still a copy of the same program. `exec`, on the other hand, keeps the
process identity but rebuilds the process **image** from scratch: the code, data,
and stack are replaced with a new program's, and when the call succeeds it does not
return. This lesson does not run `exec`, because the question it measures — who sees
what — loses its meaning after `exec`: the old process's variables, its `value_a`,
its `value_b`, its model buffer, are all gone, replaced with new ones. Using `fork`
and `exec` together is the foundation of a shell starting a new program: the shell
first produces its own copy with `fork`, the child process inherits the environment
variables and open descriptors, and then loads the desired program over that copy
with `exec`. The parent shell never changes; only the child calls `exec`, and once it
succeeds it is no longer the shell, it is the program that was called.

The members of the `exec` family differ from each other along three axes, all in
prose: how they take their arguments (`execl` and similar take a variable number of
arguments, `execv` and similar take an array), how they find the program (`execve`
only accepts a full path, `execvp` searches the `PATH` environment variable), and how
they inherit the environment (`execve` and `execvpe` accept a new environment array,
the others use the calling process's environment as is). What they all share is that
there is no return value — if the call succeeds it does not return, if it fails it
returns `-1` and the calling process continues with its own variables. An `exec`
called without `fork` irreversibly turns the calling process into another program;
that is why a shell starting a new program without losing its own process depends on
producing a copy first with `fork` and calling `exec` in that copy. Open file
descriptors stay open across `exec` by default too — unless the `FD_CLOEXEC` flag is
set, the descriptors the child inherited from `fork` are still valid in the new
program; a shell's implementation of redirection syntax (`>`, `<`) rests exactly on
this: the shell first moves a descriptor to the desired slot with `dup2`, then calls
`exec`, and the new program uses that descriptor without ever knowing where it came
from.

How `fork` produces its memory copy is not this lesson's subject; the operating
systems course's memory management section already answered that question — the
kernel most of the time does not actually duplicate pages, it marks them with
copy-on-write, and the real copy only happens when one side writes. This lesson does
not touch that internal machinery; the only thing it measures is that, whatever
mechanism does the copying, the two sides end up independent — whether it is an
integer or a multi-line buffer.

The `ModelBuffer` measurement does not imitate a real stdio buffer's internal
structure; it is just a small structure of our own, following the same copy rule.
The generality here matters: the byte sequence a real `FILE*` buffer holds, and the
integer variable this lesson measures elsewhere, are both subject to the same copy
rule at the moment of `fork` — the child takes the parent's state at that moment,
and afterward the two change independently. `ModelBuffer` makes this generality
concrete: a counter three bytes wide and an array thirty-two bytes wide get equal
treatment from `fork`'s point of view. The difference in real stdio is that the
library's decision, not the programmer's, determines when the buffer is flushed — a
line-buffered terminal flushes at the end of every line, a fully buffered file only
flushes when the buffer fills or the program closes. `fork` does not change this
flush timing, it only splits the content accumulated up to that point into two
independent copies; what violates the rule is not the absence of an `fflush(stdout)`
call, it is the expectation that accumulated content will be flushed twice.

#### The Bounding Measurement

This measurement's boundary is that it is independent of timing, and that is a
deliberate design choice. Whichever order the child's `_exit` call and the parent's
later write run in, the result does not change, because neither writes into the same
memory — each writes into its own copy. That is why the measurement needs no `sleep`
or wait; the `waitpid` call alone is enough, because it confirms not the ordering but
the independence of the copies. The model buffer measurement carries the same
boundary: choosing `"child"` and `"parent"` at different lengths is not arbitrary —
it is deliberate, so the result comes out the same on every run, because this lesson
measures not a race condition but only the presence of copying. Other classes in the
course — handoff in particular — can be sensitive to ordering, but for the copy
class, ordering never changes the result.

Another question this measurement does not cover is how the result changes with the
number of `fork` calls. `process_measurement.c` calls two `fork`s back to back, the
second only after the first child has fully closed — the second `fork` does not wait
for the first child to drop out of the process table, because `waitpid` already
collected the first one. Had a program instead started two children at the same
time, the second without waiting for the first to finish, both children would still
each carry a separate copy of the same parent memory; the copy class's guarantee is
independent of the number of children, because every `fork` call produces its own
new, independent copy on its own, unaware of previous children. This lesson does not
produce more than one child at a time, because what the result shows is already
fully proven through a single pair; extra children would only repeat the same
guarantee, not introduce a new class or a new boundary.

#### Summary

- `fork` hands the child a copy of the memory state at the moment it is called; once
  the copy is made, the two sides are independent.
- This independence runs both ways: a variable the child writes does not reach the
  parent, and a variable the parent writes after `fork` does not reach the child
  either.
- The same independence is not limited to a single integer; a larger structure, like
  a multi-line buffer, follows the same rule too.
- `fflush(stdout)` is mandatory before `fork`; otherwise buffered content gets
  copied and the same line can be written twice.
- `WIFEXITED` and `WEXITSTATUS` are read together: one says how the process ended,
  the other with what value; without one, the other can be misleading.
- The `exec` family rebuilds the process image from scratch and does not return;
  this lesson does not run it, because the question of copied variables becomes
  meaningless after `exec`.

#### Next Step

The next lesson moves to a completely different way of carrying a change than the
variables `fork` copies: a flag a signal handler raises, a handoff that reaches the
main flow only once.

### Signals

Source: https://academia.sh/en/courses/c-systems-programming/signals

A signal handler runs outside the main flow, at an unexpected point; the only safe form of sharing is a volatile sig_atomic_t flag, and if a signal arrives again while blocked it is delivered only once when it is unblocked.

`fork` gave a copy, and the two sides stayed unaware of each other. This lesson
measures a different way of carrying a change: a **signal**. While a process
continues its normal flow, the kernel stops it, runs a registered handler, the
handler returns, and the flow continues where it left off. The handler and the main
flow share the same memory space, but this sharing is not a copy like `fork`'s — the
trace the handler leaves is a handoff that reaches the main flow **once**. The
course's class **handoff** describes exactly this.

This lesson uses `raise(SIGUSR1)`, not `kill` or an external source — so the
measurement is self-contained. In real use a signal can come from three sources:
another process sends it with a `kill` call, the kernel generates it in response to
a hardware condition (invalid memory access, an arithmetic error), or the process
sends it to itself with `raise`. All three use the same delivery mechanism — the
`handler` cannot tell which path it came through, it only knows which signal number
arrived. The choice of `SIGUSR1` is not arbitrary either: POSIX leaves this pair
(`SIGUSR1`, `SIGUSR2`) specifically to the application, the kernel does not attach a
predefined meaning to them; that is why this measurement can run without risking a
change to the default handler of a signal with a predefined behavior, like `SIGINT`
or `SIGTERM`.

#### The Flag Raised by the Handler

The measurement units stay the same. This lesson's own file, `signal_measurement.c`,
measures three separate questions: does the flag the handler raises reach the main
flow, does the kernel make a blocked signal visible as pending while it is masked,
and how many times is it delivered when the mask is lifted.

```c
// observe.h — a record that classifies whom a change reaches
#ifndef OBSERVE_H
#define OBSERVE_H

enum Class { COPY, SHARED, HANDOFF, UNOBSERVED };

/* Records one change: seen is whether the side that did not make the
   change can read it (1 or 0). */
void observe(const char *change, enum Class cls, int seen);

/* Prints the count per class and the reached / not reached totals. */
void observe_summary(void);

#endif
```

```c
// observe.c — the counts live only in this unit
#include <stdio.h>
#include "observe.h"

static const char *const names[] = { "copy", "shared", "handoff", "unobserved" };
static int counts[4];
static int reached = 0;
static int not_reached = 0;

void observe(const char *change, enum Class cls, int seen) {
    /* shared and handoff reach, copy and unobserved do not */
    int expected = (cls == SHARED || cls == HANDOFF);
    counts[cls]++;
    if (seen) {
        reached++;
    } else {
        not_reached++;
    }
    printf("%-34s %-8s %s%s\n", change, seen ? "saw" : "missed",
           names[cls], seen == expected ? "" : "  MISMATCH");
}

void observe_summary(void) {
    printf("classes: copy %d, shared %d, handoff %d, unobserved %d\n",
           counts[COPY], counts[SHARED], counts[HANDOFF], counts[UNOBSERVED]);
    printf("reached %d, not reached %d\n", reached, not_reached);
}
```

```c
// signal_measurement.c — the flag raised by the handler and the blocked delivery count
#define _POSIX_C_SOURCE 200809L
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include "observe.h"

static volatile sig_atomic_t flag = 0;
static volatile sig_atomic_t delivery_count = 0;

static void handler(int s) {
    (void)s;
    flag = 1;
    delivery_count++;
}

int main(void) {
    struct sigaction sa;
    memset(&sa, 0, sizeof sa);
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGUSR1, &sa, NULL);

    /* first measurement: does the flag raised by the handler reach the main flow */
    raise(SIGUSR1);
    observe("flag raised by the handler", HANDOFF, flag == 1);

    /* second measurement: a signal arriving twice while blocked is delivered once when unblocked */
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGUSR1);
    sigprocmask(SIG_BLOCK, &set, NULL);

    delivery_count = 0;
    raise(SIGUSR1);
    raise(SIGUSR1);
    int while_blocked = (delivery_count == 0);

    /* third measurement: is the signal seen as pending while under the mask */
    sigset_t pending_set;
    sigpending(&pending_set);
    int is_pending = sigismember(&pending_set, SIGUSR1);
    observe("signal appears pending while masked", HANDOFF,
           while_blocked && is_pending == 1);

    sigprocmask(SIG_UNBLOCK, &set, NULL);
    observe("signal delivery count on unblocking", HANDOFF,
           while_blocked && delivery_count == 1);

    observe_summary();
    return 0;
}
```

```text
flag raised by the handler         saw      handoff
signal appears pending while masked saw      handoff
signal delivery count on unblocking saw      handoff
classes: copy 0, shared 0, handoff 3, unobserved 0
reached 3, not reached 0
```

**SI7.** The first measurement is direct: `raise(SIGUSR1)` tells the kernel to stop
the process and run `handler`, `handler` sets `flag` to `1`, and after `raise`
returns the main flow reads this flag and finds `1`. The flag is of type
`volatile sig_atomic_t` — this type choice is not arbitrary, it is the counterpart of
a rule: the compiler must re-read a variable of this type from memory on every read,
and must not leave a read or write half done. The handler cuts in independently of
the main flow, at an unexpected point; a non-`volatile` variable could be kept by the
compiler in a register, and a change the handler makes might never be reflected in
the copy the main flow sees.

#### Held Under the Mask

**SI8.** The second measurement directly observes the pending state. A signal sent
after a call to `sigprocmask(SIG_BLOCK, ...)` is not delivered right away; the kernel
keeps it as **pending**, but unlike the previous measurement, which read this
indirectly (through a counter not incrementing), this can be read **directly** with
`sigpending`: a call to `sigpending(&pending_set)` fills in the set of the calling
process's blocked, not-yet-delivered signals, and `sigismember(&pending_set, SIGUSR1)`
asks whether `SIGUSR1` is in that set. After two `raise` calls, before delivery
happens, this query returns `1` — the kernel has not lost the signal, has not
processed it, it is holding it somewhere. This is not a prose description of the word
"pending," it is an executable measurement: `signal_measurement.c` shows that a state
really exists under the mask, and that this state can be queried by the process.

**SI9.** The third measurement counts what this pending state does **on unblocking**.
Even if the same signal is raised twice while blocked, the pending state is singular
for most standard signals — not a count of copies to deliver, only the information
"is one pending" is kept. When the mask is lifted (`SIG_UNBLOCK`), the kernel
delivers the pending signal, but only **once**: `delivery_count` stays `0` for the
duration of the block, and becomes `1` on unblocking, not `2`. Read together with the
`sigpending` measurement, these two findings give a single picture: while blocked,
the set holds **one bit** of information — "one is pending" or "none is" — and this
bit is independent of how many times `raise` was called. The bit turns into one
delivery on unblocking, not into as many deliveries as the counter's value.

`sigaction` itself uses the same singularity rule while a handler runs. `sa.sa_mask`
was set to the empty set, but the kernel still automatically blocks **the signal
itself** during every handler call — unless the `SA_NODEFER` flag is explicitly
requested, a handler is not reentered with a second copy of the same signal it is
running for. This is a small example of the "held under the mask" that
`signal_measurement.c` measures: the same signal arriving while inside the handler
is not lost, it is held until the handler returns, then delivered once. The
`sa_mask` field exists to widen this automatic blocking — if a handler deals with
more than one signal type, the others also need to be added to this field to prevent
a race between them; this lesson works with a single signal, so it leaves it empty
with `sigemptyset`.

Another use of blocking is to prevent a check-and-act race. If a program is going to
read a flag and act on it, a signal can arrive between the read and the action and
change the flag; this lesson does not test that, but the pattern is briefly this: the
signal in question is blocked with `sigprocmask(SIG_BLOCK, ...)`, the flag is read
and a decision made, then the mask is reopened. That way the "read and decide" step
stays inside a window closed to a signal arriving in between —
`signal_measurement.c`'s `sigpending` measurement rests on the same mechanism that
confirms a signal held pending inside that window is really sitting there, not lost.

#### Undefined Behavior Inside a Handler

**SI10.** The `handler` function is only two lines: raising a flag and incrementing a
counter. This brevity is a rule, not a preference. A signal handler can cut into the
main flow at **any** point — in the middle of a `malloc` call, inside a piece of code
holding a lock, even at the moment `printf` is updating its own internal buffer.
Making a call inside a handler that is not async-signal-safe — `malloc`, `printf`,
any function that accesses a locked resource — can collide with the code it
interrupted; this is **undefined behavior**, and this lesson does not run such a
call, only describes it here. Likewise, sharing a non-`volatile` variable between the
handler and the main flow is also undefined behavior: since the compiler does not
know the variable can be changed by the handler, it may reorder or entirely skip
reads in the main flow. That is why `signal_measurement.c` only touches variables of
type `sig_atomic_t` marked `volatile`, and calls no function at all inside the
handler. This restriction is not just "good practice"; if the interrupted call is
holding a library's internal lock or static buffer right in the middle, and the
handler tries to use that same lock or buffer again, the result can be a deadlock,
corrupted data, or a random crash — neither the compiler nor the runtime reports any
error here, the behavior falls outside the definition entirely.

POSIX defines a small set of functions that are safe to call inside a handler —
direct system calls like `write`, `_exit`, `sigprocmask`, and `sigpending`, which
this lesson uses, are on that list, but buffered I/O (`printf`, `fopen`), memory
allocation (`malloc`, `free`), and most library functions are not. The reason for
this distinction is buffering and internal state: `printf` updates its own internal
buffer and a possible lock, and calling the same function again inside a handler
while that update is left half done can leave that internal state inconsistent.
`signal_measurement.c` only does `flag = 1` and `delivery_count++` — both are direct
memory writes, involving no internal state or lock, so they run safely inside a
handler.

This distinction is a different axis from the previous course's question of "which
thread runs this step": a signal handler is not a separate thread, it is the same
flow being interrupted at an unexpected point. The interrupted code and the handler
temporarily share the same stack and the same register set — that is why the trace a
handler leaves is not a copy like `fork` gives, it is a handoff back into the same
flow.

This sharing narrows further in a multithreaded process: in POSIX a signal handler
is registered at the process level, but which thread runs it depends on delivery — a
signal a thread directs at itself (with `pthread_kill`) runs on that thread, while a
signal directed at the process can run on any unmasked thread. This lesson runs
single-threaded, so this ambiguity does not surface; when the next lesson introduces
multiple threads, the question of which flow runs the signal handler stays as a
boundary that is outside that lesson's scope but worth knowing.

**SI11.** Lesson M08/K04 treated `volatile` in Java inside a multithreaded memory
model, as a keyword giving visibility and ordering guarantees: a contract coordinated
between the runtime and the hardware, ensuring a value one thread writes is seen by
another thread in the correct order. The guarantee `volatile sig_atomic_t` gives here
is much narrower: it does not establish a visibility order among multiple cores or
threads, it only prevents the compiler from keeping that variable in a register or
reordering reads or writes of it, when the **same** flow is interrupted by a signal
handler. The C standard does not define this type for multiprocessor memory
ordering; POSIX guarantees it only for an asynchronous interruption within the same
process. The same keyword in two languages solves two different problems: one
crosses a single-flow interruption boundary, the other crosses a multicore hardware
memory-consistency boundary.

#### The Bounding Measurement

This measurement does not **count** standard signals. For POSIX standard signals the
pending state is a flag, not a counter: no matter how many times the same signal is
sent while blocked, only one delivery happens on unblocking. `signal_measurement.c`
shows this exactly — against two `raise` calls, `delivery_count` only rises to `1`.
Real-time signals (`SIGRTMIN` and above) follow a different rule and can accumulate
in a queue, but this lesson is limited to standard signals and does not take up that
difference.

This boundary also determines what the measurement can confirm:
`signal_measurement.c` counts not how many times `raise` was called during the
blocked period, but only how many times delivery happened after unblocking. Even if
two `raise` calls collapse into a single delivery, this is not a loss — the guarantee
the kernel gives for standard signals is already "at least once," not "once per
call." The lesson does not count anything beyond that guarantee.

This lesson also uses `sigaction`, not the `signal()` function; that is not a style
choice. The old `signal()` interface's behavior differs across systems — on some
older Unix variants the handler reverts to the default after being called, on others
it does not — and this ambiguity is exactly what portable code wants to avoid.
`sigaction` gives explicit control over the handler's persistence, which additional
signals are blocked during the handler (`sa_mask`), and whether an interrupted system
call is restarted (`SA_RESTART`). This lesson does not set `SA_RESTART`; that means a
blocking system call interrupted by a signal could return with an `EINTR` error, but
since `signal_measurement.c` contains no such call, this behavior is not observed in
this lesson — it is only noted here as a boundary to be met later.

#### Summary

- A signal handler runs outside the main flow, at an unexpected point; the trace it
  leaves returns to the main flow within the same process.
- `volatile sig_atomic_t` is the only safe form of sharing between a handler and the
  main flow; it prevents compiler reordering and half-done reads or writes.
- This lesson's class is **handoff**: the flag the handler raises reaches the main
  flow, but it is not shared continuously like a record — it is carried once.
- The pending state can be observed directly with `sigpending`: while masked, the
  information held in the set is a single bit, "is one pending" — not how many times
  it was sent; that is why a blocked signal sent several times is still delivered
  only once when the mask is lifted.
- Making a call inside a handler that is not async-signal-safe, or sharing a
  non-`volatile` variable, is undefined behavior; this lesson runs neither.
- `volatile sig_atomic_t` in C and `volatile` in Java carry the same word but solve
  different problems: one is single-flow interruption safety, the other is
  multithreaded visibility guarantee.

#### Next Step

The next lesson continues the handoff class, but through a different channel: the
bytes a pipe carries, a handoff that flows once from child to parent and ends with
end-of-file on the second read. The common ground is this: the trace carried in a
signal was a single bit or a single counter increment, while the trace carried in a
pipe will be a byte sequence of variable length — both follow the same "once,
irreversible" handoff rule, but the shape and amount of the information they carry
are fundamentally different.

### Inter-Process Communication

Source: https://academia.sh/en/courses/c-systems-programming/inter-process-communication

A pipe carries bytes once, and every byte read drops out of the stream; a mapping set up with mmap and MAP_SHARED shows the same page to two processes at once, and these two fall into two different classes within the same lesson.

The previous lesson measured the trace a signal leaves: a flag, a handoff that
reaches the main flow once. This lesson measures the same class between two
processes, and deliberately places a different class next to it. A **pipe** carries
the bytes the child writes to the parent — every byte read drops out of the stream,
this is **handoff**. An **`mmap` mapping**, on the other hand, shows the same memory
page to two processes at once — when one writes, the other sees it at the same time,
this is **shared**. Placing the two classes side by side in the same lesson makes the
difference between them visible.

#### The Value Written to the Pipe

`observe.h` and `observe.c` do not change. This lesson's own file,
`ipc_measurement.c`, runs four measurements in order: a pipe, a shared mapping, a
two-way channel, and two messages separated by a length prefix.

```c
// observe.h — a record that classifies whom a change reaches
#ifndef OBSERVE_H
#define OBSERVE_H

enum Class { COPY, SHARED, HANDOFF, UNOBSERVED };

/* Records one change: seen is whether the side that did not make the
   change can read it (1 or 0). */
void observe(const char *change, enum Class cls, int seen);

/* Prints the count per class and the reached / not reached totals. */
void observe_summary(void);

#endif
```

```c
// observe.c — the counts live only in this unit
#include <stdio.h>
#include "observe.h"

static const char *const names[] = { "copy", "shared", "handoff", "unobserved" };
static int counts[4];
static int reached = 0;
static int not_reached = 0;

void observe(const char *change, enum Class cls, int seen) {
    /* shared and handoff reach, copy and unobserved do not */
    int expected = (cls == SHARED || cls == HANDOFF);
    counts[cls]++;
    if (seen) {
        reached++;
    } else {
        not_reached++;
    }
    printf("%-34s %-8s %s%s\n", change, seen ? "saw" : "missed",
           names[cls], seen == expected ? "" : "  MISMATCH");
}

void observe_summary(void) {
    printf("classes: copy %d, shared %d, handoff %d, unobserved %d\n",
           counts[COPY], counts[SHARED], counts[HANDOFF], counts[UNOBSERVED]);
    printf("reached %d, not reached %d\n", reached, not_reached);
}
```

```c
// ipc_measurement.c — a pipe, a shared mapping, and a two-way channel
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
#include "observe.h"

/* 1. Pipe: the value the child writes reaches the parent once, the second read gives end-of-file */
static void pipe_measurement(void) {
    int ends[2];
    if (pipe(ends) != 0) {
        return;
    }
    fflush(stdout);
    pid_t p = fork();
    if (p == 0) {
        int value = 42;
        close(ends[0]);
        ssize_t n = write(ends[1], &value, sizeof value);
        close(ends[1]);
        _exit(n == (ssize_t)sizeof value ? 0 : 1);
    }
    close(ends[1]);
    int received = 0;
    ssize_t first = read(ends[0], &received, sizeof received);
    waitpid(p, NULL, 0);
    char remaining;
    ssize_t second = read(ends[0], &remaining, 1);
    close(ends[0]);
    observe("value written to the pipe", HANDOFF,
           first == (ssize_t)sizeof received && received == 42 && second == 0);
}

/* 2. Shared mapping: the parent sees the page the child writes at the same time */
static void mapping_measurement(void) {
    int *shared = mmap(NULL, sizeof *shared, PROT_READ | PROT_WRITE,
                     MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (shared == MAP_FAILED) {
        return;
    }
    *shared = 0;
    fflush(stdout);
    pid_t p = fork();
    if (p == 0) {
        *shared = 99;
        _exit(0);
    }
    waitpid(p, NULL, 0);
    observe("shared value written by the child", SHARED, *shared == 99);
    munmap(shared, sizeof *shared);
}

/* 3. Two-way channel: a value written on one end is read from the other end */
static void channel_measurement(void) {
    int ends[2];
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, ends) != 0) {
        return;
    }
    fflush(stdout);
    pid_t p = fork();
    if (p == 0) {
        close(ends[0]);
        int value = 7;
        write(ends[1], &value, sizeof value);
        int reply = 0;
        read(ends[1], &reply, sizeof reply);
        _exit(reply == 14 ? 0 : 1);
    }
    close(ends[1]);
    int incoming = 0;
    read(ends[0], &incoming, sizeof incoming);
    int reply = incoming * 2;
    write(ends[0], &reply, sizeof reply);
    int status = 0;
    waitpid(p, &status, 0);
    close(ends[0]);
    observe("reply received over the channel", HANDOFF,
           incoming == 7 && WIFEXITED(status) && WEXITSTATUS(status) == 0);
}

/* 4. Length-prefixed message boundary: two messages from a pipe are split apart without mixing */
static ssize_t read_full(int fd, void *buffer, size_t count) {
    size_t received = 0;
    while (received < count) {
        ssize_t n = read(fd, (char *)buffer + received, count - received);
        if (n <= 0) {
            return n;
        }
        received += (size_t)n;
    }
    return (ssize_t)received;
}

static void write_prefixed(int fd, const char *text) {
    unsigned char length = (unsigned char)strlen(text);
    write(fd, &length, 1);
    write(fd, text, length);
}

static void prefix_measurement(void) {
    int ends[2];
    if (pipe(ends) != 0) {
        return;
    }
    fflush(stdout);
    pid_t p = fork();
    if (p == 0) {
        close(ends[0]);
        write_prefixed(ends[1], "first");
        write_prefixed(ends[1], "secondmessage");
        close(ends[1]);
        _exit(0);
    }
    close(ends[1]);

    unsigned char length1 = 0;
    read_full(ends[0], &length1, 1);
    char first[32];
    read_full(ends[0], first, length1);
    first[length1] = '\0';

    unsigned char length2 = 0;
    read_full(ends[0], &length2, 1);
    char second[32];
    read_full(ends[0], second, length2);
    second[length2] = '\0';

    waitpid(p, NULL, 0);
    close(ends[0]);
    observe("two prefixed messages split correctly", HANDOFF,
           strcmp(first, "first") == 0 && strcmp(second, "secondmessage") == 0);
}

int main(void) {
    pipe_measurement();
    mapping_measurement();
    channel_measurement();
    prefix_measurement();
    observe_summary();
    return 0;
}
```

```text
value written to the pipe          saw      handoff
shared value written by the child  saw      shared
reply received over the channel    saw      handoff
two prefixed messages split correctly saw      handoff
classes: copy 0, shared 1, handoff 3, unobserved 0
reached 4, not reached 0
```

**SI12.** All four lines say `saw`, but the second line's class differs from the
others. The child process writes `42` to the pipe, the parent reads it with `read` —
this is a **handoff**: the byte drops out of the stream, the second `read` call
returns `0` because there is nothing left to read, that is, end-of-file. This `0`
return is not an error, it is a **status**: `read`'s return value is normally the
number of bytes read, negative means an error, zero means "the write end has closed
and there is no more data left to read in the buffer." `pipe_measurement` calls
`close(ends[1])` on the child side, then exits with `_exit` — once the last copy of
the write end closes too, the kernel carries the information "no new data will
arrive" to the read end through exactly this `0` return. Had the child not closed the
write end, the second `read` would have blocked until data arrived — a `0` return is
not a sign of absent data, it is a sign that **all writers have detached**.

But `mapping_measurement` works differently: the call
`mmap(..., MAP_SHARED | MAP_ANONYMOUS, ...)` allocates a memory page before `fork`
and marks that page as **shared**. When `fork` copies process memory it does not copy
this particular mapping — the parent and the child end up with two separate pointers
pointing at the same physical page. When the child writes `*shared = 99`, the parent,
looking at its own `*shared` after `waitpid`, also sees `99` — because both are
looking at the same page. This is the definition of the **shared** class: it is not
carried once like a record, it is shared continuously. There is no counterpart here
to the concept of "end-of-file": there is no such thing as a memory page "running
out," the page just sits there until one of the processes calls `munmap` or the
process ends.

#### Pipe or Shared Memory

**SI13.** The visibility of these two mechanisms can be described with the same
words — "the parent sees what the child writes" — but the underlying mechanism is
completely different. A pipe is a buffer the kernel holds; writing and reading are
system calls, and data read is erased from the buffer. The two processes never look
at the same memory, the kernel is the carrier in between. A mapping set up with
`MAP_SHARED`, on the other hand, really does point at the same physical page; reading
and writing are ordinary memory access, no system call is needed, and data never
"runs out" — the page stays there every time it is read. That is why the pipe's
second read gives end-of-file, while the shared page's second read still gives the
same value; `mapping_measurement` does not try this because the difference is
already visible on the first read.

The practical consequence of this contrast is which mechanism to choose and when. A
pipe carries an ordered, one-way stream, synchronized by the kernel, so it gives a
natural answer to the question "who sent what and when" — the reading side blocks
until data arrives, it never enters a race. `MAP_SHARED`, on the other hand, does
**not** give synchronization: if two processes write to the same page at the same
time, the result depends on which write appears first, and this lesson does not set
up such a collision, it only uses a one-way write-then-read order. Shared memory
gives visibility, not ordering — locking is a separate responsibility, taken up
separately for threads in this topic's final lesson.

`channel_measurement` shows a two-way channel set up with `socketpair` once. This
channel is also a carrier that flows through the kernel and runs out, like a pipe —
the difference is that it is two-way, not one-way: the same two ends can both be
written to and read from. This lesson does not open a network socket here; `AF_UNIX`
is a local endpoint, no address or port is ever printed. The child sends a value, the
parent multiplies it by two and sends it back, the child reads the reply and writes
it into its exit code — this too is **handoff**, because every value flowing over the
channel is read once and consumed. The difference between `pipe` and `socketpair` is
the number of directions, not the class: `pipe` gives a one-way pair of file
descriptors, `socketpair` gives a fully two-way channel where both descriptors can be
both read from and written to — both are subject to the same kernel buffering rule,
the same running-out behavior.

This dependency has a concrete consequence: even though `socketpair`'s two ends look
like two independent descriptors, neither end sees end-of-file until the write side
of both descriptors is closed — just as with `pipe`. The only difference is who can
write to whom: with `pipe` one end can only write, the other can only read; with
`socketpair` both ends can be used in both directions. That is why
`channel_measurement` has to be careful about the order in which the child closes the
channel — if it did not close its own descriptor after sending the reply, the read on
the parent side could wait forever, because the kernel would still see an open write
end and would not produce end-of-file. Calling `close(ends[1])` after the
`write_prefixed` calls in `prefix_measurement` is another instance of the same rule:
the parent side does not keep waiting after reading the two messages, because the
child explicitly closes its own write end.

The choice among these three mechanisms depends on the shape of the data flow. For a
one-way, ordered stream, `pipe` is the simplest tool; if both sending and receiving
are needed from the same two ends, `socketpair` avoids opening an extra channel; if
the copying cost needs to be eliminated entirely for large or frequently shared data,
`MAP_SHARED` comes into play, but then the responsibility for synchronization falls
on the designer.

#### The Bounding Measurement

**SI14.** A pipe is a **stream**, it carries no message boundary. In the previous
three measurements every `write` call was met with a single `read`, but this
guarantee is not general: two separate `write` calls can arrive together in a single
`read`, or the data of one `write` can be split across multiple `read` calls. A pipe
keeps the bytes sent in a queue and has no idea where one message ends and another
begins — there is nothing carrying that information.

**SI15.** `prefix_measurement` really sets up this boundary and really tests it. The
child process writes two messages to the same pipe back to back — `"first"` and
`"secondmessage"` — but not as raw text; `write_prefixed` puts a one-byte length
field in front of each message. The parent side reads these two messages not with a
single `read` call, but with the `read_full` helper function: first the length byte,
then exactly that many data bytes. `read_full` itself also compensates for a
guarantee — a single `read` call may not bring back all the bytes requested, so the
function keeps reading in a loop until it reaches the requested count. The bytes of
the two messages flow one after another on the same pipe, with no separator between
them; the only thing that cuts the reading side at the right place is the length
information each message carries with it. The measurement confirms that `"first"`
and `"secondmessage"` are reconstructed correctly, without mixing — this lesson did
not draw its own message boundary on its own before, because it showed the earlier
three values in a single read each, but `prefix_measurement` draws one now: what
gets printed is only the reconstructed messages, not the raw bytes off the pipe.

`write_prefixed` carries the length as an `unsigned char`, that is, a one-byte field
— it can describe a message of at most 255 bytes. That is enough for this
measurement, because `"secondmessage"` is well under that limit; but in a real
protocol, if message size is unpredictable, one byte is not enough, a two- or
four-byte field is needed, and then the question of byte order (`endianness`) comes
in — do the sending and receiving machines write and read in the same order. This
lesson does not open that question, because the pipe runs both ends on the same
machine; in a protocol talking over a network, the order of the length field must be
explicitly fixed, otherwise a number read correctly on one end comes out wrong on the
other. The rule that matters does not change no matter how large the length field is:
the mechanism carrying it is the same — first read how much data is coming, then read
exactly that much.

`read_full`'s loop is not cosmetic, it is a direct consequence of pipe semantics. The
kernel does not guarantee delivering the bytes of a `write` call as a single unit; when
the reading side calls `read`, the kernel returns however many bytes happen to have
accumulated in the pipe buffer at that moment, which can be fewer than requested.
This never showed up in the `pipe_measurement` and `mapping_measurement`
measurements, because the values written were only a few bytes long and a single
`read` call always sufficed; but that is not the general rule. `read_full` closes
this gap in one place, once, by continuing to read until it reaches the requested
count — the calling side no longer has to deal with the question "how many `read`
calls did it take," `read_full` itself makes short reads invisible. It separately
checks the moment `n <= 0` is returned, because `0` indicates the pipe has closed and
a negative value indicates an error; both mean "a full count of bytes was expected
but did not arrive," and in that case the loop exits immediately instead of spinning
forever.

#### Summary

- This lesson places two different classes side by side: a pipe is **handoff**, a
  `MAP_SHARED` mapping is **shared** — even though both can be described with the
  sentence "the parent sees what the child writes," their mechanisms are separate.
- A pipe is a carrier that flows through the kernel and runs out as it is read; the
  second read returns `0` bytes because all write ends have closed — this is not an
  absence of data, it is a sign that the writers have detached.
- A mapping set up with `MAP_SHARED | MAP_ANONYMOUS` shows the same physical page to
  two processes at once; when one side writes, the other side reads the same page,
  data never runs out — but it gives visibility only, not ordering; locking is a
  separate responsibility.
- `socketpair` sets up a two-way channel; this too is a handoff, because every value
  that flows is read once and consumed. No network socket is opened, no address or
  port is printed.
- A pipe carries no message boundary; `prefix_measurement` really sets one up with a
  length prefix and shows it by reconstructing two messages without mixing them.

#### Next Step

The next lesson — the last in this topic — stays within the same process but moves
from processes to threads: a counter written under a lock, a **shared** value that
reaches the main thread after a `join`.

### POSIX Threads

Source: https://academia.sh/en/courses/c-systems-programming/posix-threads

Two threads in the same process changing the same memory under a lock is not a handoff, consumed once like the bytes a pipe carries, but a continuously shared exchange.

The last lesson on system interfaces stays within the same process, but gives up the
form of carrying that crosses a process boundary and moves to two streams that share
the same memory space: the **thread**. `fork` gave a copy, a signal left a handoff,
a pipe and `mmap` crossed the inter-process boundary in two different ways. Two
threads, on the other hand, never cross a boundary at all — they share the same
process's same memory from the start. A change made under a lock therefore falls
into the course's fourth class: **shared**.

`pthread_create` also differs from `fork` in how it sets this up. The moment `fork`
is called, the kernel copies the memory's page table — thanks to copy-on-write the
physical pages are not duplicated right away, but the two processes now have separate
address spaces, and when one writes the other does not see it. `pthread_create`,
however, does not open a new address space; it only allocates a new stack and starts
a second point of execution inside the same process. The code segment, the data
segment, the heap — all a single copy, both streams see the same addresses. That is
why a global variable like `counter` is the same memory location for both threads;
compared with the two independent copies of a same-named variable after `fork`, this
difference is what directly determines this lesson's class.

#### The Counter Written Under a Lock

`observe.h` and `observe.c` do not change. This lesson's own file,
`thread_measurement.c`, runs a single measurement in which two threads increment the
same counter under a lock and the result reaches exactly the expected value.

```c
// observe.h — a record that classifies whom a change reaches
#ifndef OBSERVE_H
#define OBSERVE_H

enum Class { COPY, SHARED, HANDOFF, UNOBSERVED };

/* Records one change: seen is whether the side that did not make the
   change can read it (1 or 0). */
void observe(const char *change, enum Class cls, int seen);

/* Prints the count per class and the reached / not reached totals. */
void observe_summary(void);

#endif
```

```c
// observe.c — the counts live only in this unit
#include <stdio.h>
#include "observe.h"

static const char *const names[] = { "copy", "shared", "handoff", "unobserved" };
static int counts[4];
static int reached = 0;
static int not_reached = 0;

void observe(const char *change, enum Class cls, int seen) {
    /* shared and handoff reach, copy and unobserved do not */
    int expected = (cls == SHARED || cls == HANDOFF);
    counts[cls]++;
    if (seen) {
        reached++;
    } else {
        not_reached++;
    }
    printf("%-34s %-8s %s%s\n", change, seen ? "saw" : "missed",
           names[cls], seen == expected ? "" : "  MISMATCH");
}

void observe_summary(void) {
    printf("classes: copy %d, shared %d, handoff %d, unobserved %d\n",
           counts[COPY], counts[SHARED], counts[HANDOFF], counts[UNOBSERVED]);
    printf("reached %d, not reached %d\n", reached, not_reached);
}
```

```c
// thread_measurement.c — two threads incrementing the same counter under a lock
#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <stdio.h>
#include "observe.h"

#define STEPS 1000

static long counter = 0;
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

static void *increment(void *data) {
    (void)data;
    for (int i = 0; i < STEPS; i++) {
        pthread_mutex_lock(&lock);
        counter = counter + 1;
        pthread_mutex_unlock(&lock);
    }
    return NULL;
}

int main(void) {
    pthread_t t1, t2;

    pthread_create(&t1, NULL, increment, NULL);
    pthread_create(&t2, NULL, increment, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    observe("total written by both threads under the lock", SHARED,
           counter == 2L * STEPS);

    pthread_mutex_destroy(&lock);
    observe_summary();
    return 0;
}
```

```text
total written by both threads under the lock saw      shared
classes: copy 0, shared 1, handoff 0, unobserved 0
reached 1, not reached 0
```

**SI16.** The single line says `saw`, and this result is not a matter of chance, it
is guaranteed. Both threads run the `increment` function, each incrementing
`counter` a thousand times; the main thread looks at `counter` after both
`pthread_join` calls return and finds exactly `2000`. This is fundamentally
different from the independent copies `fork` gives: `pthread_create` does not open a
new memory copy, it starts a new flow of execution in the same address space of the
same process. The `counter` variable sits in a single place, and both streams write
to that one place — that is why the class is **shared**, not the **copy** of `fork`.
Because the `STEPS` constant was chosen to be a thousand, the total is also exact:
the two threads together perform `2 * 1000 = 2000` increments, and if the lock never
loses one, the result can only be that number — a measurement that occasionally comes
out `1998` or `2001` is not a result this lesson accepts, because the course's
measurement method only verifies deterministic results that come out the same every
time.

The line `counter = counter + 1` may look like a single C expression, but it
corresponds to three separate steps: go to memory, read the value, add one, write
the result back to memory. Without a lock these three steps are open to
interruption — while one thread is between reading and writing, the other thread
can read the same old value, both write back their own computed `+1`, and one of the
two increments is lost. `pthread_mutex_lock` turns these three steps into an
indivisible unit: the thread holding the lock finishes all three before the other
thread can acquire the lock, so no increment is ever lost. The size of `STEPS` is
also deliberate — with a small enough loop count even a lockless version might give
the correct result by chance, because the two threads' steps could proceed in
sequence without colliding; a thousand-step loop reduces that chance to practically
zero, though the course text describes this without ever running it.

Java's standard collections show a different face of the same problem: if a
collection is modified by another thread while iterating over an `ArrayList` or a
`HashMap`, most implementations throw a `ConcurrentModificationException` instead of
silently corrupting the data — this "fail-fast" behavior is the scope of M08/K03's
"Iterators and Concurrent Modification" topic. The `counter` in
`thread_measurement.c`, however, is a single `long`, not a structure being iterated
over; the lock keeps it intact, but C itself has no runtime check comparable to
Java's — code that forgets the lock and writes directly to `counter` compiles without
error and silently produces the wrong result, without giving a warning like
`ConcurrentModificationException`. The check here is not in the compiler or at
runtime, it is entirely in the programmer's discipline.

#### What the Lock Guarantees: Visibility, Not Ordering

**SI17.** The `pthread_mutex_lock`/`pthread_mutex_unlock` pair wraps the line
`counter = counter + 1`, and this wrapping guarantees two separate things at once.
The first is **mutual exclusion**: two threads can never hold the lock at the same
time, so the step of reading `counter`, adding one, and writing it back is never
interrupted midway — one thread cannot start until the other has completed all three
steps. The second is **visibility**: when a thread releases the lock, the change it
made is guaranteed to be visible to the next thread that acquires the lock. Without a
lock both guarantees are lost — two threads could read the same `counter` at the same
time and add one to the same old value, and an increment could be lost.
`thread_measurement.c` never runs this lost-update case; a lockless version is
described but not executed, because its result would not be deterministic, and this
course's measurement method only verifies deterministic results.

While the lock guarantees visibility, it does not guarantee **which thread runs
first**. No matter in what order `t1` and `t2` happen to be scheduled, which one
acquires the lock more times first is never measured or printed inside
`thread_measurement.c` — only the final total is measured. This also fits the rule
the course set from the start: a thread's identity is never printed, and neither
stream prints anything among themselves, only the main thread prints a single line
after both `join` calls return.

In the Java Memory Model (JMM) a similar visibility guarantee is built with the
`volatile` keyword and synchronized blocks — M08/K04's "JVM, Concurrency, and
Performance" topic defines this through the "happens-before" relation: all writes a
thread makes before releasing a lock become visible to the next thread that acquires
that lock. The rule matches, word for word, the guarantee the
`pthread_mutex_unlock`/`pthread_mutex_lock` pair gives; the difference is that the
JMM defines this **in the language's own specification**, through the virtual
machine's memory ordering, whereas in C this guarantee comes from the POSIX thread
specification's calling contract — the `pthread_mutex_lock`/`unlock` calls
themselves constrain compiler and hardware ordering, the language has no separate
concept of a memory model of its own.

Go's concurrency model solves the same problem from the other direction. The
principle in M09/K03's "Goroutines and Channels" topic — "share memory by
communicating, don't communicate by sharing memory" — defines passing a value
through a channel as transferring that value's ownership from the sender to the
receiver; two goroutines never write to the same variable at the same time, a value
flows once over a channel. The `counter` in `thread_measurement.c` is the exact
opposite: ownership is never transferred, both threads have the right to access the
same location at the same time, and that right is bounded with a `pthread_mutex_t`.
Had the channel pattern been chosen in Go, instead of sharing `counter` each
goroutine would compute its own partial sum and send it over a channel, and the
summing step would happen at a single receiver — this would not be the **shared**
class this course measures, it would be a **handoff** pattern similar to what the
pipe carries.

#### The Bounding Measurement

**SI18.** This measurement verifies only the **final value**, not the intermediate
state. Which intermediate values `counter` passes through while under the lock —
whether `1`, `2`, `3` in order, or however the two threads' steps happen to
interleave — is never measured anywhere, because measuring it would itself require an
extra locked read, and that would change the system being measured. The only thing
`thread_measurement.c` verifies is that none of the `2000` steps was lost; the
question of which step came from which thread is outside this lesson's scope. This
bound is deliberate: the course measures whether two streams see each other, it does
not reconstruct the race between them. A measurement that wanted to observe a race
condition itself — for example counting which thread acquired the lock in which
position — would become dependent on timing and would break this course's
deterministic-result rule; that is why no such measurement appears here.

This lesson's biggest limitation is that it **assumes the lock is used correctly**.
The `lock` variable is acquired before every access inside `increment`, but nothing
enforces that — if a third function were added that read `counter` without the lock,
the compiler would give no warning, the code would silently become wrong. Rust's
concurrency model moves this gap into the type system: M10/K04's "`Send` and `Sync`"
topic marks, at compile time, whether a type can safely be moved between threads
(`Send`) and whether it can safely be used as a shared reference (`Sync`). A shared
counter in Rust is not held as a bare variable but inside a wrapper like
`Mutex<i64>`, and code that tries to access that wrapper without acquiring the lock
**does not compile** — it moves an error like the one in `thread_measurement.c` from
runtime to compile time. C has no such check: even though `pthread_mutex_t lock`
sits in the same file as `counter`, the bond between them is only a rule described in
this lesson's text, not a constraint the compiler knows about. This course's
measurement method does not close that gap either — it only verifies a single run in
which the lock is used correctly, it never runs and shows a version where the lock is
forgotten.

#### Next Step and a Combined View of the Classes

**SI19.** Across these five lessons the same four classes appeared with different
carriers. A variable after `fork` was a **copy** — the two sides independent, no
change ever reached the other side. The flag left by a signal handler was a
**handoff** — it reached the main flow once. The byte a pipe carried was also a
**handoff**, the second read ended in end-of-file. A mapping set up with `MAP_SHARED`
and this lesson's locked counter were **shared** — a record continuously shared,
when one side changes it the other side sees it too. The fourth class,
**unobserved**, stayed within the first lesson's own scope: cases where the
observation itself goes unseen.

It was also possible to produce the same result with a different mechanism — for
example incrementing `counter` with an atomic operation instead of a lock, or using
some other synchronization tool over shared memory instead of a thread. This lesson's
limit is that it only measures the most common pattern built with
`pthread_mutex_t`; atomic operations and other lock kinds fall outside this course's
scope.

These four classes answer a question independent of the carrier itself: can a side
other than the one that made a change see that change, and if so, when, how many
times? `fork` answers no — a copy does not show it. A signal and a pipe answer yes,
but only once — a handoff is consumed. `MAP_SHARED` and the locked counter answer yes
and continuously — a shared value is not consumed. The first lesson's observation
mechanism itself is a fourth, rotten answer to this question: one side sees
something but the measurement never catches it — unobserved. The `observe()` function
used across all five lessons tagged these four answers with a single word each and
checked on its own whether that tag was consistent with the measured `saw`/`missed`
value; no lesson skipped this check, and no measurement ended with a `MISMATCH` tag.

Because this classification looks at the **visibility pattern**, not the carrier, it
can also be applied to concurrency tools in other languages — the JMM in M08/K04,
the channel in M09/K03, `Send`/`Sync` in M10/K04 all answer the same four questions
in their own syntax, only where the guarantee is built (runtime, type system,
language specification) changes. C's POSIX thread and process interface always
builds this guarantee into the calling contract: the `fork` call itself defines the
copy class, the `pthread_mutex_lock`/`unlock` pair the shared class, signal delivery
and pipe reads the handoff class. System interfaces close here: five lessons
answered, with four classes, the question of who outside a process or a thread can
see a change.

#### Summary

- Two threads share the same memory space within the same process; unlike `fork`, no
  separate copy is opened.
- This lesson's class is **shared**: a change made under a lock is visible to both
  sides, continuously shared like a record.
- `pthread_mutex_lock`/`unlock` give two guarantees: mutual exclusion and
  visibility. They do not guarantee which thread runs first.
- The measurement verifies only the final total; it does not measure the
  intermediate state or the ordering between threads, because measuring that would
  itself become dependent on timing.
- A lockless version is only described in prose, never run; because its result is
  not deterministic, it does not fit this course's measurement method.
- The same visibility question is solved in different places in other languages: in
  the JMM's `happens-before` relation in Java, in channel ownership transfer in Go,
  in the `Send`/`Sync` types in Rust — in C only in the `pthread_mutex_t` calling
  contract, with no compiler support.

#### Next Step

System interfaces are complete here. Across five lessons the same `observe()` unit
measured five different carriers — copied memory, the flag left by a handler, a
pipe, a shared page, a locked counter — with the same four classes; none replaced
another, because each answered a different form of the same question, "who sees what
and when." The next lesson (Linked Structures) carries the same observation unit out
of the process and thread boundary and into a single data structure: when two
pointers point at the same node, does one see the change the other makes?

## Data Structures and Idioms

Linked structures and aliasing, ring buffers and queues, hash tables, object-like design with function tables, and idioms for cleaning up operating system resources from a single exit point.

### Linked Structures

Source: https://academia.sh/en/courses/c-systems-programming/linked-structures

A C implementation of a linked list and a binary search tree; measuring how changing a node through an alias and a shallow copy produce shared visibility, while a deep copy cuts it.

The previous lesson measured who releases a block of memory. This course starts with a
different axis: who sees a change when you modify a node. When two different pointers
hold the same block of memory, a change made through one is visible through the other —
the source does not say so, because both variables have the identical declaration
(`struct Node *`). The linked list is the clearest example of this sharing: the field
that leads to the next node carries a pointer, and whoever holds that pointer sees the
same node.

This lesson builds the linked list and the binary search tree from scratch and compares
two of the four classes, shared and copy, on the same structure: accessing and changing
a node through an alias produces shared visibility (shared); duplicating the entire list
with new nodes cuts that sharing (copy). The previous course's (**Memory and Pointers in
C**) question of "who releases this node" still applies here — releasing the tree still
honors that contract — but the axis this lesson measures is different: not ownership,
visibility.

#### Linked List: Insertion, Traversal, Release

The list is singly linked, and every insertion happens at the head. `list_total` walks
every node and sums their values; `list_free` saves the next node before releasing each
one. `list_shallow_copy` does not allocate a new node — it returns the incoming head as
is, so the list it returns shares the same nodes as the original. `list_deep_copy`
instead allocates a new node for each one and copies its value; the list it returns owns
an independent chain of memory blocks.

```c
// node.h — singly linked list and basic operations
#ifndef NODE_H
#define NODE_H

struct Node {
    int value;
    struct Node *next;
};

struct Node *list_insert(struct Node *head, int value);
int list_total(const struct Node *head);
void list_free(struct Node *head);
struct Node *list_shallow_copy(struct Node *head);
struct Node *list_deep_copy(const struct Node *head);

#endif
```

```c
// node.c — implementation of the list operations
#include <stdlib.h>
#include "node.h"

struct Node *list_insert(struct Node *head, int value) {
    struct Node *node = malloc(sizeof *node);
    if (node == NULL) {
        return head;
    }
    node->value = value;
    node->next = head;
    return node;
}

int list_total(const struct Node *head) {
    int total = 0;
    for (const struct Node *n = head; n != NULL; n = n->next) {
        total += n->value;
    }
    return total;
}

void list_free(struct Node *head) {
    while (head != NULL) {
        struct Node *next = head->next;
        free(head);
        head = next;
    }
}

struct Node *list_shallow_copy(struct Node *head) {
    return head;
}

struct Node *list_deep_copy(const struct Node *head) {
    struct Node *new_head = NULL;
    struct Node *tail = NULL;
    for (const struct Node *n = head; n != NULL; n = n->next) {
        struct Node *node = malloc(sizeof *node);
        if (node == NULL) {
            list_free(new_head);
            return NULL;
        }
        node->value = n->value;
        node->next = NULL;
        if (tail == NULL) {
            new_head = node;
        } else {
            tail->next = node;
        }
        tail = node;
    }
    return new_head;
}
```

#### A Node Changed Through an Alias

The list is built with the values 1, 2, 3. The pointer `second` points at the second
node — not a new node, but one of the list's own nodes. A change made through `second`
is reflected in the list's total, because both point at the same block of memory.

```c
// alias.c — accessing the list's own node through a second pointer
#include <stdio.h>
#include "node.h"

int main(void) {
    struct Node *head = NULL;
    head = list_insert(head, 3);
    head = list_insert(head, 2);
    head = list_insert(head, 1);

    printf("%s %d\n", "total before changing through the alias", list_total(head));

    struct Node *second = head->next;
    second->value = 20;

    printf("%s %d\n", "total after changing through the alias", list_total(head));
    printf("%s %d\n", "is second the same node as head->next", (int)(second == head->next));

    list_free(head);
    return 0;
}
```

```
total before changing through the alias 6
total after changing through the alias 24
is second the same node as head->next 1
```

**DS1.** The `second` variable is of type `struct Node *`, and so is `head` — nothing in
the source says anywhere whether `second` points at the list's own node or an
independent copy. What decides this is not the declaration but the assignment itself:
`second = head->next` is not a copy of a value but a copy of a **pointer**, meaning
`second` and `head->next` carry the same address. The line `second->value = 20`
therefore also changes the total read through `head`: from 6 to 24. The last line
confirms this without printing an address — the comparison `second == head->next` gives
**1**, showing that the two pointers point at the same node. The class is **shared**: a
second reader (here, the call to `list_total` through `head`) sees the change, because
there is a single shared object between them, not two copied objects.

#### Shallow Copy, Deep Copy

`list_shallow_copy` does not allocate even a single node — it returns the head it
receives as is. Because of this, the pointer it returns and the original list point at
the same nodes, and a change made through one is visible through the other.
`list_deep_copy` allocates a new node for each one; the list it returns carries the
values as they were at the moment of copying, but afterward shares no node with the
original.

```c
// copy_comparison.c — shallow copy shares, deep copy cuts the sharing
#include <stdio.h>
#include "node.h"

int main(void) {
    struct Node *head = NULL;
    head = list_insert(head, 3);
    head = list_insert(head, 2);
    head = list_insert(head, 1);

    struct Node *shallow = list_shallow_copy(head);
    head->next->value = 200;
    printf("%s %d\n", "second value seen by the shallow copy", shallow->next->value);

    struct Node *deep = list_deep_copy(head);
    head->next->value = 2000;
    printf("%s %d\n", "second value seen by the deep copy", deep->next->value);

    list_free(deep);
    list_free(head);
    return 0;
}
```

```
second value seen by the shallow copy 200
second value seen by the deep copy 200
```

**DS2.** Both lines print the same number (200), but for different reasons, and that
difference is what this lesson measures. On the first line, `shallow` points at the same
nodes as `head`; the assignment `head->next->value = 200` also changes
`shallow->next->value`, because both look at a single object — the class is **shared**.
On the second line, `deep` was built by allocating new nodes at the moment of copying
(while the second node still carried the value 200); the assignment
`head->next->value = 2000`, made **after** the copy, does not affect `deep`, because
`deep`'s nodes are independent memory blocks from `head`'s. `deep->next->value` still
carries the 200 from the moment of copying, not 2000 — the class is **copy**: the change
never reaches a second reader, because there is no shared object between them. Because
the pointer returned by `list_shallow_copy` is exactly identical to `head`, this lesson
releases only `deep` and `head` separately; releasing `shallow` as well would release the
same block twice.

#### Binary Search Tree: Insertion, Traversal, Release

The linked list's single-next-pointer sharing extends to two pointers (`left`, `right`)
in the binary tree, but the principle is the same: when a node is reached through more
than one path, the source does not show which path was used. `tree_insert` is
recursive, `tree_total` walks in order (inorder) and sums the values, `tree_free`
releases the subtrees before releasing the root — the list's "who releases it" contract
still applies here; this lesson focuses not on that contract but on the tree's
visibility.

```c
// tree.h — basic binary search tree operations
#ifndef TREE_H
#define TREE_H

struct TreeNode {
    int value;
    struct TreeNode *left;
    struct TreeNode *right;
};

struct TreeNode *tree_insert(struct TreeNode *root, int value);
int tree_total(const struct TreeNode *root);
void tree_free(struct TreeNode *root);

#endif
```

```c
// tree.c — insertion, inorder traversal, and release
#include <stdlib.h>
#include "tree.h"

struct TreeNode *tree_insert(struct TreeNode *root, int value) {
    if (root == NULL) {
        struct TreeNode *node = malloc(sizeof *node);
        if (node == NULL) {
            return NULL;
        }
        node->value = value;
        node->left = NULL;
        node->right = NULL;
        return node;
    }
    if (value < root->value) {
        root->left = tree_insert(root->left, value);
    } else {
        root->right = tree_insert(root->right, value);
    }
    return root;
}

int tree_total(const struct TreeNode *root) {
    if (root == NULL) {
        return 0;
    }
    return tree_total(root->left) + root->value + tree_total(root->right);
}

void tree_free(struct TreeNode *root) {
    if (root == NULL) {
        return;
    }
    tree_free(root->left);
    tree_free(root->right);
    free(root);
}
```

```c
// tree_alias.c — accessing the tree's own node through a second pointer
#include <stdio.h>
#include "tree.h"

int main(void) {
    struct TreeNode *root = NULL;
    int values[] = {5, 3, 8, 1, 4};
    for (size_t i = 0; i < sizeof values / sizeof values[0]; i++) {
        root = tree_insert(root, values[i]);
    }
    printf("%s %d\n", "total of the tree's values before", tree_total(root));

    struct TreeNode *left_child = root->left;
    left_child->value = 30;
    printf("%s %d\n", "total after changing through left_child", tree_total(root));

    tree_free(root);
    return 0;
}
```

```
total of the tree's values before 21
total after changing through left_child 48
```

**DS3.** `left_child` points at the same node as `root->left` — it did not add a new
branch to the tree; we reached an existing node through a second path (through its own
variable instead of the root's `left` field). The assignment `left_child->value = 30`
raises this node's value from 3 to 30, and `tree_total(root)` sees it: from 21 to 48.
The same principle as the second example in the linked list — the class is again
**shared**. The contract we honor when releasing the tree (`tree_free`) is a separate
question: who releases each node, the axis of the previous course (**Memory and Pointers
in C**). This lesson does not reopen that question; it only measures whether a
change is visible through a second pointer.

#### Partial Shallow Copy Through Struct Assignment

`list_shallow_copy` and `list_deep_copy` were two extreme examples: one copied nothing
at all, the other copied every node. But C has a third, more insidious way of copying a
struct: **struct assignment** (`=`). When you write `struct Node copy = *head;`, the
compiler copies every field of the struct that `head` points at into a new variable —
but if one of "every field" is itself a pointer, that pointer is also copied as a value,
not the place it points at.

```c
// struct_assignment.c — a partial shallow copy through variable-type assignment
#include <stdio.h>
#include "node.h"

int main(void) {
    struct Node *head = NULL;
    head = list_insert(head, 3);
    head = list_insert(head, 2);
    head = list_insert(head, 1);

    struct Node copy = *head;
    printf("%s %d\n", "copy's value field right after the assignment", copy.value);

    head->value = 100;
    printf("%s %d\n", "copy.value after head->value changes", copy.value);

    head->next->value = 200;
    printf("%s %d\n", "copy.next->value after changing through the second node", copy.next->value);

    list_free(head);
    return 0;
}
```

```
copy's value field right after the assignment 1
copy.value after head->value changes 1
copy.next->value after changing through the second node 200
```

**DS14.** The line `struct Node copy = *head;` does two different things, both inside
the same assignment: the `value` field is an `int`, and the assignment copies it **by
value** — this is why, after `head->value = 100` runs, `copy.value` still holds its
first value, 1. But the `next` field is a `struct Node *`, and the assignment copies it
by value too — the value copied is an *address*, meaning `copy.next` and `head->next`
carry the same address after the assignment. When `head->next->value = 200` runs, this
change is also visible through `copy.next->value`: 200. Within a single assignment
line, the `value` field produces the **copy** class while the node that `next` points at
produces the **shared** class at the same time — the difference between a "shallow
copy" and a "deep copy" is sometimes not a matter of which function you chose, but of
which fields of a struct carry pointers. `list_shallow_copy`'s returned pointer was
shared in its entirety; here only the node that `next` points at is shared, the `value`
field itself is not — two separate classes within the same struct assignment.

#### The Bounding Measurement

A pointer's sharing is invisible in the source. When two list heads or two tree
branches point at the same node, the type of the structure does not say so — the
declaration `struct Node *` can point at either the list's own node or an independent
copy, and there is no distinction between them at the source level. In this lesson we
compared pointers in both cases without printing an address: the equality
`second == head->next` told us whether the two variables looked at the same block of
memory or at different blocks. The difference between `list_shallow_copy` and
`list_deep_copy` also shows up not in a signature in the source but in what their
bodies do: one performs no allocation at all, the other allocates a new node for every
node. It is not possible to tell whether a function shares by looking at its return
type; this can only be understood by reading the body or by measuring.

#### Summary

- The linked list and the binary search tree were built on the same principle: more
  than one pointer can reach a node at the same time, and the source does not say so.
- Accessing and changing a node through an alias produced the **shared** class in both
  the list and the tree: a second reader (`list_total`, `tree_total`) saw the change.
- `list_shallow_copy` stayed **shared** because it returns the head without allocating
  a node; `list_deep_copy` produced the **copy** class because it allocates a new node
  for every node.
- A partial copy made through struct assignment (`=`) can produce the **copy** class
  in a struct's scalar fields and the **shared** class in what its pointer fields point
  at, at the same time.
- Whether two pointers point at the same node can be measured only through equality
  (`==`), without printing an address.
- Who releases a struct (K02's question) and who sees a change (this course's
  question) are separate axes; this lesson measures the second one.

#### Next Step

In this lesson we always held a pointer to reach a node, and that pointer was
permanent. The next lesson, Ring Buffers and Queues, looks at a structure where what
is shared is not the pointer itself but a position (an index): a producer and a
consumer use the same array, but one writes while the other reads, and a written value
can be overwritten before it is read. The class there is not shared but handoff: the
value reaches the reader exactly once, and can then disappear.

### Ring Buffers and Queues

Source: https://academia.sh/en/courses/c-systems-programming/ring-buffers-and-queues

A fixed-capacity ring buffer and a linked queue in which a value passes exactly once between a producer and a consumer; measuring, through equality, that a read value no longer stays in the buffer.

The previous lesson measured the **shared** class, where a node was seen by two pointers
at once: a change was there no matter when it was read, because the shared object was
permanent. This lesson measures a different class through a different structure: a
producer writes a value into a buffer, a consumer reads it — but a read here is a
**take** operation, not a **peek**. Once a value is read, it does not stay in the
buffer; a second read returns empty. This behavior defines **handoff**, one of the four
classes: the value passes through an intermediary and reaches a reader, but has no
lasting visibility after that reader.

#### Ring Buffer: Fixed-Capacity Write and Read

The buffer holds a fixed array of four elements; `head` carries the index of the next
read, `tail` the index of the next write, `count` the number of occupied elements.
`ring_write` does not write if the buffer is full and returns `0`; `ring_read` does not
read if the buffer is empty and returns `0`. Both return `1` if successful.

```c
// ring.h — fixed-capacity ring buffer
#ifndef RING_H
#define RING_H

#define RING_CAPACITY 4

struct Ring {
    int data[RING_CAPACITY];
    int head;
    int tail;
    int count;
};

void ring_init(struct Ring *ring);
int ring_write(struct Ring *ring, int value);
int ring_read(struct Ring *ring, int *out);

#endif
```

```c
// ring.c — write and read at fixed capacity
#include "ring.h"

void ring_init(struct Ring *ring) {
    ring->head = 0;
    ring->tail = 0;
    ring->count = 0;
}

int ring_write(struct Ring *ring, int value) {
    if (ring->count == RING_CAPACITY) {
        return 0;
    }
    ring->data[ring->tail] = value;
    ring->tail = (ring->tail + 1) % RING_CAPACITY;
    ring->count = ring->count + 1;
    return 1;
}

int ring_read(struct Ring *ring, int *out) {
    if (ring->count == 0) {
        return 0;
    }
    *out = ring->data[ring->head];
    ring->head = (ring->head + 1) % RING_CAPACITY;
    ring->count = ring->count - 1;
    return 1;
}
```

#### A Value Passing Through the Buffer Exactly Once

Three values are written in order, then read in order. The order is preserved (the
first value written is the first read), but the real measurement is in the last two
lines: trying to read the same value a second time returns empty, because the value was
**taken** out of the buffer on the first read, not left there.

```c
// one_time_pass.c — three values written and read, then the buffer stays empty
#include <stdio.h>
#include "ring.h"

int main(void) {
    struct Ring ring;
    ring_init(&ring);

    ring_write(&ring, 10);
    ring_write(&ring, 20);
    ring_write(&ring, 30);

    int value = 0;
    ring_read(&ring, &value);
    printf("%s %d\n", "first value read", value);
    ring_read(&ring, &value);
    printf("%s %d\n", "second value read", value);
    ring_read(&ring, &value);
    printf("%s %d\n", "third value read", value);

    int ok = ring_read(&ring, &value);
    printf("%s %d\n", "fourth read succeeded", ok);
    printf("%s %d\n", "did value change after the failed fourth read", (int)(value == 30));

    return 0;
}
```

```
first value read 10
second value read 20
third value read 30
fourth read succeeded 0
did value change after the failed fourth read 1
```

**DS4.** The signature of `ring_read` (`int ring_read(struct Ring *ring, int *out)`)
does not say **who is responsible for releasing** a pointer, and in the same way it
doesn't say whether the value it receives stays in the buffer — what decides that is the
line `ring->count = ring->count - 1`, inside the body itself. The three values come out
in order (10, 20, 30), because `head` advances by one on every read and the buffer
preserves first-in, first-out order. The fourth call returns `0` because `count` has
dropped to zero; since nothing is written to `out`, `value` keeps carrying **30** left
over from the previous call — this is proof that `value` itself did not change, not
that the buffer offered a new value. The class is **handoff**: every value reached
exactly one reader, and left no trace in the buffer after that reader; this is where it
differs from the **shared** class — there the shared object kept standing even after
being read, here the slot that was read empties out.

#### Producer and Consumer Interleaving

Writes and reads can interleave: two writes, one read, one more write, two reads. This
mimics the real situation where a producer and a consumer use the same buffer at
different speeds. Because the buffer can hold at most four elements, the read before the
third write makes room.

```c
// interleaved_order.c — write and read calls interleaved
#include <stdio.h>
#include "ring.h"

int main(void) {
    struct Ring ring;
    ring_init(&ring);
    int value = 0;

    ring_write(&ring, 1);
    ring_write(&ring, 2);
    ring_read(&ring, &value);
    printf("%s %d\n", "read after two writes", value);

    ring_write(&ring, 3);
    ring_read(&ring, &value);
    printf("%s %d\n", "second read after three writes", value);
    ring_read(&ring, &value);
    printf("%s %d\n", "third read after three writes", value);

    int ok = ring_read(&ring, &value);
    printf("%s %d\n", "fourth read succeeded", ok);

    return 0;
}
```

```
read after two writes 1
second read after three writes 2
third read after three writes 3
fourth read succeeded 0
```

**DS5.** No matter what order the write and read calls interleave in, each read
returns the oldest **not-yet-read** value — the result of `head` advancing only on a
read and `tail` advancing only on a write. This second run produces the same three
values as the first (this time 1, 2, 3) in the same order, but the extra write placed
between them does not change the outcome: the buffer can hold at most four elements at
any moment, and because the read before the third write makes room, no overflow ever
happens. This flexibility between when a value is written and when it is read is a
second property of handoff: the producer and the consumer do not have to run at the
same speed, the buffer between them acts as a cushion — but what happens when a write
is attempted beyond capacity (overwriting an unread value) is not this lesson's
subject; that question belongs to the next lesson, which measures the **unobserved**
class.

#### Queue: The Same Principle at Unbounded Capacity

The ring buffer has a fixed capacity; the queue applies the same first-in first-out
order with linked nodes, without a capacity limit. `queue_enqueue` adds a node at the
tail, `queue_dequeue` removes the node at the head, returns its value, and releases the
node — here too, the value that was read does not stay in the queue.

```c
// queue.h — unbounded FIFO queue
#ifndef QUEUE_H
#define QUEUE_H

struct QueueNode {
    int value;
    struct QueueNode *next;
};

struct Queue {
    struct QueueNode *head;
    struct QueueNode *tail;
};

void queue_init(struct Queue *queue);
void queue_enqueue(struct Queue *queue, int value);
int queue_dequeue(struct Queue *queue, int *out);

#endif
```

```c
// queue.c — enqueue at the tail, dequeue from the head
#include <stdlib.h>
#include "queue.h"

void queue_init(struct Queue *queue) {
    queue->head = NULL;
    queue->tail = NULL;
}

void queue_enqueue(struct Queue *queue, int value) {
    struct QueueNode *node = malloc(sizeof *node);
    if (node == NULL) {
        return;
    }
    node->value = value;
    node->next = NULL;
    if (queue->tail == NULL) {
        queue->head = node;
    } else {
        queue->tail->next = node;
    }
    queue->tail = node;
}

int queue_dequeue(struct Queue *queue, int *out) {
    if (queue->head == NULL) {
        return 0;
    }
    struct QueueNode *node = queue->head;
    *out = node->value;
    queue->head = node->next;
    if (queue->head == NULL) {
        queue->tail = NULL;
    }
    free(node);
    return 1;
}
```

```c
// queue_passage.c — a value dequeued also reaches the reader exactly once
#include <stdio.h>
#include "queue.h"

int main(void) {
    struct Queue queue;
    queue_init(&queue);

    queue_enqueue(&queue, 100);
    queue_enqueue(&queue, 200);

    int value = 0;
    queue_dequeue(&queue, &value);
    printf("%s %d\n", "first value dequeued", value);
    queue_dequeue(&queue, &value);
    printf("%s %d\n", "second value dequeued", value);

    int ok = queue_dequeue(&queue, &value);
    printf("%s %d\n", "dequeue succeeded on an empty queue", ok);

    return 0;
}
```

```
first value dequeued 100
second value dequeued 200
dequeue succeeded on an empty queue 0
```

**DS6.** The queue uses a different memory model than the ring buffer — instead of a
fixed array, each element sits in its own allocated node, and `queue_dequeue` releases
the node after reading it (`free(node)`). But the class being measured is the same:
every value reaches exactly one call, and after that call it leaves no trace in the
queue. The absence of a capacity limit (`queue_enqueue` always succeeds, unless
`malloc` fails) does not change the handoff class; it only removes the ring buffer's
notion of "full."

#### Reject-on-Full or Overwrite Policy

`ring_write` rejects writing to a full buffer. But in some producer/consumer setups
the newest value matters more than the oldest — in that case, instead of rejecting on
full, overwriting the oldest element is preferred. Both policies can operate on the
same `struct Ring`; they differ only in what they do when the buffer is full.

```c
// ring_overwrite.h — declaration of a write policy that overwrites the oldest value when full
#ifndef RING_OVERWRITE_H
#define RING_OVERWRITE_H

#include "ring.h"

int ring_write_overwrite(struct Ring *ring, int value);

#endif
```

```c
// ring_overwrite.c — implementation of a write policy that overwrites the oldest value when full
#include "ring_overwrite.h"

int ring_write_overwrite(struct Ring *ring, int value) {
    if (ring->count == RING_CAPACITY) {
        ring->head = (ring->head + 1) % RING_CAPACITY;
    } else {
        ring->count = ring->count + 1;
    }
    ring->data[ring->tail] = value;
    ring->tail = (ring->tail + 1) % RING_CAPACITY;
    return 1;
}
```

```c
// fullness_policy.c — comparing the reject and overwrite policies
#include <stdio.h>
#include "ring_overwrite.h"

int main(void) {
    struct Ring ring;
    ring_init(&ring);

    ring_write(&ring, 10);
    ring_write(&ring, 20);
    ring_write(&ring, 30);
    ring_write(&ring, 40);

    int rejected = ring_write(&ring, 50);
    printf("%s %d\n", "result of writing to a full buffer under the reject policy", rejected);

    int overwrite_result = ring_write_overwrite(&ring, 99);
    printf("%s %d\n", "result of writing to a full buffer under the overwrite policy", overwrite_result);

    int out;
    for (int i = 0; i < RING_CAPACITY; i++) {
        ring_read(&ring, &out);
        printf("%s %d\n", "value read in order after the overwrite", out);
    }

    return 0;
}
```

```
result of writing to a full buffer under the reject policy 0
result of writing to a full buffer under the overwrite policy 1
value read in order after the overwrite 20
value read in order after the overwrite 30
value read in order after the overwrite 40
value read in order after the overwrite 99
```

**DS15.** Both policies make a different choice on a full buffer, but both are
deterministic: `ring_write` returns `0` on full without changing anything — the value
`50` never enters the structure at all. `ring_write_overwrite` instead advances `head`
by one and writes over the index where the oldest element (`10`) was sitting; `10`
disappears without ever reaching a reader. This defines **unobserved**, one of the four
classes: a value entered the structure but was overwritten before it reached any
reader. The difference between the rejected `50` and the overwritten `10`
matters — `50` was never written at all, `10` was written and then disappeared;
neither is ever readable in the end, but for different reasons.

#### The Bounding Measurement

A read function's signature does not say whether the value it reads stays at the
source. `int ring_read(struct Ring *ring, int *out)` and `int list_sum(const struct
Node *head)` (from the previous lesson) belong to the same family: both read a
structure. But one leaves the value it read inside the structure (the list is
permanent), the other removes the value it read from the structure (the buffer empties
out). This difference does not show up in the parameter types, the return type, or
even the function name; it can only be measured by looking at whether the body
decrements `count`, or by making two calls in a row and comparing their results —
exactly as this lesson did.

#### Summary

- The ring buffer applies the same first-in-first-out order in a fixed-capacity
  array that the queue applies in linked nodes without a capacity limit.
- A value does not stay in the buffer or the queue after being read; a second read or
  dequeue returns empty — the class is **handoff**.
- Handoff's difference from **shared**: a shared object stays in place after being
  read, but a handoff's read slot empties and the value can never be read again.
- A producer's and a consumer's write/read order can interleave; the buffer acts as a
  cushion between them.
- Reject-on-full and overwrite-oldest are two separate policies: the rejected value
  never enters the structure at all, the overwritten value enters and then
  disappears without ever reaching a reader — this second case defines the
  **unobserved** class.
- Whether a read function leaves the value it read behind at the source does not show
  up in its signature; it can only be measured by reading the body or by comparing two
  consecutive calls.

#### Next Step

In this lesson we measured that a value is read exactly once and then leaves the
buffer. The next lesson, Hash Tables, moves on with a different question: does a value
stored under a key always give the same result when read back repeatedly under that
key, or is a copy produced at read time? The class measured there is **copy**: the
value read from the table is not the table's own copy, but an independent copy the
caller receives.

### Hash Tables

Source: https://academia.sh/en/courses/c-systems-programming/hash-tables

Measuring that reading a record from a hash table with separate chaining produces its own copy, without changing the record in the table; and how colliding keys chain in the same bucket.

The previous two lessons measured a value either appearing permanently through a
shared object (**shared**) or passing through a mediator exactly once and
disappearing (**handoff**). This lesson measures a third class, **copy**: reading a
record from a hash table does not remove that record from the table, and does not
produce an object shared with the table's record either — it hands the caller **its
own copy** of the record. A change made on this copy never reaches the table at all,
because there is no memory block shared between the two.

#### Hash Table: Insertion and Lookup with Separate Chaining

The table consists of a fixed number of buckets; each bucket is a linked list (a
chain). `hash_index` maps a key to an index based on the bucket count. `hash_insert`
updates the record if the key already exists in the chain, otherwise adds a new node
at the front of the chain. `hash_find` writes a **copy** of the record to `out` and
returns `1` if it finds the record; returns `0` if it does not.

```c
// hash.h — hash table with separate chaining
#ifndef HASH_H
#define HASH_H

#define HASH_CAPACITY 8

struct Record {
    int amount;
    int counter;
};

struct HashNode {
    int key;
    struct Record value;
    struct HashNode *next;
};

struct HashTable {
    struct HashNode *buckets[HASH_CAPACITY];
};

void hash_init(struct HashTable *t);
void hash_insert(struct HashTable *t, int key, struct Record value);
int hash_find(const struct HashTable *t, int key, struct Record *out);
void hash_free(struct HashTable *t);

#endif
```

```c
// hash.c — implementation of the hash table operations
#include <stdlib.h>
#include "hash.h"

static int hash_index(int key) {
    return (key % HASH_CAPACITY + HASH_CAPACITY) % HASH_CAPACITY;
}

void hash_init(struct HashTable *t) {
    for (int i = 0; i < HASH_CAPACITY; i++) {
        t->buckets[i] = NULL;
    }
}

void hash_insert(struct HashTable *t, int key, struct Record value) {
    int i = hash_index(key);
    for (struct HashNode *n = t->buckets[i]; n != NULL; n = n->next) {
        if (n->key == key) {
            n->value = value;
            return;
        }
    }
    struct HashNode *node = malloc(sizeof *node);
    if (node == NULL) {
        return;
    }
    node->key = key;
    node->value = value;
    node->next = t->buckets[i];
    t->buckets[i] = node;
}

int hash_find(const struct HashTable *t, int key, struct Record *out) {
    int i = hash_index(key);
    for (struct HashNode *n = t->buckets[i]; n != NULL; n = n->next) {
        if (n->key == key) {
            *out = n->value;
            return 1;
        }
    }
    return 0;
}

void hash_free(struct HashTable *t) {
    for (int i = 0; i < HASH_CAPACITY; i++) {
        struct HashNode *n = t->buckets[i];
        while (n != NULL) {
            struct HashNode *next = n->next;
            free(n);
            n = next;
        }
    }
}
```

#### The Read Record Being Its Own Copy

A record is inserted and then read, then the read copy is changed. The actual
measurement is whether the value read **again** from the table after this change
still holds its original value.

```c
// found_copy.c — is the read record changed, does the table stay unaffected
#include <stdio.h>
#include "hash.h"

int main(void) {
    struct HashTable t;
    hash_init(&t);

    struct Record initial = {100, 1};
    hash_insert(&t, 3, initial);

    struct Record found;
    hash_find(&t, 3, &found);
    printf("%s %d\n", "amount first read", found.amount);

    found.amount = 900;
    printf("%s %d\n", "copy's amount after the copy is changed", found.amount);

    struct Record found_again;
    hash_find(&t, 3, &found_again);
    printf("%s %d\n", "amount read again from the table", found_again.amount);

    hash_free(&t);
    return 0;
}
```

```
amount first read 100
copy's amount after the copy is changed 900
amount read again from the table 100
```

**DS7.** `hash_find`'s signature (`int hash_find(const struct HashTable *t, int key,
struct Record *out)`) does not say whether the `struct Record` written to `out` is the
table's own record or an independent copy — what decides this is the line `*out =
n->value`, a **struct assignment**. Assigning one struct to another in C copies its
fields one by one; after this assignment, `found` does not point at the same memory
block as `n->value` — it sits in its own independent block. The assignment
`found.amount = 900` therefore changes only `found`; the table parameter is already
passed to `hash_find` as `const struct HashTable *`, and the function never changes
it at all. The second `hash_find` call reads from the table **again** and gives back
the first value (**100**) — not 900. The class is **copy**: a change made on the read
record never reaches a second reader (here, the table itself), because there is no
object shared between them.

#### Re-inserting With the Same Key

When `hash_insert` is called a second time with the same key, it does not add a new
node — it updates the existing node's `value` field. This does not affect a copy
already read earlier — that copy already sits in its own separate memory block.

```c
// reinsertion.c — updating with the same key does not affect a previous copy
#include <stdio.h>
#include "hash.h"

int main(void) {
    struct HashTable t;
    hash_init(&t);

    struct Record initial = {5, 1};
    hash_insert(&t, 7, initial);

    struct Record found_before;
    hash_find(&t, 7, &found_before);
    printf("%s %d\n", "amount read before the update", found_before.amount);

    struct Record updated = {50, 2};
    hash_insert(&t, 7, updated);

    printf("%s %d\n", "old copy's amount after the update", found_before.amount);

    struct Record found_after;
    hash_find(&t, 7, &found_after);
    printf("%s %d\n", "amount read from the table after the update", found_after.amount);

    hash_free(&t);
    return 0;
}
```

```
amount read before the update 5
old copy's amount after the update 5
amount read from the table after the update 50
```

**DS8.** `found_before` is a copy taken before the `hash_insert(&t, 7, updated)`
call — since it has no remaining link to the table at all, it does not change when the
table is updated; it stays at **5**. `found_after`, read from the table again, sees
the current value (**50**), because this time `hash_find` was called **after** the
update and took a new copy of the table's state at that moment. These two lines
together show the copy class's boundary: a copy carries the value it held at the
moment it was taken forever; even if its source changes afterward, the copy **never**
sees this, because seeing it would require a shared link between them.

#### Colliding Keys Chained in the Same Bucket

Since the capacity is eight, keys 3 and 11 fall into the same bucket (`3 % 8 = 3`,
`11 % 8 = 3`). In this case `hash_insert` adds the second node at the front of the
chain; `hash_find` finds the correct key by searching the chain.

```c
// colliding_keys.c — two different keys falling into the same bucket
#include <stdio.h>
#include "hash.h"

int main(void) {
    struct HashTable t;
    hash_init(&t);

    struct Record first = {11, 0};
    struct Record second = {22, 0};
    hash_insert(&t, 3, first);
    hash_insert(&t, 11, second);

    struct Record result;
    hash_find(&t, 3, &result);
    printf("%s %d\n", "amount found for key 3", result.amount);
    hash_find(&t, 11, &result);
    printf("%s %d\n", "amount found for key 11", result.amount);

    hash_free(&t);
    return 0;
}
```

```
amount found for key 3 11
amount found for key 11 22
```

**DS9.** Even though the two keys fall into the same bucket, `hash_find` finds the
correct record, because the search does not look only at the bucket's index — it also
checks the `key` field of every node in the chain (`n->key == key`). This shows why a
hash table is not an array but an **array of lists**: more than one key can fall into
the same bucket, and the chain resolves this collision. The collision itself does not
change the class this lesson measures — both `hash_find` calls still each produce
their own copy.

#### A Version That Holds the Key by Pointer

The table so far has held the key as an `int`, **by value** — the key passed to
`hash_insert` is copied, and the node stores its own independent copy. A version can
also be written that instead holds the key by pointer; this turns the class the key
measures from **copy** to **shared**, and creates a different hazard.

```c
// hash_ptr.h — declaration of the hash table variant that holds the key by pointer
#ifndef HASH_PTR_H
#define HASH_PTR_H

#include "hash.h"

struct HashNodePtr {
    const int *key;
    struct Record value;
    struct HashNodePtr *next;
};

struct HashTablePtr {
    struct HashNodePtr *buckets[HASH_CAPACITY];
};

void hash_ptr_init(struct HashTablePtr *t);
void hash_ptr_insert(struct HashTablePtr *t, const int *key, struct Record value);
int hash_ptr_find(const struct HashTablePtr *t, int key_value, struct Record *out);
void hash_ptr_free(struct HashTablePtr *t);

#endif
```

```c
// hash_ptr.c — implementation of the hash table variant that holds the key by pointer
#include <stdlib.h>
#include "hash_ptr.h"

static int hash_ptr_index(int key_value) {
    return (key_value % HASH_CAPACITY + HASH_CAPACITY) % HASH_CAPACITY;
}

void hash_ptr_init(struct HashTablePtr *t) {
    for (int i = 0; i < HASH_CAPACITY; i++) {
        t->buckets[i] = NULL;
    }
}

void hash_ptr_insert(struct HashTablePtr *t, const int *key, struct Record value) {
    int i = hash_ptr_index(*key);
    struct HashNodePtr *node = malloc(sizeof *node);
    if (node == NULL) {
        return;
    }
    node->key = key;
    node->value = value;
    node->next = t->buckets[i];
    t->buckets[i] = node;
}

int hash_ptr_find(const struct HashTablePtr *t, int key_value, struct Record *out) {
    int i = hash_ptr_index(key_value);
    for (struct HashNodePtr *n = t->buckets[i]; n != NULL; n = n->next) {
        if (*(n->key) == key_value) {
            *out = n->value;
            return 1;
        }
    }
    return 0;
}

void hash_ptr_free(struct HashTablePtr *t) {
    for (int i = 0; i < HASH_CAPACITY; i++) {
        struct HashNodePtr *n = t->buckets[i];
        while (n != NULL) {
            struct HashNodePtr *next = n->next;
            free(n);
            n = next;
        }
    }
}
```

```c
// key_pointer.c — does the lookup break when the variable holding the key later changes
#include <stdio.h>
#include "hash_ptr.h"

int main(void) {
    struct HashTablePtr t;
    hash_ptr_init(&t);

    int key_variable = 5;
    struct Record value = {42, 1};
    hash_ptr_insert(&t, &key_variable, value);

    struct Record found;
    int found_before = hash_ptr_find(&t, 5, &found);
    printf("%s %d\n", "was it found with key 5 before the change", found_before);

    key_variable = 99;

    int found_with_old_value = hash_ptr_find(&t, 5, &found);
    printf("%s %d\n", "found with old value 5 after changing the variable to 99", found_with_old_value);

    int found_with_new_value = hash_ptr_find(&t, 99, &found);
    printf("%s %d\n", "found with new value 99 after changing the variable to 99", found_with_new_value);

    hash_ptr_free(&t);
    return 0;
}
```

```
was it found with key 5 before the change 1
found with old value 5 after changing the variable to 99 0
found with new value 99 after changing the variable to 99 0
```

**DS16.** `hash_ptr_insert` computes the node's bucket from `*key` — the **current**
value of `key_variable` at that moment — and this bucket never changes afterward. But
the node does not copy the key; it holds the address of `key_variable` — so there is a
**shared** relationship between the `key` field and the caller's variable. When
`key_variable` changes to `99`, the memory `n->key` points at also becomes `99`, but
the node still sits in the bucket for `5`. Result: when searched with `5`, the node
sitting in the bucket no longer has the key `5` (not found); when searched with `99`,
the correct bucket (the bucket for `99`) is empty (also not found). The record
becomes unreachable by any key — placement into a bucket was a one-time decision, but
the comparison keeps reading live memory. `hash_insert` is immune to this hazard,
because it copies the key by value; holding the key by pointer loses this protection.

#### The Bounding Measurement

A lookup function's return type does not say whether the record it returns is shared
with the table's own memory. The `struct Record *out` parameter carries a pointer,
but where that pointer points is the caller's own local variable — the table has no
knowledge of this variable. Because this lesson's `hash_find` always performs a
struct assignment (`*out = n->value`), the class is always **copy**; a function
written with the same signature would give **shared** as its result if it instead
wrote the address of the table's own node into `out` (if `out` were a `struct Record
**`). This difference can only be understood by reading the body, or — as done in
this lesson — by changing the source afterward and measuring whether the copy sees
it.

#### Summary

- A hash table distributes into a fixed number of buckets; each bucket is a chain,
  and colliding keys within the same chain are distinguished by checking the `key`
  field.
- `hash_find` produces a **copy** of the record through a struct assignment (`*out =
  n->value`); a change on the copy never reaches the table — the class is **copy**.
- A copy keeps carrying the value it held at the moment it was taken; even if the
  source changes afterward, a copy taken earlier does not see this.
- **Copy**'s difference from **shared**: in shared there is a single shared object, in
  copy there are two independent objects; its difference from **handoff**: in
  handoff the read slot empties, in copy the table can hand back the same record
  again and again (in its current form).
- Whether a lookup function's returned value is a copy or a shared object does not
  show up in its signature; it can only be understood by reading the body or by
  measurement.
- Holding the key by pointer instead of by value turns the class into **shared**: a
  bucket's placement is fixed to the value at insertion time, but the comparison
  reads live memory — if the outside variable changes, the record can become
  unreachable by any key.

#### Next Step

These three lessons measured three separate classes (shared, handoff, copy) through a
linked structure, a buffer, and a table. The next lesson, Object-Like Design, looks at
how C builds the impression of an "object" in a language without classes — through
function pointers and opaque structs — and shows how these three classes can coexist
within the same object interface.

### Object-Like Design

Source: https://academia.sh/en/courses/c-systems-programming/object-like-design

In object-like interfaces built with an opaque struct and function pointers embedded in the struct, measuring that two handles share the same object and that a method call does not change that sharing.

C has no classes, but the impression of an "object" can be built with two tools: an
**opaque struct** (a type whose internal fields are visible only in the `.c` file
where it's defined) and embedded **function pointers** (callable fields that read like
a "method" call but are actually just data fields). This lesson builds both, and asks
the same question: when an "object" is accessed through two handles, is a change made
through one visible through the other? The answer is once again **shared** — opacity
and method-call syntax do not change the underlying sharing, they only change how it
is read.

#### Opaque Struct: An Object Interface Through Information Hiding

`struct Object`'s definition lives only in `object.c`; `object.h` has only a **forward
declaration** (`struct Object;`). This means a file that includes the header cannot
access the `value` field directly, and can only speak to the object through the
`object_set`/`object_get` functions — these two functions are the object's "public
interface."

```c
// object.h — an opaque object interface
#ifndef OBJECT_H
#define OBJECT_H

struct Object;

struct Object *object_create(int initial);
void object_set(struct Object *object, int value);
int object_get(const struct Object *object);
void object_free(struct Object *object);

#endif
```

```c
// object.c — the struct's actual fields, hidden from callers
#include <stdlib.h>
#include "object.h"

struct Object {
    int value;
};

struct Object *object_create(int initial) {
    struct Object *object = malloc(sizeof *object);
    if (object == NULL) {
        return NULL;
    }
    object->value = initial;
    return object;
}

void object_set(struct Object *object, int value) {
    object->value = value;
}

int object_get(const struct Object *object) {
    return object->value;
}

void object_free(struct Object *object) {
    free(object);
}
```

When `object_free` gets called is a separate question — the previous course's (**Memory
and Pointers in C**, K02) question of "who releases this object", which here too is a
contract taken on by whoever calls `object_create`. This lesson measures not that
question but whether two handles share the same object.

#### Two Handles Sharing the Same Object

`object` and `second_handle` carry the same `struct Object *` value — both point to the
same object. The opaque type only blocks **direct** access to the `value` field; access
through `object_set`/`object_get` still reaches the same object.

```c
// two_handles.c — two variables holding the same object
#include <stdio.h>
#include "object.h"

int main(void) {
    struct Object *object = object_create(1);

    printf("%s %d\n", "value at creation", object_get(object));

    struct Object *second_handle = object;
    object_set(second_handle, 42);

    printf("%s %d\n", "value read through the original handle", object_get(object));
    printf("%s %d\n", "is second_handle the same object as object", (int)(second_handle == object));

    object_free(object);
    return 0;
}
```

```
value at creation 1
value read through the original handle 42
is second_handle the same object as object 1
```

**DS10.** The assignment `second_handle = object` does not copy an object, it copies a
**pointer** — in the end both carry the address of the same `malloc` block, just like
the assignment `second = head->next` in the linked list (the Linked Structures
lesson). The call `object_set(second_handle, 42)` changes the `value` field, and that
change is read back through `object_get(object)`: **42**. The opaque type changes
nothing here — the fact that `object->value` cannot be written to from outside doesn't
hide that `object` and `second_handle` share the same object, it only restricts how
that sharing can be reached (only through functions). The last line confirms this
without printing an address: the comparison `object == second_handle` gives **1**. The
class is **shared**.

#### The Impression of Method Calls Through Function Pointers

The second approach doesn't hide information, it embeds the "methods" directly inside
the struct: the `increment` and `get_value` fields are function pointers, and
`counter_object_init` binds them to concrete functions. When the caller writes
`counter.increment(&counter, 5)`, this produces a syntax similar to the C++ call
`counter.increment(5)` — but `increment` is not a method, it is a data field.

```c
// counter_object.h — an object that carries its own operations
#ifndef COUNTER_OBJECT_H
#define COUNTER_OBJECT_H

struct CounterObject {
    int count;
    void (*increment)(struct CounterObject *, int);
    int (*get_value)(const struct CounterObject *);
};

void counter_object_init(struct CounterObject *counter, int initial);

#endif
```

```c
// counter_object.c — the functions a counter object's fields point at
#include "counter_object.h"

static void counter_increment(struct CounterObject *self, int amount) {
    self->count = self->count + amount;
}

static int counter_get_value(const struct CounterObject *self) {
    return self->count;
}

void counter_object_init(struct CounterObject *counter, int initial) {
    counter->count = initial;
    counter->increment = counter_increment;
    counter->get_value = counter_get_value;
}
```

```c
// method_call.c — two handles calling the same struct's function pointers
#include <stdio.h>
#include "counter_object.h"

int main(void) {
    struct CounterObject counter;
    counter_object_init(&counter, 0);

    struct CounterObject *first_handle = &counter;
    struct CounterObject *second_handle = &counter;

    first_handle->increment(first_handle, 5);
    printf("%s %d\n", "value read through the second handle after the first handle's increment",
           second_handle->get_value(second_handle));

    second_handle->increment(second_handle, 3);
    printf("%s %d\n", "value after both handles have incremented in sequence", counter.get_value(&counter));

    return 0;
}
```

```
value read through the second handle after the first handle's increment 5
value after both handles have incremented in sequence 8
```

**DS11.** `first_handle` and `second_handle` both carry `&counter` — the address of
the same local variable, exactly like the `object`/`second_handle` pair in the earlier
measurement. `first_handle->increment(first_handle, 5)` raises `counter.count` from 0
to 5, and `second_handle->get_value(second_handle)` sees it: **5**. Because
`counter.increment` and `counter.get_value` are function pointers, `first_handle->increment`
and `second_handle->increment` read the same pointer too — the function actually
called is `counter_increment` either way. The syntax that looks like a method call
never changes how `counter.count` is reached; `increment` and `get_value` only carry,
**as data**, which function gets called. The class is once again **shared**: both
handles point at the same struct, and the only difference between them is the syntax
used to reach it.

#### A Shared Method Table

`counter_object.c`'s approach gives every counter its own copy of the two function
pointers, even though every counter uses the exact same two functions. `CounterManager`
instead stores a single pointer to a shared, `const` table of methods — every manager
points at the same table rather than carrying its own copy of the function pointers.

```c
// counter_vtable.h — a shared, read-only method table
#ifndef COUNTER_VTABLE_H
#define COUNTER_VTABLE_H

struct CounterManager {
    int count;
    const struct CounterMethods *methods;
};

struct CounterMethods {
    void (*increment)(struct CounterManager *self, int amount);
    int (*get_value)(const struct CounterManager *self);
};

extern const struct CounterMethods COUNTER_METHODS;

void counter_manager_init(struct CounterManager *manager, int initial);

#endif
```

```c
// counter_vtable.c — one table, shared by every counter manager
#include "counter_vtable.h"

static void counter_increment_v(struct CounterManager *self, int amount) {
    self->count = self->count + amount;
}

static int counter_get_value_v(const struct CounterManager *self) {
    return self->count;
}

const struct CounterMethods COUNTER_METHODS = { counter_increment_v, counter_get_value_v };

void counter_manager_init(struct CounterManager *manager, int initial) {
    manager->count = initial;
    manager->methods = &COUNTER_METHODS;
}
```

```c
// shared_table.c — two managers pointing at the exact same method table
#include <stdio.h>
#include "counter_vtable.h"

int main(void) {
    struct CounterManager first;
    struct CounterManager second;
    counter_manager_init(&first, 0);
    counter_manager_init(&second, 100);

    first.methods->increment(&first, 5);
    second.methods->increment(&second, 5);

    printf("%s %d\n", "first counter's value", first.methods->get_value(&first));
    printf("%s %d\n", "second counter's value", second.methods->get_value(&second));
    printf("%s %d\n", "do both managers point at the same method table",
           (int)(first.methods == second.methods));

    return 0;
}
```

```
first counter's value 5
second counter's value 105
do both managers point at the same method table 1
```

**DS17.** `first.count` and `second.count` are independent of each other — each
instance carries its own `int` field, and changing one does not affect the other. But
`first.methods` and `second.methods` carry the **same** address: `&COUNTER_METHODS`, a
`const` object defined exactly once at file scope. In the `CounterObject` design, each
instance kept its **own copy** of the `increment` and `get_value` fields (even though
they pointed at the same functions, from two separate memory locations); here,
instances share not the individual function addresses but a single pointer to the
table itself. Two different classes are visible inside the same struct at once: for
the `count` field, instances are as independent as a **copy**; for the table that
`methods` points at, they are **shared** — the table is a single object, and every
instance reaches it through the same address.

#### Producing an Independent Object With a Cloning Function

The two examples so far (handle copying, the shared method table) always produced the
**shared** class: whether a pointer was copied or shared, the underlying structure
stayed one. But `object.h`'s public interface is already enough to produce an
independent object — reading the current value with `object_get` and building a new
object from that value with `object_create` produces a **deep copy**, without ever
touching the opaque type's internal field.

```c
// object_clone.h — cloning an opaque object through its own interface
#ifndef OBJECT_CLONE_H
#define OBJECT_CLONE_H

#include "object.h"

struct Object *object_clone(const struct Object *source);

#endif
```

```c
// object_clone.c — building an independent object through the public interface
#include "object_clone.h"

struct Object *object_clone(const struct Object *source) {
    return object_create(object_get(source));
}
```

```c
// object_clone_demo.c — a clone that no longer shares anything with the source
#include <stdio.h>
#include "object.h"
#include "object_clone.h"

int main(void) {
    struct Object *source = object_create(7);
    struct Object *clone = object_clone(source);

    printf("%s %d\n", "clone's initial value", object_get(clone));

    object_set(source, 99);
    printf("%s %d\n", "clone's value after source changed", object_get(clone));
    printf("%s %d\n", "is clone the same object as source", (int)(clone == source));

    object_free(source);
    object_free(clone);
    return 0;
}
```

```
clone's initial value 7
clone's value after source changed 7
is clone the same object as source 0
```

**DS19.** `object_clone` never touches the struct `source` points at — it only reads
the current value with `object_get(source)` and builds a **new `malloc` block** with
`object_create`. As a result, `clone` carries a separate address from `source`: the
last line confirms this with `clone == source` returning **0**. `object_set(source, 99)`
changes only the block `source` points at; the block `clone` points at is unaffected,
so `object_get(clone)` still reads **7**. This is the exact opposite of the lesson's
first two examples: `second_handle = object` copied a pointer and shared the same
struct (**shared**), while `object_clone` uses the public interface to build a **new
struct**, landing the result in the **copy** class — the two objects are now
independent, and changing one does not change the other. The difference is not in the
shape of the code: both assign a pointer, both work behind the opaque type; the
difference is whether the right-hand side carries an **existing address** or comes
from a **fresh allocation**.

#### The Bounding Measurement

The shape of an "object" interface — whether it's an opaque type, function pointers
inside a struct, or ordinary functions — does not determine the underlying sharing.
`object_set(second_handle, 42)` and `first_handle->increment(first_handle, 5)` look
different (one reads like an ordinary function call, the other like a call through a
struct field), but both rest on the same principle: modifying a structure reached
through a pointer is visible through every pointer that reaches that structure. An
interface looking "object-like" — information hiding, method-call syntax — does not
hide or remove the sharing, it only shapes how the shared structure is accessed. The
sharing itself can still only be measured by pointer equality (`object ==
second_handle`, `first_handle == second_handle`).

#### Summary

- An opaque type (the forward declaration `struct Object;`) blocks direct access to
  the internal fields, but does not change the sharing between two handles pointing at
  the object; function pointers embedded inside a struct (`increment`, `get_value`)
  also give a method-call syntax but remain data fields. Sharing can only be measured
  by pointer equality.
- When two handles point at the same object/struct, a change made through one is
  visible through the other (no matter which interface style is used) — the class is
  **shared**.
- **Who releases** an object (K02's question) and **who sees** a change (this lesson's
  question) are separate axes.
- Instances carrying their own function pointers versus being linked by a pointer to a
  single shared table give different results: in the first, every instance keeps its
  own copy; in the second, the table is single and every instance reaches it through
  the same address.
- Building a new object through the public interface (only `object_create`/`object_get`)
  is different from copying a pointer: the result lands in the **copy** class, and the
  two objects stay independent of each other.

#### Next Step

In both this lesson's examples, resource management was simple: a single `object_free`
call, or no allocation at all. The next lesson, Resource Cleanup Idioms, measures
situations where several resources (several allocations, several open handles) must be
opened in sequence within a function and, on failure, closed in the correct order —
there the class is **unobserved**: a resource is closed and disappears on the failure
path without ever reaching a reader.

### Resource Cleanup Idioms

Source: https://academia.sh/en/courses/c-systems-programming/resource-cleanup-idioms

Measuring a resource that closes without ever reaching a reader, on the failure path of a function that opens several resources in sequence.

This lesson closes the series' final measurement class: **unobserved**. In earlier
lessons, a change either stayed with its writer (copy), became visible to everyone
through a shared object (shared), or passed through a mediator and was read exactly
once (handoff). Here is the fourth case: a resource is opened, and closed by a
failure before it is filled or read — it **never reaches** any reader at all. What is
measured is not where a value reached, but which resource disappeared without ever
reaching anywhere.

#### Resources Opened in Order, Closed in Reverse Order

`Store` carries two separate allocations: an `int` array and a label string. Both are
opened in sequence inside `store_open`; if one fails, everything opened up to that
point is closed **in reverse order** — the one opened last is closed first.

```c
// store.h — a struct carrying more than one resource
#ifndef STORE_H
#define STORE_H

struct Store {
    int *values;
    char *label;
    int element_count;
};

int store_open(struct Store *s, int element_count, const char *label);
void store_close(struct Store *s);
int *store_take_values(struct Store *s);

#endif
```

```c
// store.c — sequential opening, reverse-order rollback
#include <stdlib.h>
#include <string.h>
#include "store.h"

int store_open(struct Store *s, int element_count, const char *label) {
    s->values = NULL;
    s->label = NULL;
    s->element_count = 0;

    s->values = malloc((size_t)element_count * sizeof *s->values);
    if (s->values == NULL) {
        return 0;
    }

    s->label = malloc(strlen(label) + 1);
    if (s->label == NULL) {
        free(s->values);
        s->values = NULL;
        return 0;
    }
    strcpy(s->label, label);

    s->element_count = element_count;
    return 1;
}

void store_close(struct Store *s) {
    free(s->label);
    free(s->values);
    s->label = NULL;
    s->values = NULL;
}

int *store_take_values(struct Store *s) {
    int *moved = s->values;
    s->values = NULL;
    s->element_count = 0;
    return moved;
}
```

Opening and closing these two resources gives the same answer to the previous
course's (**Memory and Pointers in C**, K02) question of "who releases it": whoever
calls `store_open` is also responsible for calling `store_close`. This lesson is not
about that contract — it is about how to measure that, when `store_open` stops
halfway, the `label` field was never written to at all.

#### A Resource Closing Without Ever Reaching a Reader

The expression `(size_t)element_count * sizeof *s->values` inside `store_open` wraps
around to an enormous value when `element_count` is negative, since it converts to an
unsigned type — no `malloc` can satisfy a size that large, and the call is guaranteed
to return `NULL`. `values` cannot be allocated, `label` is never opened at all, and
`store_open` returns 0.

```c
// failed_open.c — the first resource fails to open, the second never opens at all
#include <stdio.h>
#include "store.h"

int main(void) {
    struct Store s;
    int result = store_open(&s, -1, "measurement");

    printf("%s %d\n", "store_open result", result);
    printf("%s %d\n", "is label NULL because it was never opened", (int)(s.label == NULL));
    printf("%s %d\n", "is values also NULL because it could not be allocated", (int)(s.values == NULL));

    store_close(&s);
    return 0;
}
```

```
store_open result 0
is label NULL because it was never opened 1
is values also NULL because it could not be allocated 1
```

**DS12.** The `-1` given as `element_count` wraps to an enormous value once
`(size_t)element_count` converts it to an unsigned type; multiplying that value by
`sizeof(int)` asks `malloc` for a size no environment can satisfy — the call returns
`NULL`, and `store_open` exits **immediately** in its first `if` branch. `malloc` is
never even called for the `label` field; `s.label` stays at the `NULL` it was assigned
at the top of the function. The `label` resource disappears — without reaching a
reader, without carrying a value — **without ever having existed**. The class is
**unobserved**: the copy, shared, and handoff classes from earlier lessons all assumed
a value reached *somewhere*; here it reaches nowhere at all. `store_close` is still
called regardless — `free(NULL)` is well-defined and harmless, so the cleanup call can
always be made unconditionally.

#### Rolling Back the First Resource While the Second Fails to Open

Now suppose the first resource (`values`) opens successfully, but allocation for the
second (`label`) fails — `store_open` would then have to roll the first one back too.
To observe this directly (since a real `malloc` failure cannot be triggered in this
environment), we reproduce `store_open`'s rollback branch as a separate function,
`store_open_second_fails` — the same rollback pattern, with the second allocation
point forced to fail.

```c
// store_open_second_fails.c — rolling back the first resource while the second fails to open
#include <stdlib.h>
#include "store.h"

int store_open_second_fails(struct Store *s, int element_count) {
    s->values = NULL;
    s->label = NULL;
    s->element_count = 0;

    s->values = malloc((size_t)element_count * sizeof *s->values);
    if (s->values == NULL) {
        return 0;
    }

    /* the second allocation is deliberately treated as having failed here */
    free(s->values);
    s->values = NULL;
    return 0;
}
```

```c
// second_rollback.c — the call where the first resource opened is rolled back
#include <stdio.h>
#include "store.h"

int store_open_second_fails(struct Store *s, int element_count);

int main(void) {
    struct Store s;
    int result = store_open_second_fails(&s, 4);

    printf("%s %d\n", "store_open_second_fails result", result);
    printf("%s %d\n", "is the first resource NULL because it was rolled back", (int)(s.values == NULL));
    printf("%s %d\n", "is the second resource NULL because it never opened", (int)(s.label == NULL));

    return 0;
}
```

```
store_open_second_fails result 0
is the first resource NULL because it was rolled back 1
is the second resource NULL because it never opened 1
```

**DS13.** `values` genuinely opens here (`malloc` succeeds for a 4-element array),
but the moment the second resource's opening is treated as having failed, it is
**rolled back** with `free(s->values)`. `s.values` being `NULL` shows that the
resource opened and then closed without ever reaching a reader — much like a single
reading of a handoff value in the Ring Buffers and Queues lesson, except here there is
no reader at all; the value is produced and then destroyed within the same function.
This pattern is exactly what the `goto cleanup` idiom, covered in the previous course's
**Error Handling Patterns** lesson, does: there, closing in reverse order from a
single exit point was built with `goto`; here the same ordering was written by hand
with explicit `if`/`free` calls. This lesson does not re-teach that idiom — it only
measures the result it produces: the resource disappearing unobserved.

#### The Order of exit, _exit, and atexit

The rollback so far was built with hand-written `if`/`free` calls. C's `atexit`
function offers another route: it registers a handler to "run when the process
terminates normally." But this registration depends on **how** the process
terminates — the difference between `exit` and `_exit` reveals another face of the
unobserved class here.

```c
// atexit_order.c — exit runs atexit handlers in the reverse of their registration order
#include <stdio.h>
#include <stdlib.h>

static void cleanup_a(void) {
    printf("%s\n", "cleanup_a ran");
}

static void cleanup_b(void) {
    printf("%s\n", "cleanup_b ran");
}

int main(void) {
    atexit(cleanup_a);
    atexit(cleanup_b);

    printf("%s\n", "main's body finished");
    exit(0);
}
```

```
main's body finished
cleanup_b ran
cleanup_a ran
```

```c
// exit_and_atexit_skipped.c — _exit never runs atexit handlers at all
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static void never_runs(void) {
    printf("%s\n", "this line is never printed");
}

int main(void) {
    atexit(never_runs);

    printf("%s\n", "right before calling _exit");
    fflush(stdout);
    _exit(0);
}
```

```
right before calling _exit
```

**DS18.** In the first program, `atexit(cleanup_a)` is registered first and
`atexit(cleanup_b)` second; when `exit(0)` is called, the two run in the **reverse of
their registration order** — `cleanup_b` first, then `cleanup_a`. This repeats
`store_open`'s reverse-order rollback pattern (this lesson's first section), now
automated through `atexit`. In the second program, `never_runs` is likewise
registered with `atexit`, but the process terminates not with `exit` but with
**`_exit`** — POSIX's `_exit` function closes the process immediately, without
running any `atexit` handler at all. Because the registered handler is never called,
`"this line is never printed"` is never printed; the output consists only of the line
before `_exit`. This is the unobserved class at the function level: `never_runs` was
**registered** to reach a reader (an output), but was never run at all. The
`fflush(stdout)` line makes a separate point too: unlike `exit`, `_exit` does not
flush buffered `stdio` output — without that line, the `"right before calling
_exit"` line could have disappeared as well, because the buffer vanishes along with
`_exit` without ever being flushed.

#### Handing Off a Resource to Another Variable

`store_close` always kept ownership of `s.values` inside the `Store` struct.
Ownership can also be **handed off** to another variable: `store_take_values` detaches
the array from `s` and hands it to the caller, leaving `s.values` at `NULL` — `s` can
no longer reach that array; access has moved, through a single path, to the new
owner.

```c
// data_handoff.c — ownership of the array inside the store handed off to another variable
#include <stdio.h>
#include <stdlib.h>
#include "store.h"

int main(void) {
    struct Store s;
    store_open(&s, 3, "temporary");

    int *taken = store_take_values(&s);
    taken[0] = 10;

    printf("%s %d\n", "is store.values NULL after the handoff", (int)(s.values == NULL));
    printf("%s %d\n", "value written through taken", taken[0]);

    free(taken);
    store_close(&s);
    return 0;
}
```

```
is store.values NULL after the handoff 1
value written through taken 10
```

**DS20.** Before `store_take_values` is called, the only path to the array is
`s.values`. After the call, `moved` (`taken` on the caller's side) carries the array,
and `s->values` is set to `NULL` — there are never two access paths at the same time;
one closes exactly as the other opens. This differs from this lesson's other
**unobserved** examples: there, the resource disappeared without ever reaching any
reader; here, the resource reaches **exactly one** reader (its new owner) and becomes
unreachable through the old path — the class is **handoff**. After the handoff, the
`free(s.values)` line inside the `store_close(&s)` call is safe: `s.values` is already
`NULL`, and `free(NULL)` does nothing; responsibility for freeing the array now
belongs to whatever code holds `taken`, which is exactly what the separate
`free(taken)` call carries out.

#### The Bounding Measurement

The unobserved class measures not that a value reached the *wrong* place, but that it
reached **no place at all**. The checks `s.label == NULL` and `s.values == NULL` are
the proof of this: the resource was disabled without ever being seen by a reader,
assigned, or copied. This is fundamentally different from the previous four lessons'
measurements — copy, shared, and handoff all said "a value reached somewhere, we are
measuring how it got there"; here the question is "a resource disappeared without
reaching anywhere, how do we verify that." The verification tool stays the same:
pointer comparison (`== NULL`), without printing an address or a value.

#### Summary

- When several resources are opened in sequence, a failure path must roll them back
  in **reverse order** — the one opened last is closed first.
- When a resource fails to open, that resource never reaches any reader (staying
  `NULL` is the measurable proof of this); when a second resource fails to open, the
  first one already opened is rolled back the same way. This pattern produces the
  same result as the `goto cleanup` idiom in the Error Handling Patterns lesson; this lesson does not teach
  that idiom, only measures its result.
- The class is **unobserved**: what is measured is not where a value reached, but
  which resource disappeared without ever reaching anywhere.
- Handlers registered with `atexit` run in the reverse of their registration order
  when `exit` is called; `_exit` closes the process without running any of them and
  without flushing buffered `stdio` output either.
- When a resource's ownership is handed off to another variable, the old path is set
  to `NULL` and the only access stays with the new owner — the class is **handoff**,
  different from unobserved in that the resource reaches exactly one reader.

#### Next Step

These five lessons measured the question "who sees this change" across four classes:
copy, shared, handoff, unobserved. The next topic takes up these classes not one
structure at a time but together — in composite cases where more than one data
structure is used together inside the same function, and figuring out which change
falls into which class has to be worked out by hand.

## Project Infrastructure

Build systems with a dependency graph, producing static and dynamic libraries, isolated unit tests and mocking, debugging with breakpoints and watchpoints, and how optimization levels preserve observable behavior.

### Build Systems

Source: https://academia.sh/en/courses/c-systems-programming/build-systems

Measures, by building the timestamp comparison in C itself, why a target whose dependency was never written still stays out of date under a rule-based build model.

The previous topic measured whom a resource is left with on exit — who ends up holding a
block once its owner is done with it. This lesson carries the same question from a source
file to a compiled program: when a header file changes, does that change actually reach
every target that depends on it? If it doesn't, the target keeps carrying a copy of its old
self, unaware of the newer version sitting in the hands of whoever wrote it.

A rule-based build system makes this decision with a single comparison: is the timestamp of
every input a target depends on newer than the timestamp of the target's own last
production? If so, the target is rebuilt. For this rule to work correctly, the target's
dependency list has to be **complete** — an entry that isn't in the list is never even
asked about, no matter how much it has changed.

This decision doesn't stop at a single target. A program is usually built from several
targets, and those targets can depend on one another: a source file can depend on a header,
and that header can depend on another header. A rule-based system follows this chain
**transitively** — a single change at the bottom of the chain puts every target in between
onto the rebuild list, one after another. If a target in the middle of the chain has an
incomplete dependency list, everything after it inherits that same gap; the gap doesn't stay
confined to one target, it reaches the whole subtree hanging off it.

#### The Timestamp and Dependency Decision

The model below holds a build target's state: the timestamp of when it was last produced,
and the version number embedded in it at that moment. The decision is gathered into a single
function.

```c
// target.h — a build target's production timestamp and dependency check
#ifndef TARGET_H
#define TARGET_H

struct Target {
    int build_time;
    int built_version;
};

/* check_dependency compares a header's timestamp against the target's last
   build time. If the header's timestamp is newer than the target's, the
   target is rebuilt and built_version is updated to header_version. If this
   call never happens at all — that is, if the dependency was never written
   into the list — the target keeps its old state unchanged. */
void check_dependency(struct Target *t, int header_time, int header_version);

#endif
```

The rule lives in exactly one place; a rebuild happens whenever the header's timestamp is
newer than the target's own.

```c
// target.c — the decision comes from a single rule: is the dependency newer than the target
#include "target.h"

void check_dependency(struct Target *t, int header_time, int header_version) {
    if (header_time > t->build_time) {
        t->built_version = header_version;
        t->build_time = header_time;
    }
}
```

Now two targets with the same starting state are compared. For the `written` target,
`check_dependency` is called with the header's **current** timestamp and version — the
dependency is present in the list, and the build system knows the header changed. For the
`unwritten` target, this call never happens at all, as if the header never appeared in that
target's dependency list in the first place. The header itself advanced to version 2 in
exactly the same way for both targets; the only difference is whether the build system
**knew** about it.

```c
// measurement.c — the written/unwritten decision and the three-link chain's decision in one run
#include <stdio.h>
#include "target.h"

int main(void) {
    struct Target written = { 10, 1 };     /* build_time=10, version=1 */
    struct Target unwritten = { 10, 1 };   /* same starting state */

    /* the header actually advanced to version 2, timestamp 20 */
    check_dependency(&written, 20, 2);
    /* this line is missing for unwritten -- the dependency never appears in its list */

    printf("written dependency built_version: %d\n", written.built_version);
    printf("unwritten dependency built_version: %d\n", unwritten.built_version);

    /* Three-link chain: base -> middle -> last. In the complete version, middle
       declares its dependency on base and updates its own record; last then
       inherits the updated record because it depends on middle. In the
       incomplete version, middle never declares its dependency on base, so
       even though last is properly linked to middle, there's no updated
       record for it to inherit. */
    struct Target mid_written = { 10, 1 };
    struct Target last_written = { 10, 1 };
    struct Target mid_unwritten = { 10, 1 };
    struct Target last_unwritten = { 10, 1 };

    check_dependency(&mid_written, 20, 2);
    check_dependency(&last_written, mid_written.build_time, mid_written.built_version);

    /* check_dependency is never called for mid_unwritten */
    check_dependency(&last_unwritten, mid_unwritten.build_time, mid_unwritten.built_version);

    printf("full chain last built_version: %d\n", last_written.built_version);
    printf("missing chain last built_version: %d\n", last_unwritten.built_version);
    return 0;
}
```

```text
written dependency built_version: 2
unwritten dependency built_version: 1
full chain last built_version: 2
missing chain last built_version: 1
```

**PI1.** Timestamps are represented here as plain integer counters, not the calendar value a
real filesystem would carry. The comparison's outcome doesn't change, because the rule only
ever looks at **ordering** — which of the two integers is greater.

The `written` target saw the header's new version, and its `built_version` field was
updated to 2. The `unwritten` target stayed at 1: the header genuinely changed, but that
relationship was never written into the build system's dependency list, so the comparison
was never even run. The `unwritten.built_version` field, entirely unaware of the new value
the writing side left behind, keeps carrying a **copy** of the old version. This one
comparison places the decisions of two different write patterns side by side: starting from
the same initial state, from the same real change, and diverging into two different
outcomes purely on whether that change was written into the dependency list.

**PI2.** The three-link chain is represented here with three separate `struct Target`
variables; in a real build system this relationship is held in a dependency graph, and the
chain's length isn't fixed. What the model measures — a missing link spreading its gap to
the rest of the chain — doesn't depend on the size of the graph.

`mid_written` and `last_written` carry the base's update forward in two steps: `mid_written`
declares that it depends on the base, and `last_written` declares that it depends on
`mid_written`. `mid_unwritten`, on the other hand, never declares its dependency on the
base, so its own record stays frozen at 1; even though `last_unwritten`'s dependency on
`mid_unwritten` is written **completely**, there's no new record left for it to inherit —
`last_unwritten.build_time` and `mid_unwritten.build_time` come out equal, so the comparison
`header_time > t->build_time` never holds in the first place. This makes concrete why a
single missing record in the middle of a chain affects not just its own target but the whole
subtree hanging off it: even though the last link wrote its own dependency correctly, it has
no way to compensate for the gap in the link before it.

In a real source tree the chain isn't limited to three links, and it isn't linear either: a
middle target can have dozens of leaf targets depending on it, and all of them inherit the
same gap at the same time. In the `mid_unwritten` example there was a single `last_unwritten`;
in a real tree this means every target depending on `mid_unwritten` stays out of date — the
effect of the gap doesn't show up in one target, it shows up in every target attached to it
at once. This spread makes finding a gap in the middle of a chain both more consequential and,
in a way, easier: seeing several targets stay stale at the same time in the same pattern is a
much more visible signature than a single target staying stale in isolation, and it usually
points debugging straight at the common ancestor — `mid_unwritten` itself.

Some build systems derive this list from a generator instead of writing it by hand: it scans
the source files itself, works out which header was included by which target, and produces
the dependency list automatically (model). What a generator-based approach buys is a lower
chance the list gets forgotten — the rule itself stays the same, only who writes the list
changes.

The difference between the two approaches is that a hand-written list is **static**, while a
generated list is **re-derived before every build**. A hand-written list stays incomplete if
it isn't updated when a source file gains a new header; a generator structurally rules this
forgetting out, because it looks at the source itself on every run and rebuilds the list from
scratch. The price is that the generator step itself now has to work correctly — if a
generator skips a header, the result falls into the same class as a hand-written incomplete
list, only the source of the gap is the generator rather than a person.

Broadly, a generator-based approach works like this: the compiler (or a separate tool that
accompanies it) already sees which headers were actually included while it processes a
source file — because the preprocessor stage opens every `#include` line anyway. Instead of
discarding this information, it's written to a separate record file: the source file's name
and the list of every header it's transitively dependent on. This record file is read before
the next build and folded into the build system's dependency list. The result is a list
derived from the source's own real content rather than written by hand; when a source file
gains a new header, the generator catches this change on its own at the next build and
updates the record file. In a small source tree a hand-written list might not cause any
trouble — a handful of files, a handful of headers, all visible at a glance. As the tree
grows, the requirement that every new `#include` line also be added to the list by hand
somewhere doesn't scale: forgetting to add it becomes easier than writing the code itself,
because updating the list doesn't happen with the same attention, at the same moment, as
changing the source file.

A generator-based approach carries its own cost: the record file is the output of the
previous build, so on the very first build — before any record file exists yet — the build
system has to start without a dependency list at all. This is why many rule-based systems run
in two passes: first they read whatever record files already exist (if any) and build the
dependency graph, then they order the actual build against that graph. If a record file is
missing or corrupt, the system stays on the safe side and rebuilds that target anyway — it
reads missing information as "unknown, so build it" rather than "unchanged." This is the
structural advantage a generator-based approach has over the risk of a hand-written list
staying silently incomplete: when something goes wrong, the usual result is an unnecessary
rebuild, not a silently skipped one.

#### The Bounding Measurement

This model only sees the dependency relationship **the build system knows about**, not
whether the header's content was actually read. As long as `check_dependency` isn't called,
the model produces the same result as if it assumed the header never changed at all — even
though the header genuinely has changed. What's being measured is the correctness of the
dependency **record**, not the correctness of the source itself. This distinction matters
because two different classes of failure shouldn't be confused: the record itself can be
broken (this lesson's subject), or the record can be correct while the compiler's use of that
record is broken — for instance, a case that falls under the previous module's staging
question. Both produce the same symptom in the end — "the target behaves more out of date
than expected" — but their roots are entirely separate, and so are their fixes: the first is
fixed by adding the missing line to the dependency list, the second by re-examining the stage
involved (preprocessor, compiler, or linker).

The previous module asked at which **stage** a change becomes visible: preprocessor,
compiler, linker, or runtime. The question here sits on a different axis entirely — a unit
whose header changed but that never got rebuilt is, from the staging question's point of
view, an ordinary preprocessor input; from the visibility question's point of view, it is a
copy, because the target never saw the change at all.

The independence of these two axes means clarifying which question is actually being asked
when debugging a failure. A target behaving more out of date than expected can come from two
different causes: either the source file never went through the right stage (the
preprocessor didn't expand a macro, the compiler reused a stale object file), or the source
file went through the right stages but the build system never rebuilt it at all. This model
only measures the second cause — which value reaches the target's `built_version` field, not
how that value gets processed afterward. A build system log is the most direct way to tell
these two situations apart: which targets got rebuilt and which were left **completely
untouched** is visible in the log, and an unexpectedly untouched target is a concrete symptom
of the class this lesson measures.

The chain example shows both axes at once. `last_unwritten` staying at its old version
traces back to a single missing record — `mid_unwritten` never declaring its dependency on
the base — but where that gap becomes visible is at the **end** of the chain. Someone
debugging `last_unwritten` first checks its own dependency list, finds it correct
(`last_unwritten` correctly declares its dependency on `mid_unwritten`), and doesn't look for
the problem there; the real gap sits one link earlier. This shows why debugging a transitive
chain doesn't end at looking at one target — the problem is found by tracing backward through
the chain itself, not from the visible end of it. In a large source tree this tracing can
involve too many links to follow by hand, which is why a generator-based approach's record
files also double as a diagnostic tool — a way to confirm which target is really tied to
which headers by looking at a file instead of relying on memory.

#### Summary

- A rule-based build system compares the timestamp of every input a target depends on
  against the target's own production time.
- When the dependency list is incomplete, a change that isn't in the list is never even
  asked about, and the target keeps its old output.
- The `unwritten` target's `built_version` field is a copy of the value the writing side
  left stale, entirely independent of the header's real version — the class this lesson
  measures is **copy**.
- A generator-based approach reduces the chance of forgetting by deriving the dependency
  list automatically; it doesn't change the rule itself.
- The visibility question is an axis independent of the previous module's staging question.
- A dependency chain is transitive: a missing record in the middle of the chain leaves the
  whole subtree hanging off it stale too — the fault should be looked for at the link
  holding the missing record, not at the visible end of the chain; because generator-based
  systems read missing information as "unknown" rather than "unchanged," they stay on the
  safe side and rebuild, so the margin for error leans toward unnecessary rebuilding rather
  than silently skipping one.

In practice there are a few typical reasons a dependency ends up left out of the list. When
a source file gets refactored and split into a new header, updating the old list is a
separate step, and it can be skipped. When a file moves from one directory to another, the
path in the list can keep pointing at the old location; the rule then either never matches
anything or tracks the wrong file. A new target created by copy-pasting an existing one also
copies its source's dependency list, which may not reflect the new target's actual
dependencies — headers specific to the new target never make it into the list at all. All
three cases arrive at the same outcome: the list carries a snapshot of a previous edit rather
than the source's real current state. What a generator-based approach closes is exactly this
gap — the list never needing to be maintained by hand as a structure **separate** from the
source at all.

#### Next Step

Even a correctly built target doesn't turn into a program on its own — it usually gets
**linked** with other units. That linking carries its own timestamp question: when a
library is updated, when does the program linked against it see that update — right away,
at the next rebuild, or never? The next lesson measures how two different ways of linking
against a library decide which version the program sees once that library is updated; it's
the linking-stage counterpart of this lesson's "was it written into the list or not"
question.

### Building Libraries

Source: https://academia.sh/en/courses/c-systems-programming/building-libraries

Measures how whether a library is bound into the program by embedding or read at load time decides which value the program sees when the library's version changes.

Once a target is correctly compiled into an object, it doesn't run on its own — most programs
link against functions and data that other units produce. This linking can happen in two
different ways: the library's current value is **copied** into the program's own body, or the
program reaches the library through a **pointer** and reads the current value every time. When
the library is later updated, these two programs see something different from each other.

The same distinction plays out between a variable that never opens beyond a single translation
unit and one that does. A `static` file-scope variable gives every translation unit its own
copy; a variable shared through an `extern` declaration has exactly one definition, and every
unit that uses it sees the same object.

This lesson brings back the previous lesson's **copy** class at a different layer. There, copy
was the stale value carried by a record that was never written into the dependency list; here,
copy is a library version taken at bind time and never updated again. Both produce the same
symptom — a value older than expected — but one arises in the build system's decision layer,
the other in the linking layer. Being able to tell these two layers apart is the first step in
narrowing down where to look when hunting a bug.

#### Embedded Version and Pointer-Based Access

The model below defines a library's version and two structures that bind to it through two
different paths: one copies the version into its own field at bind time, the other stores the
pointer leading to the library.

```c
// binding.h — version visibility of embedded and pointer-based binding
#ifndef BINDING_H
#define BINDING_H

struct Library {
    int version;
};

struct EmbeddedBinding {
    int embedded_version;   /* value copied at bind time, independent afterward */
};

struct PointerBinding {
    const struct Library *target;   /* not a value, but the path to the library is stored */
};

void embedded_bind(struct EmbeddedBinding *g, const struct Library *lib);
int embedded_version(const struct EmbeddedBinding *g);

void pointer_bind(struct PointerBinding *b, const struct Library *lib);
int pointer_version(const struct PointerBinding *b);

#endif
```

```c
// binding.c — embedded binding copies, pointer binding rereads on every read
#include "binding.h"

void embedded_bind(struct EmbeddedBinding *g, const struct Library *lib) {
    g->embedded_version = lib->version;
}

int embedded_version(const struct EmbeddedBinding *g) {
    return g->embedded_version;
}

void pointer_bind(struct PointerBinding *b, const struct Library *lib) {
    b->target = lib;
}

int pointer_version(const struct PointerBinding *b) {
    return b->target->version;
}
```

**PI3.** `embedded_bind` here stands in for the embedding a real program performs at compile
time: the library's current value behaves as if it were fixed before the program even starts
running. In a real build system this step happens during compilation; here it's modeled with a
copy operation at runtime, to make the timing visible.

These two structures correspond to two ways of binding a library in a real build environment.
`EmbeddedBinding` represents a **statically linked** library: the library's compiled code is
copied directly into the program's own executable, and the program never looks at the library
again. Even if the library is later updated and recompiled, the copy frozen into the statically
linked program's body doesn't change — unless the program itself is relinked, the updated
version never reaches it at all. `PointerBinding`, on the other hand, represents a **shared**
(dynamically) linked library: the executable doesn't carry the library's own code, it only holds
a path leading to it and goes to the library on every access. When the library is updated and
rebuilt, the program itself sees the updated version on its next run without ever being
recompiled — because the read is done from the library itself, not from a copy embedded in the
program's body.

**PI4.** The real-world difference between static and shared linking lies in load timing and in
the existence of a separate library file on disk; this model represents that timing difference
within a single runtime flow, by making the `embedded_bind` call happen before the library is
updated. The measured outcome — the statically linked one staying stale, the shared one seeing
the update — arises not from the timing itself but from the difference between copying and
holding a reference, so the model skipping the real loading mechanism doesn't change the result.

#### static and extern Scope

When a library is split across several translation units, the difference between counters
private to each unit's own file and a single counter shared by all units can be measured in the
same framework.

This contrast is set up **in the header**, not in the source file: declaring a variable as
`extern` in a header like `shared.h`, included by several source files, promises that the
variable will have exactly one definition and that every unit including it will see the **same
object**. Writing a `static` declaration into the same header couldn't make that promise —
`static` confines its scope directly to the translation unit it appears in; if it were written
into a header and included by every source file, each including unit would produce its own
**separate** copy, not a single shared object. So if sharing is wanted, the declaration must be
`extern` in the header and the definition must sit in exactly one source file; if keeping it
private to a unit is wanted, the declaration must stay `static` in the source file itself and
never move into a header at all.

Getting this distinction wrong has two different consequences. If a variable that needs to be
shared is mistakenly left `static` — for instance, if no `extern` declaration is ever added to
the header and every source file is left to define its own copy — the units keep separate
counters, unaware of one another, and a shared total like `shared_counter` never comes into
existence; each unit believes the partial count it sees is the correct one. The opposite mistake
is also possible: if a variable with the same name is defined directly, without `extern`, in
more than one source file — that is, if each source file tries on its own to define that name as
if it were a shared object — the linker finds the same name defined in more than one place, and
this usually ends in a link error. The `extern` declaration in the header prevents this second
mistake too, because it gives every unit that includes the header only a **declaration**, not a
**definition**; the definition stays in exactly one source file.

```c
// shared.h — the declaration that exposes the shared counter
#ifndef SHARED_H
#define SHARED_H

extern int shared_counter;    /* extern: one definition, every unit shares the same object */

void state_increment(void);
int state_count(void);

#endif
```

```c
// state.c — the static counter is private to this unit, the extern counter is shared
#include "shared.h"

static int unit_counter = 0;   /* static: a copy private to this translation unit */
int shared_counter = 0;         /* the extern declaration's one definition is here */

void state_increment(void) {
    unit_counter++;
    shared_counter++;
}

int state_count(void) {
    return unit_counter;
}
```

The unit containing `main` also keeps its own `static` counter; even though its name is the
same as the one in `state.c`, `static` scope makes it a separate object, and it doesn't break
compilation.

```c
// measurement.c — embedded/pointer binding and static/extern scope in a single run
#include <stdio.h>
#include "binding.h"
#include "shared.h"

static int unit_counter = 0;   /* private to this unit; separate from the same-named variable in state.c */

int main(void) {
    struct Library library = { 1 };
    struct EmbeddedBinding embedded;
    struct PointerBinding pointer_bound;

    embedded_bind(&embedded, &library);
    pointer_bind(&pointer_bound, &library);

    library.version = 2;   /* library moved on to version 2 after binding */

    printf("embedded binding version: %d\n", embedded_version(&embedded));
    printf("pointer binding version: %d\n", pointer_version(&pointer_bound));

    state_increment();
    state_increment();
    unit_counter++;
    shared_counter++;

    printf("state.c unit counter: %d\n", state_count());
    printf("main unit counter: %d\n", unit_counter);
    printf("shared counter: %d\n", shared_counter);
    return 0;
}
```

```text
embedded binding version: 1
pointer binding version: 2
state.c unit counter: 2
main unit counter: 1
shared counter: 3
```

The `embedded` structure stayed at the value of 1 it read at bind time — it's unaware that the
library later moved on to version 2, because what it holds is a **copy**. The `pointer_bound`
structure, on the other hand, read the current value because it stores the path to the same
library object; this is a model of the **shared** visibility that stands opposite embedded
binding — this lesson itself doesn't measure it, it's here only for comparison.

These two results mirror, one for one, the response static and shared linking give to a library
update. `embedded` models the behavior of a statically linked program: the library moved on to
version 2, but the program had already embedded a copy of version 1 into its own body, and that
copy never changed again — the only way for it to see the library's new version is for the
program itself to be rebuilt by **relinking** against the library. `pointer_bound` models the
behavior of a shared-linked program: the program itself was never recompiled, but because it
reads the path to the library and sees the library's **current** state on every read, it caught
the update automatically. This difference sums up the fundamental trade-off to weigh when
choosing between the two linking styles: static linking is predictable and immune to the library
changing out from under it, but it requires relinking to pick up an update; shared linking picks
up the update automatically, but the price of that automation is that when the library changes
is outside the program's own control.

The `static` counter in the `state.c` unit counted only its own calls and stayed at 2; the
same-named `static` counter in the `main` unit is entirely independent of it and carries its own
value of 1. `shared_counter`, on the other hand, is the single object both units increment, so
it reached 3, the sum of both.

The same `static`/`extern` distinction holds for functions too; this lesson only shows it
through a variable because the visibility of a copied value is more directly traceable there. If
a function is declared `static`, it can be called only from its own translation unit; another
`static` function with the same name can be defined in a different unit, and the two never
collide, because the linker doesn't see either of them as an externally visible symbol. This is
a cheap way of preventing helper function names from clashing in a large codebase: every helper
that needs to stay private to a unit is left `static`, and only functions that genuinely need to
be shared are moved into the header with an `extern` declaration (or without one, since
functions default to `extern` in C). If this choice isn't made early, undoing it later isn't
cheap: a function first spreads as shared, other units start calling it, and if it's then meant
to be closed back into its own unit, every place that calls it now has to change too — which is
why the visibility decision is made at the moment the library's interface is first drawn up, not
afterward.

#### The Bounding Measurement

This model doesn't measure how a library is actually loaded, only **which value** the two
binding styles reach. In a real environment, embedded binding is set up at compile time and
pointer-based binding at program startup; here both are modeled within a single runtime flow,
with their timing made visible. If the library's interface — that is, the order and types of the
fields inside `struct Library` — changes between the two sides, pointer-based binding reading
the current rather than the stale value is no longer a guarantee; this is a boundary that opens
onto undefined behavior, and it isn't run here.

The real-world consequence of static versus shared linking usually shows up in how a security or
bug fix is distributed. When a bug is fixed in one version of a library and the library is
rebuilt, a shared-linked program picks up that fix on its next run without being touched at all
— the fix ships together with the library. A statically linked program doesn't get the same fix;
the old code it embedded into its own body keeps running, and for the fix to reach it, the
program itself has to be relinked against the library and redistributed. This shows that the
choice between the two linking styles isn't only a matter of performance or file size, but also
a matter of maintenance and distribution responsibility: shared linking broadcasts an update
from a single central point (the library itself), while static linking requires every program's
own copy to be updated separately.

The previous lesson's question was whether a dependency was **written down** or not; this lesson
shows that even when a dependency is correctly written down, **how** it is bound to changes
visibility — the two questions complement each other, they don't stop at the same stage.

These two questions can offer two different explanations for the same bug. If a program
unexpectedly carries a stale value, the first thing checked is whether the dependency is written
into the build system's list; if the list is correct but the value still stays stale, the second
question comes into play — is the binding style embedded or pointer-based? An embeddedly bound
value can carry a stale copy even of a dependency correctly written into the list, because the
copying already happened at bind time, before the library changed. These two causes produce a
similar-looking result — in both cases the program sees a stale value — but their sources are
different and they call for different fixes: one wants the dependency list completed, the other
wants the binding style converted to pointer-based.

A practical way to tell these two causes apart is to change the library and rerun the program
**without recompiling** it. If the binding is embedded, the change in the library makes no
difference at all — the program still carries the old, embedded value, because the copying
finished at compile time and a later change to the library can never reach the copy in the
program's memory. If the binding is pointer-based, the same step reveals the new value right
away, because the program reaches the library again every time. This single step is different
from repeating the missing-from-the-list problem: in the missing-from-the-list problem the
library is never read correctly at all; in the binding-style problem the library is read
correctly, but the moment of reading falls at the wrong time.

A third consequence of static versus shared linking shows up when the library's interface
changes. If the field order or type of a structure like `struct Library` changes, a statically
linked program can stay unaware of the change because it embedded a copy of the old interface
into its own body; it doesn't notice the fault as long as it isn't recompiled, but when it is
recompiled, it errors out at compile time and the problem becomes visible. A shared-linked
program, on the other hand, can meet the interface change not at compile time but at runtime —
if the library is rebuilt with the new interface and the program's body is left unchanged, the
program expects the old interface while the library offers the new one, and this mismatch shows
up as a runtime error rather than a compile error. The name for this difference is **interface
compatibility**: in static linking, incompatibility stops early and loudly; in shared linking, it
can go on late and silently.

#### Summary

- Embedded binding copies the library's value at bind time; even if the library later
  changes, the program keeps carrying the old value — the class this lesson measures is
  **copy**.
- Pointer-based binding stores the path to the library and sees the current value on
  every read; this is a model of the shared visibility that stands opposite embedded
  binding.
- The `static`/`extern` contrast is set up in the header: if sharing is wanted, the
  declaration must be `extern` in the header and the definition must sit in exactly one
  source file; if keeping something private to a unit is wanted, the declaration must
  stay `static` in the source file without ever moving into a header — `static` gives
  every including unit its own copy, `extern` binds to a single shared object.
- If the library's interface disagrees between the two sides, pointer-based binding
  reading the current value safely is no longer guaranteed.
- Static linking requires the program to be relinked to pick up an update; shared
  linking distributes the update centrally from the library itself.

#### Next Step

Knowing that a library is correctly bound doesn't show that the code using it works correctly.
The next lesson looks at how testing a unit by **isolating** it from the rest of the program
measures whether a change leaks into another test.

### Testing Frameworks

Source: https://academia.sh/en/courses/c-systems-programming/testing-frameworks

Measures why a change made inside a forked child running a test never leaks into the next test, and why that isolation stops short of covering filesystem state.

Being correctly bound to a library doesn't show that the code using it works correctly.
Testing this itself carries a visibility problem: if a test sees the state left behind by
another test, you can no longer tell which test actually failed. A small test framework solves
this problem by running each test in its own copy.

#### The Test Table and Isolated Running

The core of a test framework is a table of function pointers, a single assertion macro, and a
pass/fail count. When an assertion fails, the test function returns 0; if it finishes with
nothing left to assert, it returns 1.

```c
// framework.h — declaration of the test table, assertion macro, and shared state
#ifndef FRAMEWORK_H
#define FRAMEWORK_H

typedef int (*TestFunction)(void);

struct TestEntry {
    const char *name;
    TestFunction run;
};

#define ASSERT(condition) do { if (!(condition)) return 0; } while (0)

extern int shared_state;            /* to observe leakage between tests */
extern int mock_call_count;         /* how many times the mock dependency was called */
extern int fixture_setup_count;     /* how many times the fixture was set up before a test */
extern int fixture_teardown_count;  /* how many times the fixture was torn down after a test */

int test_set_state(void);
int test_state_is_zero(void);
int test_see_fixture_value(void);
int test_write_to_file(void);
int test_read_from_file(void);
int mock_dependency(int input);
int stub_dependency(int input);
int do_real_work(int input, int (*dependency)(int));
int fixture_setup(void);
int fixture_teardown(void);

#endif
```

```c
// framework.c — test bodies, mock dependency, and two file-based tests
#include <stdio.h>
#include "framework.h"

int shared_state = 0;
int mock_call_count = 0;

int test_set_state(void) {
    shared_state = 9;
    return 1;
}

int test_state_is_zero(void) {
    ASSERT(shared_state == 0);
    return 1;
}

int test_see_fixture_value(void) {
    ASSERT(shared_state == 5);   /* fixture_setup must have brought this to 5 */
    return 1;
}

int test_write_to_file(void) {
    FILE *f = fopen("trace.dat", "w");
    if (!f) return 0;
    fputc('1', f);
    fclose(f);
    return 1;
}

int test_read_from_file(void) {
    FILE *f = fopen("trace.dat", "r");
    if (!f) return 0;
    int value = fgetc(f);
    fclose(f);
    return value == '1';
}

int mock_dependency(int input) {
    mock_call_count++;
    return input * 2;
}

int fixture_setup_count = 0;
int fixture_teardown_count = 0;

int stub_dependency(int input) {
    (void)input;              /* the stub's answer is fixed, it never reads the input at all */
    return 42;                 /* a predetermined answer that never changes */
}

int do_real_work(int input, int (*dependency)(int)) {
    return dependency(input) + 1;
}

int fixture_setup(void) {
    fixture_setup_count++;
    shared_state = 5;    /* the known starting state the fixture sets up */
    return 1;
}

int fixture_teardown(void) {
    fixture_teardown_count++;
    shared_state = 0;    /* leave the state clean for the next test */
    return 1;
}
```

**PI5.** The test table is shown with only two entries; the mechanism generalizes to any
number of entries, because running them in order is nothing more than a loop over the table.

The helper that runs each test in its own child uses the forking contract set up in the
previous module: the buffer is flushed, the child runs the test and reports the result through
its exit status, and the parent only reads that status.

**PI6.** The fixture is shown here with a single pair of functions — one setup, one teardown;
in a real framework the fixture can vary from test to test, but the requirement of being a pair
(one teardown for every setup) doesn't change.

If there is common preparation that has to run before and after the test itself, pulling that
preparation out into a separate **fixture** (a setup/teardown pair) instead of writing it into
the body of every test one by one keeps the same preparation from being written over and over,
and, more importantly, guarantees that the teardown always runs regardless of the test's
result. A fixture's contract is this: the setup runs exactly once before the test, the teardown
runs exactly once after the test — whether the test passes or fails, even if the test returns
early at an assertion. The `fixture_setup`/`fixture_teardown` pair below models this contract:
`fixture_setup` brings `shared_state` to a known starting value (5) and increments the setup
count; `fixture_teardown` resets the state and increments the teardown count. These two counts
are the way to verify that the fixture is really set up once and torn down once for every test.

A stub serves a different purpose than a mock, and this lesson keeps the two apart. A **stub**
(`stub_dependency`) returns a fixed, predetermined answer no matter what input it's given, and
never records that it was called at all — its only purpose is to stand in for the real
dependency so the rest of the test can run. A **mock** (`mock_dependency`), on top of that,
makes its own calling pattern observable; in this lesson it does that with a call count. The
distinction comes down to this: a stub prepares the test's **input**, a mock becomes part of
the test's **verification**. If a test only wants to see that `do_real_work` produces the right
number, a stub is enough and simpler; if the test also wants to verify how many times the
dependency was called, a mock is needed. Mixing the two up and using a mock everywhere piles an
unnecessary observation burden onto tests that never ask about call counts; using a stub
everywhere leaves blind the tests that need to verify calling behavior.

```c
// measurement.c — isolated running, unisolated leakage, and the filesystem boundary in one run
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "framework.h"

static int run_isolated(TestFunction func) {
    fflush(stdout);   /* otherwise the child would also carry the parent's unflushed buffer */
    pid_t pid = fork();
    if (pid == 0) {
        _exit(func() ? 0 : 1);
    }
    int status = 0;
    waitpid(pid, &status, 0);
    return WIFEXITED(status) && WEXITSTATUS(status) == 0;
}

/* run_with_fixture: setup and teardown in the parent, the test itself in an isolated child */
static int run_with_fixture(TestFunction func) {
    fixture_setup();
    int result = run_isolated(func);
    fixture_teardown();
    return result;
}

int main(void) {
    printf("isolated set state: %s\n",
           run_isolated(test_set_state) ? "passed" : "failed");
    printf("isolated state is zero: %s\n",
           run_isolated(test_state_is_zero) ? "passed" : "failed");

    /* the same two tests, this time called directly in the same process, unisolated */
    test_set_state();
    printf("unisolated state is zero: %s\n",
           test_state_is_zero() ? "passed" : "failed");

    int result = do_real_work(3, mock_dependency);
    printf("mock dependency call count: %d\n", mock_call_count);
    printf("do_real_work result: %d\n", result);

    int stub_result = do_real_work(3, stub_dependency);
    printf("do_real_work result with stub: %d\n", stub_result);

    printf("fixtured see-value test: %s\n",
           run_with_fixture(test_see_fixture_value) ? "passed" : "failed");
    printf("fixture setup count: %d\n", fixture_setup_count);
    printf("fixture teardown count: %d\n", fixture_teardown_count);
    printf("shared_state in parent after fixture: %d\n", shared_state);

    printf("isolated write to file: %s\n",
           run_isolated(test_write_to_file) ? "passed" : "failed");
    printf("isolated read from file: %s\n",
           run_isolated(test_read_from_file) ? "passed" : "failed");
    return 0;
}
```

```text
isolated set state: passed
isolated state is zero: passed
unisolated state is zero: failed
mock dependency call count: 1
do_real_work result: 7
do_real_work result with stub: 43
fixtured see-value test: passed
fixture setup count: 1
fixture teardown count: 1
shared_state in parent after fixture: 0
isolated write to file: passed
isolated read from file: passed
```

The first two lines show two tests that use the same `shared_state` variable running in
separate children: `test_set_state` sets `shared_state` to 9 in its own child, but when that
child exits, the change disappears along with it. When `test_state_is_zero` runs in its own
child, it inherits a copy of the parent's `shared_state`, still 0, and the assertion passes.
The third line runs the same two tests unisolated: both run in a single process, on the real
`shared_state`, and the 9 left behind by the first directly affects the second, so the
assertion fails — this is exactly the leakage that process isolation prevents.

The dangerous side of this leak is that the result depends on the **order** in the test file.
If the same two tests had been written with `test_state_is_zero` running first, the leak would
never show up, and both tests would pass even in their unisolated form — because
`shared_state` wouldn't have been pulled to 9 yet. This is the most deceptive property of an
unisolated test suite: the suite looks completely green in one order, and turns red in the same
suite once the tests are reordered or a new test steps in between, even though no test's own
logic has changed. The source of the fault is not in any single test but in the hidden
dependency between them — and that dependency stays invisible to code review, because the two
tests never call each other, they only share the same variable. The real payoff of process
isolation shows up here: because every test runs in its own child, the result stays the same
no matter what order the tests are written in — order stops being a variable that affects the
correctness of a test.

`mock_dependency` is a function pointer that stands in for the real dependency; `do_real_work`
calls it without changing its own logic, and the call count is measured through
`mock_call_count`. This count, just like `shared_state`, lives in a single process's memory —
if a test read it inside a forked child, the count that child sees would also be its own copy
and wouldn't make it back to the parent; the only thing that returns to the parent is whether
the test passed.

What the mock doesn't change here is `do_real_work`'s own logic: the function still calls the
pointer it's given, only the body that pointer points to has been swapped from the real
dependency for a body that keeps a count. This splits what the test measures in two — the
correctness of `do_real_work`'s own arithmetic on one side, and how many times and in what
order the dependency it calls was invoked on the other. The second question can't be tested
with the real dependency, because there's no way to observe the real dependency's own call
count from outside; the mock is the only tool that makes this observation possible.

The result obtained with `stub_dependency` is 43 — the 1 that `do_real_work` adds on top of
the stub, which returns a fixed 42 no matter the input. This line says nothing about the call
count, because the stub doesn't count anything to begin with; it only shows that
`do_real_work`'s own arithmetic correctly processes the value the dependency returns. Compared
against the result of 7 obtained with the mock, the two tests test the same function for two
different questions: one, "does it produce the right answer", the other, "does it call the
dependency the right number of times".

**PI7.** The `fixture_setup`/`fixture_teardown` pair here runs in the parent process,
immediately before and immediately after the call to `run_isolated`; the test itself still
runs in its own child, the fixture is not moved into the child.

The `run_with_fixture` line confirms the fixture's contract with three numbers. The setup
count is 1, the teardown count is 1 — that is, both ran exactly once, no more and no less. The
test itself passed the `shared_state == 5` assertion, because `fixture_setup` set this value in
the parent **before** the test's child, and the child inherited this value through the fork.
The last line shows that `shared_state` in the main process is back to 0 after
`fixture_teardown` runs — the fixture's teardown happens in the main process after the test's
child has already exited, because the setup and teardown never fork, they run directly in the
parent. This shows why the fixture has to stay **outside** the isolated run: the fixture is
common preparation repeated before and after every test, it doesn't need to be moved into a
separate child each time the way the test itself does — if it were moved, the effect of the
setup (shared_state being 5) could never reach the test's child at all, because every child
inherits only the state at the moment of its own fork.

#### The Bounding Measurement

The last two lines show where process isolation stops. `test_write_to_file` writes a value to
the `trace.dat` file in its own child, and the child exits; the memory state disappears along
with it. But `test_read_from_file`, even though it runs in an entirely separate child, can open
the same file and read the value the previous test wrote, and the assertion passes. Forking
only copies memory; the filesystem is an object that lives outside the process and isn't part
of the area fork isolates. If two tests touch the same file, they can affect each other even
though there is isolation between them at the memory level.

This boundary isn't limited to files; every shared resource that lives outside the process
carries the same problem. A network socket, a lock file, an environment variable kept by the
operating system itself, or a shared-memory region — none of these are inside the memory area
fork copies, so none of them get automatically isolated by forking. Knowing this distinction
while writing a test suite has a practical consequence: running tests in separate processes
doesn't **guarantee** that the tests are independent of each other — it only guarantees
independence at the memory level. Tests that touch a resource outside the process need an
extra precaution: either the resource itself is reset before every test, or the tests are
designed to never share the resource at all — for instance, by having every test generate its
own temporary file name.

Another side of the filesystem boundary is whether the test gets **cleaned up**. The
`trace.dat` file stays on disk after this run; on the next run, `test_write_to_file` writes
over a file that already exists, which doesn't change the result in this example because the
function reopens and rewrites the file every time, but if a different test used the same name
for another purpose, the content left over from the old run would be the first value the new
test reads. Memory doesn't have this problem, because every run starts its own process from
scratch and the previous run's memory is reclaimed by the operating system; the filesystem, on
the other hand, keeps sitting there unchanged even after the process has ended. This is why the
contract for tests that use files is one step broader than for tests that use memory: a clean
starting point has to be guaranteed not only between tests but also **between runs** — the
fixture can step in here too, by adding deleting the file to the teardown step.

This extended fixture could have been built by having `fixture_teardown` not only reset
`shared_state` but also call `remove("trace.dat")`; this step wasn't added here, because the
goal was to show that the filesystem falls outside fork, not how file cleanup should be done.
The general rule is this: a fixture only tears down the resource it itself set up — if
`fixture_setup` touches `shared_state`, then `fixture_teardown` only reverts that; if there
were another fixture that created a file, its teardown would likewise be limited to deleting
its own file. Changing, at the teardown stage, a resource the setup never touched widens the
fixture's contract and makes it harder to track which test is responsible for which resource.

#### Summary

- A test framework consists of a table of function pointers, an assertion macro, and a
  pass/fail count; each entry is a row in the table.
- Running each test by forking it into a separate child keeps the change the test leaves in
  memory from reaching the next test — the class this lesson measures is **copy**.
- When the same tests run unisolated, in a single process, the first one's change leaks into
  the second, and the run order can change the result.
- Turning a dependency into a mock through a function pointer measures the call count without
  changing the real dependency; a stub, without counting calls, prepares the test's input by
  returning a fixed answer — the two serve different purposes.
- A fixture separates setup and teardown from the test itself and guarantees the teardown
  always runs regardless of the test's result; it runs in the parent process, it isn't moved
  into the test's child.
- Process isolation cuts off memory, not the filesystem; two tests touching the same file
  aren't separated from each other by forking. This boundary isn't limited to files — every
  resource outside the process, such as a socket, a lock, or an environment variable, is
  equally outside isolation.

#### Next Step

When a test fails, you need to see what deviated from expectation, and at which step. The next
lesson looks at how stopping a running program and stepping through it makes the intermediate
values in its memory visible.

### Using a Debugger

Source: https://academia.sh/en/courses/c-systems-programming/using-a-debugger

Measures, by modeling breakpoints and watchpoints in C, why a breakpoint never sees an intermediate value between steps, and why a watchpoint sees that same value.

When a test fails, which input deviated from what was expected, and at which step, is
unknown — only the result is known. A debugger makes a running program's intermediate
values visible, either by stopping it or by watching it. But "making visible" doesn't
carry a single meaning: **what** a debugger sees depends on how it was told to stop.

The previous lesson showed that a test failed, but not why it failed. If an assertion
fails, the only thing known is that the condition was false; at which step the value
feeding that condition got that way never shows up in the test's own output. A debugger
fills that gap, either by stopping the program at a chosen point or by watching every
change to a variable. Even though both approaches observe the same running process,
which steps get seen depends on the strategy — and that difference directly determines
which value gets seen and which gets missed while hunting a bug.

This lesson compares three stopping strategies: one that stops by location (a
breakpoint), one that stops on every change (a watchpoint), and one that stops by a
state (a conditional breakpoint). All three observe the same running process, but each
was built to answer a different question — "what happens once this step is reached,"
"when does this value change," and "when does this state occur." A fourth tool, the
core dump, comes into play at a completely different time: while the process is no
longer running, it reads the state it left behind. What the four tools share is that
all of them answer the same underlying question — what was the program's internal
state at some moment — under different conditions.

#### Breakpoint and Watchpoint

The model below defines a function that writes three values into a variable across
three steps, in order. This function can be run with two different observation styles:
one stops only at selected steps and calls the observer there, the other stops at every
step.

```c
// step.h — three-step write sequence and two observation styles
#ifndef STEP_H
#define STEP_H

typedef void (*ObserverFunction)(int step, int value);

/* Writes 1, 2, 3 into a variable in order across three steps. At a step not
   in the breakpoints array, the observer is never called at all -- the
   program moves on past that step without stopping. */
void run_with_breakpoint(ObserverFunction notify, const int *breakpoints, int count);

/* The same three-step write sequence, but the observer is called at EVERY
   step -- there's no breakpoint list, because every write is itself a reason to stop. */
void run_with_watchpoint(ObserverFunction notify);

/* Instead of a step list, a condition is defined over the value itself: the
   observer is only called once the value reaches the threshold. Which step
   it stops at isn't known in advance -- only which condition it stops under is known. */
void run_with_conditional_breakpoint(ObserverFunction notify, int threshold);

#endif
```

```c
// step.c — breakpoint stops selectively, watchpoint stops on every write
#include "step.h"

static int is_at_breakpoint(int step, const int *points, int count) {
    for (int i = 0; i < count; i++) {
        if (points[i] == step) return 1;
    }
    return 0;
}

void run_with_breakpoint(ObserverFunction notify, const int *breakpoints, int count) {
    for (int step = 0; step < 3; step++) {
        int value = step + 1;   /* step 0 -> 1, step 1 -> 2, step 2 -> 3 */
        if (is_at_breakpoint(step, breakpoints, count)) {
            notify(step, value);
        }
    }
}

void run_with_watchpoint(ObserverFunction notify) {
    for (int step = 0; step < 3; step++) {
        int value = step + 1;
        notify(step, value);   /* stops on every write, no selection */
    }
}

void run_with_conditional_breakpoint(ObserverFunction notify, int threshold) {
    for (int step = 0; step < 3; step++) {
        int value = step + 1;
        if (value >= threshold) {   /* looks at the value itself, not the step number */
            notify(step, value);
        }
    }
}
```

**PI8.** Stepping is modeled here as a single loop's progression: each loop iteration
represents a step, and whether the observer gets called represents whether the program
stops at that step. A real debugger triggers step-by-step progression through a user's
command; here it's run within a single execution flow to make the timing visible.

**PI9.** The condition here is reduced to a single integer comparison (`value >=
threshold`); in a real debugger the condition can be an arbitrary boolean expression,
but the mechanism is the same — the condition is evaluated at every step, and it only
stops when it holds.

The difference between a breakpoint and a conditional breakpoint is **what is known in
advance**. An ordinary breakpoint stops at specific step numbers — which steps are of
interest must be known beforehand. A conditional breakpoint never looks at the step
number at all; it looks only at the value itself and stops whenever the condition
holds, regardless of which step that turns out to be. This is the difference between
"stop at the third step" and "stop once the value reaches 2" — the first looks at
location, the second at state. When the location isn't known in advance but what the
state should be is known (the moment a counter crosses a given threshold, say), a
conditional breakpoint does something an ordinary breakpoint cannot: even if there are
hundreds or thousands of steps, it stops exactly when the sought state occurs.

```c
// measurement.c — breakpoint skips 2, watchpoint sees 2
#include <stdio.h>
#include "step.h"

static int seen_count = 0;
static int seen_values[3];

static void record(int step, int value) {
    (void)step;
    seen_values[seen_count] = value;
    seen_count++;
}

int main(void) {
    int points[2] = { 0, 2 };   /* breakpoints at step 0 and step 2 */

    run_with_breakpoint(record, points, 2);
    printf("breakpoint seen count: %d\n", seen_count);
    printf("breakpoint first seen: %d\n", seen_values[0]);
    printf("breakpoint second seen: %d\n", seen_values[1]);

    seen_count = 0;
    run_with_watchpoint(record);
    printf("watchpoint seen count: %d\n", seen_count);
    printf("watchpoint second seen: %d\n", seen_values[1]);

    seen_count = 0;
    run_with_conditional_breakpoint(record, 2);   /* stops once the value reaches 2 */
    printf("conditional breakpoint seen count: %d\n", seen_count);
    printf("conditional breakpoint first seen: %d\n", seen_values[0]);
    printf("conditional breakpoint second seen: %d\n", seen_values[1]);
    return 0;
}
```

```text
breakpoint seen count: 2
breakpoint first seen: 1
breakpoint second seen: 3
watchpoint seen count: 3
watchpoint second seen: 2
conditional breakpoint seen count: 2
conditional breakpoint first seen: 2
conditional breakpoint second seen: 3
```

The breakpoints were set at step 0 and step 2; the program stopped at those two steps
and saw the values 1 and 3, in that order. At the step in between, step 1, the variable
held the value 2, but since there was no breakpoint there, the observer was never
called — the value 2 was never seen. When the same three-step sequence was run with the
watchpoint, the observer was called at every step: all three values were recorded, and
the second observation was exactly the 2 that the breakpoint had missed.

The last three lines show the conditional breakpoint's behavior. With the threshold
given as 2, the value at step 0 (1) stayed below the threshold, so the observer was
never called there; the values at steps 1 and 2 (2 and 3) reached or crossed the
threshold, so it stopped at both. Seen count 2, first seen 2, second seen 3 — a
different set from the ordinary breakpoint, which stopped at steps 0 and 2 and saw 1
and 3. Both mechanisms scan the same three-step sequence, but one selects by
**location** and the other by **state**; that's why they produce different observation
sets from the same sequence. This difference determines which mechanism to choose based
on what kind of information is known in advance: if the step being sought is known, a
breakpoint is enough and cheaper; if only a state is being sought (a value crossing a
given threshold, say) and which step it occurs at isn't known, a conditional breakpoint
is required.

A watchpoint and a conditional breakpoint look similar but aren't the same thing. A
watchpoint stops on **every** write and makes no selection — that's why it sees all
three values. A conditional breakpoint stops only when the condition holds; if the
threshold had been given as 4 instead of 2, it would never have stopped at all, because
the largest value in the sequence is 3 and none of them reach 4. A watchpoint
guarantees "never miss anything," and it pays for that guarantee by stopping at every
step; a conditional breakpoint gives no such guarantee — it only never misses the state
its condition defines, and it skips everything outside that condition just like an
ordinary breakpoint. The choice among the three depends on whether what's being sought
is a **location**, any **change**, or a specific **state**.

#### The Bounding Measurement

When a program terminates unexpectedly, a dump of its memory state at that moment can
be examined afterward — to look at the stack and variable values at the moment of the
crash, without running the program again. This dump isn't produced here: actually
crashing a process is outside this lesson's scope, only what it's used for is
explained. A breakpoint and a watchpoint stop and observe a **running** process; a dump
instead reads the state a **stopped** process left behind, after the process no longer
exists — the two answer the same visibility question at two different times.

A core dump's real value is for the moments when the observer **isn't ready**. All
three of a breakpoint, a watchpoint, and a conditional breakpoint require the person
hunting the bug to have started the program under a debugger beforehand, or to attach
to it afterward — when and where the program will stop must be predicted, to some
degree, in advance. If a process running in production, with no one watching it,
crashes at an unexpected moment, recreating that moment is usually impossible; the
input, the timing, or the system state may never repeat in exactly the same way again.
At such a crash, the operating system can write the process's memory image at that
moment — the stack, the heap, and the register values — to a file. That file can later
be handed to a debugger, once the process is no longer running, and the variable
values, the call chain, and the state at the moment of the crash can be examined as
though the program were still standing at that moment — except it can no longer be
stepped forward, because the dump is nothing but a single frozen frame, not a running
process.

This exposes the fundamental difference separating a dump from breakpoints and
watchpoints: breakpoints and watchpoints require **an observation set up in advance**,
while a dump leaves behind a record **even if no observation was ever set up at all**.
If a bug can be reproduced and where to stop is known, a breakpoint or a watchpoint is
preferred, because it allows stepping forward over a live process. If the bug happens
only once, at an unpredictable moment, and while the observer isn't ready, the only
tool left is a dump — reading a frozen copy of that moment afterward, without needing
to recreate the crash itself.

This model has a limit of its own: the value observed here was set up to stay exactly
identical to the value written in the source code itself; in an optimized program, the
value a debugger sees may not be the same as the one in the source — a variable may be
held only in a register, or removed entirely.

Although the difference between a breakpoint and a watchpoint appears, in this lesson's
model, to be bounded by the length of a single sequence, in a real program it can stay
hidden across a much wider range. In a three-step sequence, noticing that a single
value in between got missed is relatively easy; in a loop running through hundreds of
steps, if a breakpoint is only set at the entry and the exit, a faulty intermediate
value in between can go completely unseen for the whole run. A watchpoint stopping on
every write removes this risk, but it has a cost: if a variable is written frequently,
the program runs far slower because it stops on every write. The choice between a
breakpoint and a watchpoint is therefore not just a question of "which one sees more,"
but "how well is it known in advance which step needs to be seen" — if it isn't known
at all, a watchpoint's slowness is preferred over a breakpoint's risk of missing
something.

The three stopping strategies can also be used one after another in a single bug hunt.
Suppose a total variable drops below zero at an unexpected point. The first step is
usually a watchpoint: it stops every time the variable is written, and after a few runs
it becomes clear in which range the sign flips. As the range narrows, the watchpoint's
cost — the slowdown from stopping on every write — is no longer needed; a fixed
breakpoint is placed at the start of that range, and the program is stopped and stepped
through only there. If the value turning negative repeats not under a single condition
but under several different conditions depending on the input, the breakpoint gives way
to a conditional breakpoint: the stop is now defined not as "once this line is reached"
but as "once the total drops below zero," independent of which input triggers it. Using
these three tools in sequence isn't arbitrary — each step narrows the information the
previous one found (an approximate location, then an exact location, then the state
itself) and hands it off to the next. If the bug occurred in production, only once, at
a moment no one was watching, this sequential hunt never even begins; the only thing
left is a core dump, and then the question isn't "where should it stop" but "what
record was left behind."

#### Summary

- A breakpoint stops only at selected steps; an intermediate value passing between two
  breakpoints is never observed at all — the class this lesson measures is
  **unobserved**.
- A watchpoint stops on every write; it also sees the intermediate value a breakpoint
  missed — this is a contrast model.
- A conditional breakpoint looks at the value's own state, not at the step number; when
  the location isn't known but the state being sought can be defined, it does what a
  breakpoint cannot.
- A core dump doesn't stop a running process; it reads the state of a terminated
  process afterward, and it leaves behind a record even if no observation was ever set
  up in advance.
- In this model, the observed value was set up to match the source value; in an
  optimized program, this match isn't guaranteed.
- Which stopping strategy to choose depends on whether what's being sought is a
  location, any change, or a specific state; a real hunt uses all three in sequence —
  a range roughly narrowed with a watchpoint is pinned down precisely with a
  breakpoint, and a recurring, condition-dependent state is caught with a conditional
  breakpoint.

Both a breakpoint and a watchpoint work by **stopping** the program: it runs at its
normal speed until it stops, and at the moment it stops, its memory state becomes
readable as if frozen. This is a second axis separating the two methods from a core
dump — a dump is a single frame taken while the process itself is no longer running,
while a breakpoint or a watchpoint can show many frames, one after another, over a
running process. Which one is more suitable when hunting a bug depends on whether the
bug shows up **at a single moment** or **across a sequence of steps**: a single crash
is examined with a dump, while a drift accumulating across steps is examined with
stopping points.

#### Next Step

The value a variable holds at runtime doesn't always stay identical to the value it's
assigned in the source — the next lesson takes up where that difference comes from.

### Optimization Levels

Source: https://academia.sh/en/courses/c-systems-programming/optimization-levels

Uses the model of an optimizer deleting an unread write to cover the as-if rule, the guarantee volatile provides, and a closing tally of the course's fifteen lessons.

The previous lesson closed by noting that the value a debugger sees doesn't always match
what the source wrote: a variable may live in a register, or may have been removed entirely.
This lesson takes up where that difference comes from. An optimizer can remove a write that
doesn't change observable behavior — a write it removes never reaches any reader. The course
has measured this with a single question throughout: who sees this change? For optimization,
the answer reaches **unobserved** by a path different from the other fourteen changes: there
is no copy here and no intermediary. The write happens, but before any read reaches it,
another write lands on top of it.

#### Write Overwritten Before Any Read

Think of a flow in three steps: a location is written with `1`, the same location is
written with `2`, and then that location is read somewhere. If the first write's effect was
never read before the second write arrived, then by the time the second write lands, every
trace of the first write in that location is gone. No party observing the program's
behavior — not output written to a file, not a value sent to another process, not shared
state protected by a lock — can tell whether the first write ever happened at all. The
program could have skipped the first write entirely and the observed result would not
change.

**PI10.** An optimizer model can remove the first of two consecutive writes to the same
location once it detects that no read occurred between them; whether it removes it or not,
the observable result is the same.

The measurement uses the same record format as the course's core:

```c
// observe.h — a record that classifies whom a change reaches
#ifndef OBSERVE_H
#define OBSERVE_H

enum Class { COPY, SHARED, HANDOFF, UNOBSERVED };

/* Records one change: seen is whether the side that did not make the
   change can read it (1 or 0). */
void observe(const char *change, enum Class cls, int seen);

/* Prints the count per class and the reached / not reached totals. */
void observe_summary(void);

#endif
```

```c
// observe.c — the counts live only in this unit
#include <stdio.h>
#include "observe.h"

static const char *const names[] = { "copy", "shared", "handoff", "unobserved" };
static int counts[4];
static int reached = 0;
static int not_reached = 0;

void observe(const char *change, enum Class cls, int seen) {
    /* shared and handoff reach, copy and unobserved do not */
    int expected = (cls == SHARED || cls == HANDOFF);
    counts[cls]++;
    if (seen) {
        reached++;
    } else {
        not_reached++;
    }
    printf("%-34s %-8s %s%s\n", change, seen ? "saw" : "missed",
           names[cls], seen == expected ? "" : "  MISMATCH");
}

void observe_summary(void) {
    printf("classes: copy %d, shared %d, handoff %d, unobserved %d\n",
           counts[COPY], counts[SHARED], counts[HANDOFF], counts[UNOBSERVED]);
    printf("reached %d, not reached %d\n", reached, not_reached);
}
```

The excerpt below is taken from the course's core, and it's this lesson's own excerpt — its
names and body do not change across the course:

```c
// optimization_measurement.c — the optimization excerpt from who_sees.c, unchanged
#include <stdio.h>
#include "observe.h"

struct Step {
    char op;
    int value;
};
static void optimization(void) {
    struct Step flow[] = { { 'w', 1 }, { 'w', 2 }, { 'r', 0 } };
    int readers = 0;
    int last_write = -1;
    for (int i = 0; i < 3; i++) {
        if (flow[i].op == 'w') {
            last_write = i;
        } else if (last_write == 0) {
            readers++;
        }
    }
    observe("write overwritten before any read", UNOBSERVED, readers > 0);
}

int main(void) {
    optimization();
    observe_summary();
    return 0;
}
```

```text
write overwritten before any read  missed   unobserved
classes: copy 0, shared 0, handoff 0, unobserved 1
reached 0, not reached 1
```

The `flow` array carries three steps: `'w'` is a write, `'r'` is a read. The loop tracks,
at each read step, which write was most recently in effect (`last_write`). When the read
arrives, if `last_write` still points at the **zeroth** write (`last_write == 0`), that read
counts as having seen the first write's value, and `readers` increases. In this model
`flow` holds, in order, `w=1`, `w=2`, `r`: the second step overwrites the first by moving
`last_write` to `1`; by the time the read at the third step arrives, `last_write` is no
longer `0`, it's `1`. No read ever sees the first write — `readers` stays at zero. The call
to `observe` records this with class `UNOBSERVED` and passes `readers > 0` (that is, `0`,
false) as the `seen` field.

This is the model for why an optimizer **can** delete an unread write: even if the first
write is never made at all, the `last_write` sequence — and therefore the `readers` count —
stays the same. If the program contained a single write instead of two — writing `2`
directly — the observable result (what the third step reads) would not change. The model
places `write overwritten before any read` in the `unobserved` class for exactly this
reason: a change was made, but it never reached any reader.

#### The As-if Rule

An optimizer's right to delete a write isn't arbitrary; it rests on a single condition: the
**as-if rule**. This rule defines a program's run-time behavior in terms of a set of
**observable behavior**, and as long as the optimizer preserves that set, it may change how
the program executes step by step in any way it likes — it only has to look as if it ran in
source order, not actually run that way. Observable behavior is bounded by three things: the
order and content of input/output operations, accesses to objects marked `volatile`, and the
program's termination state. In the model above, the first write falls under none of these:
it isn't written to a file, it isn't `volatile`, and it doesn't affect how the program
terminates. So removing it does not violate the as-if rule.

**PI11.** A write can only be overwritten before being read if it falls into **none** of
the parts of observable behavior; the moment it falls into one, that right disappears.

The as-if rule doesn't conflict with the core's other fourteen measurements — it completes
them. The variable the child changes after `fork` (copy class) isn't subject to the as-if
rule at all — it's about the existence of two separate memories, not about write order
within a single process. The flag the handler raises (handoff class) enters the as-if rule
from the opposite direction: because it's marked `volatile sig_atomic_t`, the optimizer can
never touch that write — the next section works through this in detail.

#### The Guarantee Volatile Provides

When the `volatile` keyword is applied to a variable, it tells the compiler that every read
and write to that variable counts as part of observable behavior. The consequence is direct:
a `volatile`-marked write cannot be deleted even if no read follows it; a `volatile`-marked
read cannot be skipped even if its result is used nowhere. The `flag` variable in the
signals lesson was declared `volatile sig_atomic_t` for exactly this reason: if the
optimizer could delete the write the handler makes to that flag on the grounds that "nobody
is reading it yet," before the main flow reads it, the handoff would never happen at all.

In the model's own terms: if the position in the `flow` array were `volatile`, the first
write would still be overwritten before being read, but it could not be deleted, because the
write itself would count as part of observable behavior. This doesn't change the record's
class — by the time the second write arrives, no read has still seen the first write's
value, and the record is still `unobserved`. What changes is only the optimizer's right: the
write stays in the program; it just never reaches a reader. This lesson doesn't measure
that with separate code; `volatile`'s effect shows up in the code an optimizer produces, and
a model in source text cannot print that code. This lesson's `main` function calls
`optimization` and then `observe_summary`; the output shows the one record marked `missed`
falling into the `unobserved` class.

The guarantee `volatile` provides is narrow, and it should not be stretched further. On its
own it does not provide ordering between threads, does not provide atomicity, and does not
establish a memory barrier — the lock and `join` from the POSIX Threads lesson are still
required. All `volatile` gives is the guarantee that accesses to that object will not be
deleted by the optimizer and that their order will be preserved; this is exactly why the
only form of sharing between a signal handler and the main flow is `volatile sig_atomic_t`.

#### The Bounding Measurement

The model decides whether a write can be deleted by looking at a single rule — whether
observable behavior is preserved. A real optimizer makes this decision with much broader
information: whether a variable's address has been taken, whether it's visible from another
translation unit, whether it could be aliased through a pointer. This lesson does not write
an optimization level as a flag, and does not print compiler output, generated code, or a
measurement's duration — because none of these can be shown by a model in source text; they
are a real tool's real decision. The model shows only the rule itself: which writes can be
deleted, and which cannot. If a write's address is held by another function, that function
can read the location when called back; in that case the write is now part of observable
behavior and cannot be silently deleted the way the model shows. The model not showing this
is not a gap, it's a boundary: the excerpt only handles consecutive writes within a single
flow, not cross-function visibility.

#### Summary

- `write overwritten before any read` (model): if two consecutive writes land on one
  location and the first is never read before the second arrives, no trace of the first
  remains.
- The as-if rule says an optimizer may delete a write only if doing so will not change
  observable behavior; observable behavior is bounded by input/output, `volatile` accesses,
  and termination state.
- `volatile` counts every access to an object as part of observable behavior; that's why a
  `volatile`-marked write is never deleted, even if it's never read.
- The `volatile sig_atomic_t` from the signals lesson relied on exactly this guarantee: the
  flag a handler raises cannot be deleted by the optimizer even while the main flow has not
  yet read it.
- The class this lesson measures is **unobserved**: a write happens but never reaches any
  reader — it doesn't stay on the writer's own side (copy), and it doesn't pass through an
  intermediary and get seen once (handoff); it gets overwritten and disappears.
- The model shows not a real optimizer's decision but the rule that decision rests on;
  address-taking and cross-function visibility are outside the model's scope.

#### Course Wrap-Up

Across fifteen lessons, this course asked a single question: **who sees this change?** This
lesson answers that question for the last time, and the answer reaches unobserved by a path
different from the other fourteen — there's a write, no reader, because the next write
arrived before any read ever did. The table below collects all fifteen changes:

| Lesson | Measured change | Who sees it | Bounding measurement |
|---|---|---|---|
| File Descriptors | The offset shared by a descriptor duplicated with `dup` | shared | The descriptor number does not say what is shared; the sharing is measured only through offset behavior |
| Process Management | The variable the child changes after `fork` | copy | The measurement does not depend on timing; it does not measure a race, it measures that the two copies are independent |
| Signals | The flag the handler raises | handoff | The same signal arriving again while blocked is delivered only once when unblocked |
| Inter-Process Communication | The value written to the pipe | handoff | A pipe is a stream, it carries no message boundary; a boundary is established with a separate length prefix |
| POSIX Threads | The counter the thread writes under the lock | shared | Only the final value is verified; intermediate states and write order are not measured |
| Linked Structures | The node changed through an alias | shared | A pointer's sharing is not visible at the source; whether two heads point at the same node is measured by equality |
| Ring Buffers and Queues | The item put in the ring | handoff | The read function's signature doesn't say whether the value stays in the structure; this is measured with two reads in a row |
| Hash Tables | The key changed after insertion | copy | The insert function's signature doesn't say whether the key is copied; if the key were held by pointer, the class would be shared |
| Object-Like Design | A deposit made by one user | shared | The shape of the interface (opaque type, function pointers, plain functions) does not determine sharing; only pointer equality does |
| Resource Cleanup Idioms | The buffer exited without being flushed | unobserved | Unobserved means reaching nowhere; this is verified with `== NULL` checks, without printing the value |
| Build Systems | The header with a dependency that was never written | copy | The model only sees the dependency that was written; it has no way of knowing on its own which header is actually read |
| Building Libraries | An update to a statically linked library | copy | The model only measures which value is reached; if the interface changes, the pointer-based guarantee is lost, and that case is not run |
| Testing Frameworks | The change made by an isolated test | copy | Process isolation cuts off memory change, not filesystem change |
| Using a Debugger | The intermediate value between two stops | unobserved | A breakpoint and a watchpoint observe a running process; a core dump reads one that has already stopped; no dump is produced |
| Optimization Levels | A write overwritten before any read | unobserved | The model only handles consecutive writes within a single flow; address-taking is out of scope |

**5 copy · 4 shared · 3 handoff · 3 unobserved.**

This distribution matches the distribution of the core's fifteen observations exactly — not
an unexpected coincidence, because the core carries exactly one observation per lesson, and
each lesson takes its own excerpt from that observation. Seven changes reached the other
side, eight did not; the ones that reached passed through an intermediary (handoff) or a
shared object (shared); the ones that didn't either stayed on the writer's own side (copy)
or never landed anywhere at all (unobserved). This distinction held from the start of the
course: a change **having been made** does not guarantee that it was **seen**.

The same question was asked again at different levels throughout the course — between two
processes' memory, between a handler and the main flow, in the memory two threads share, in
the trace one build unit leaves for another, and finally in a single write an optimizer
deletes. At every level, the answer fell into one of the same four classes; the number of
classes never changed, only which mechanism fell into which class changed.

The next course (C++ Fundamentals and Object Design) covers classes and resource
management; it will establish its own measurement axis in its own first lesson.

