# The C library compatibility layer

A design note and milestone plan for **libdanos-c** — the mini C library that lets
`zig cc` cross-compile C programs for danos. It is milestone **P0** of
[python-on-danos-milestones.md](python-on-danos-milestones.md), expanded here the
way [character-devices-and-tty.md](character-devices-and-tty.md) expands P1.
CPython is the driving consumer, but the layer is general: any portable C program
within its surface should build.

## What it is — and the three things it is not

The deliverable is a **sysroot**: a set of C headers plus a static `libdanos-c.a`,
handed to `zig cc -target x86_64-freestanding-none` via `-isystem` and linked into
every C binary. Three explicit non-goals keep it small:

- **Not a musl port.** Whole-musl assumes Linux syscall semantics at its bottom
  (the door the Zig roadmap deferred, twice now). We *lift* musl's pure-computation
  source files and *write* a danos-native bottom — see the layer split below.
- **Not full POSIX — *yet*.** Stage 1's surface is "what CPython's minimal
  configuration and ordinary portable C need" — roughly 100–150 functions — and
  at that stage absence is a *feature*: configure scripts probe and adapt, and a
  linker error is honest. But the end state is a **full C compatibility layer**
  (see "The road to full coverage" below); the absence table is a schedule of
  arrivals, not a wall.
- **Not a second runtime.** The library is a thin C-ABI re-spelling of the same
  danos-native surface `runtime` already provides. It contains no policy of its
  own; when the Zig track's `runtime.os` seam is authored, the libc bottom
  re-targets it near-mechanically — the fourth appearance of the roadmap's "same
  surface" symmetry.

One scoping rule sits above all three — the **size doctrine**: this layer serves
**applications only**. The kernel and the system services never link libdanos-c;
they stay danos-native Zig over `runtime`, small and static, because leanness is
an operating-system property. Applications have their own budget and may be as
big as they need to be. The libc is how big software *lands on* danos, never how
danos itself is built.

## The layer split: lift the mathematics, write the plumbing

The realization that makes 100–150 functions tractable: a libc is two very
different kinds of code, and the hard kind is portable.

| Layer | Contents | Source |
|-------|----------|--------|
| **Pure computation** | `string.h`/`memcpy` family, all of libm, `strtod`/`dtoa`, `strtol`, `qsort`, `ctype` tables, `gmtime` calendar math, the `printf`/`scanf` engines, `setjmp` (a dozen instructions of x86-64 asm) | **Lift from musl**, vendored under `library/c/third-party/musl/` (MIT; files compile standalone) |
| **OS plumbing** | fds (`open`/`read`/`write`/`close`/`lseek`/`stat`/`getcwd`/`chdir`/`isatty`), `mmap`/`munmap`, clocks, `exit`, `getenv`, `getentropy` | **Write in Zig**, exporting C ABI over the `runtime` syscall + VFS client surface |
| **The middle** | `malloc` over danos `mmap` (simple free-list; CPython's arenas sit above), `FILE*` buffering, `errno` | **Write in Zig** (small, danos-shaped) |
| **Entry** | `crt0`: the existing danos entry shim ([sysv.md](os-development/sysv.md)) bridged to C `main(argc, argv, envp)`, `environ` initialised, `exit` flushing stdio | **Write** |

Two liftings deserve their own line because getting them wrong is silent
corruption rather than a linker error:

- **`strtod`/float formatting.** Python's float `repr` guarantees shortest
  round-trip; that property lives entirely in these routines. musl's are correct;
  an improvised one would be subtly wrong for years. Lift, never write.
- **The stdio engines.** musl's `vfprintf`/`vfscanf` are self-contained around
  its `FILE` abstraction (function-pointer read/write slots), so the whole
  formatted-I/O engine lifts too — we implement only the fd-backed slots
  (`__stdio_write`-shaped) and the buffering glue.

## Header policy

Hand-write the headers as danos's own minimal set rather than importing musl's
(musl's are entangled with Linux ABI details), borrowing declarations freely.
Freestanding compiler headers (`stdint.h`, `stddef.h`, `stdarg.h`, `stdbool.h`,
`float.h`, `limits.h`) come from clang via `zig cc` — do not duplicate them.
`errno.h` values are the danos errno enum re-spelled with POSIX names; there is no
Linux numbering to be compatible with, so the enum is the truth.

Deliberate absences, and their planned arrivals — this table is the
compatibility matrix, and "the road to full coverage" below is the schedule
that empties it:

| Absent | Arrives with |
|--------|--------------|
| `pthread.h` | the post-P5 pthread subset over `thread_spawn`/futex — but see the risk below |
| real `signal.h` (beyond no-op `signal()`/`raise` stubs) | M17 signals-over-IPC in the libc |
| `dlfcn.h` | [dynamic-libraries.md](dynamic-libraries.md) D1 |
| `fork`/`exec*`/`wait*` | P5 exposes danos spawn as `posix_spawn`; `fork` itself never (see below) |
| `socket.h` | a future networking track |
| locale beyond `"C"` | stage 3 evaluation (CPython is UTF-8-mode happy without it) |
| pipes (`pipe()`) | P5 process-control cluster |

## The road to full coverage

The layer grows in three stages; only stage 1 is a current milestone (P0), but
the stages exist so stage-1 decisions never have to be unmade:

- **Stage 1 — CPython-minimal** (P0, the slicing below): ~100–150 functions,
  static-only, absences honest.
- **Stage 2 — the danos-complete layer**: the full hosted C11 standard library,
  plus every POSIX facility danos semantics support, landing as its enabling
  milestone lands — pipes and `posix_spawn` at P5, real signals at M17, the
  pthread subset after P5, `dlfcn.h` at
  [dynamic-libraries](dynamic-libraries.md) D1, sockets with networking. Stage 2
  is not one milestone but the standing rule that **every system capability
  gets its C spelling when it ships**, so the matrix above drains as the OS
  grows.
- **Stage 3 — ecosystem grade**: the point where "portable C program" generally
  means "builds on danos" (autotools-style probing included). Reaching it is
  mostly stage 2 compounding, plus the long tail (locale, wide-char,
  `fnmatch`/`glob`/`regex` — the last three lift from musl like the rest). At
  this stage, re-evaluate hand-grown-vs-musl-port once with real data; the
  standing recommendation remains danos-native — musl's bottom assumes Linux
  syscall semantics, and by stage 3 the danos bottom exists and is tested —
  with musl continuing as the quarry for computation code.

Two boundaries are permanent and worth stating at every stage: **`fork` never
comes** — danos is a spawn-shaped OS, and `fork`'s address-space-duplication
semantics are hostile to everything from capabilities to threads; software that
hard-requires `fork` (not `posix_spawn`) stays off the platform. And the
**public ABI stays the vDSO + IPC protocols** — a full libc is a compatibility
*layer*, not a second stable system ABI.

## Milestone slicing

1. **sysroot-skeleton** — layout under `library/c/` (a build package:
   `include/`, Zig sources, vendored musl subtree); `crt0`; string/mem +
   `ctype` lifted; a `build.zig` step making C binaries first-class targets.
   *Test:* a C program using only computation links and runs in QEMU
   (`c-hello` printing via a raw `write` extern to `debug_write`).
2. **fd-plumbing** — `errno`; open/read/write/close/lseek/stat/unlink/mkdir/
   rename over the `runtime` VFS client; `getcwd`/`chdir`/`getenv`/
   `getentropy` arriving as P1 lands them (stubbed truthfully until then:
   `getenv` empty, `getentropy` `ENOSYS`). *Test:* QEMU `c-file-io` — create,
   write, reopen, read back, stat size + mtime through FAT.
3. **malloc** — free-list allocator over danos `mmap`; `calloc`/`realloc`/
   `free`; alignment guarantees documented. *Test:* host + QEMU allocator
   torture (interleaved sizes, realloc growth, alignment asserts).
4. **stdio** — `FILE*`, buffering modes, the lifted printf/scanf engines wired
   to the fd slots; `snprintf` family; stdin/stdout/stderr over fd 0/1/2.
   *Test:* host round-trip suite for format engines (especially `%.17g`
   float round-trip); QEMU `c-stdio` cooked-line echo once P1's console exists.
5. **mathematics-and-time** — libm lifted wholesale; `strtod`/`strtol`;
   `clock_gettime` (monotonic + realtime over `clock`/`wall_clock`);
   `gmtime`/`mktime`/`strftime` (UTC only — no timezone database);
   `setjmp`/`longjmp`; `qsort`/`bsearch`; `abort`/`assert`. *Test:* host
   `strtod`/`dtoa` vectors against known-hard cases; QEMU `c-time` sanity
   against the wall clock.

Slices 1, 3, 4-host, and 5-host have **no dependency on P1** and can start
immediately; slice 2 and the QEMU halves interleave with P1 as it lands.

**Exit for the layer as a whole** (= P0's exit): `c-hello` and `c-file-io` green
in the QEMU suite, and the host-side computation tests green — at which point P2
(CPython configure) becomes the layer's real integration test.

## Testing strategy: two targets, on purpose

The computation layer is target-independent, so it is unit-tested **on the host**
(built for the host triple, compared against the host libc's answers —
thousands of cheap oracle checks for `strtod`, `printf`, libm edge cases). The
plumbing layer only means anything **on danos**, so it is tested in the QEMU
suite like every other subsystem. Keeping the split explicit stops the slow-QEMU
suite from absorbing tests that a host `zig test` runs in milliseconds.

## Risks and gotchas

- **CPython's configure may insist on pthreads.** WASI-class targets build
  threadless, but verify this *first* in P2 bring-up; the fallback is a
  truthfully-single-threaded `pthread.h` stub set (create returns `EAGAIN`,
  mutexes are no-ops — valid when only one thread can exist). Decide from
  evidence, not assumption.
- **`long double` is x87 80-bit on x86-64.** musl's libm handles it, but keep
  CPython away from it (`configure` uses `double` throughout by default);
  don't hand-write anything touching x87.
- **errno is a contract, not a convention.** The Zig plumbing must map every
  `runtime` error to a POSIX name consistently — CPython turns errno into
  exception types (`FileNotFoundError` is `ENOENT`). One table, tested.
- **`malloc` alignment**: 16-byte minimum on x86-64 (SSE spills in
  compiled C). The free-list must guarantee it from day one; retrofitting
  alignment bugs out of an allocator is misery.
- **Vendoring discipline.** The musl subtree is lift-only — never edited in
  place (patches live beside it if ever needed), pinned to one musl release,
  with the file list documented so a version bump is a re-copy, not an
  archaeology dig.
- **stdio buffering vs. crashes.** Buffered stdout + a crashing program eats
  output — the classic debugging trap. `stderr` stays unbuffered (per C
  standard) and `exit`/`abort` flush; document that `_exit` does not.

## Decisions needing sign-off

- **Lift-from-musl for all pure computation** (vendored, pinned, unedited) rather
  than writing or porting whole-musl.
- **Hand-written danos-native headers**; danos errno values are the numbering.
- **`library/c/` as a build package** producing both the sysroot and the
  first-class C-binary build step.
- The **deliberate-absence table** as the living compatibility matrix, drained
  by the three-stage road above — with exactly one permanent "never": `fork`.
- **Full coverage as the end state** (stage 3), reached by the standing rule
  that every system capability ships with its C spelling — not by a musl port.

## Related

- [python-on-danos-milestones.md](python-on-danos-milestones.md) — this is P0.
- [dynamic-libraries.md](dynamic-libraries.md) — ships in this sysroot
  (`dlfcn.h` + the loader) once its D1 lands.
- [python-on-danos.md](python-on-danos.md) — the design note that scoped the
  layer.
- [character-devices-and-tty.md](character-devices-and-tty.md) — P1; supplies
  the console that makes stdio interactive.
- [zig-self-hosting.md](zig-self-hosting.md) — the `runtime.os` seam the
  plumbing layer will re-target when it exists.
- [os-development/sysv.md](os-development/sysv.md) — the entry stack `crt0`
  bridges.
