262 lines
14 KiB
Markdown
262 lines
14 KiB
Markdown
# Python on danos: the CPython milestone
|
||
|
||
A design note (not built yet) on bringing **CPython** to danos, compiled with the Zig
|
||
toolchain (`zig cc`). Like [zig-self-hosting.md](zig-self-hosting.md), it is
|
||
forward-looking: it sets a direction and the decisions that follow from it.
|
||
|
||
## Why Python, and why now
|
||
|
||
The Zig self-hosting road is gated on a compiler fork and a long std-library seam.
|
||
Python is the **stop-gap that removes the wait**: a working CPython gives danos a way
|
||
to write programs — services, tools, application prototypes — *without* the Zig
|
||
compiler being self-hosted, and it brings the pure-Python package ecosystem along as
|
||
a bonus. The intended division of labour:
|
||
|
||
- **Zig** — the kernel, drivers, and anything on a data plane (interrupt paths,
|
||
DMA rings, block I/O). Unchanged.
|
||
- **Python** — the control plane and the prototyping surface: services that are
|
||
event loops over IPC, policy logic that changes often, application experiments,
|
||
and eventually the shell.
|
||
|
||
Python is also the scripting language for the terminal-and-shell arc: the first
|
||
real danos application is planned as a terminal, a terminal wants a shell, a shell
|
||
wants a scripting language — and [xonsh](https://xon.sh) (a shell written in
|
||
Python) marks where that road can end.
|
||
|
||
### Non-goals
|
||
|
||
- **No drivers in Python.** Interrupt handling, ring management, and DMA stay in
|
||
Zig. Python may *supervise and configure* drivers; it does not sit in their hot
|
||
paths (interpreter overhead and garbage-collection pauses in an interrupt path
|
||
are disqualifying).
|
||
- **No dynamic loading during bring-up, no `pip`.** The whole arc here ships
|
||
statically linked. Dynamic libraries are a real *later* milestone
|
||
([dynamic-libraries.md](dynamic-libraries.md)) — an application-layer
|
||
facility that unlocks `ctypes` and loadable extension modules; the operating
|
||
system itself stays static and lean regardless (the size doctrine below).
|
||
`pip` stays out either way until a networking track exists.
|
||
- **No fork.** `os.fork` will not exist. This costs almost nothing (see "The
|
||
spawn model fits").
|
||
|
||
## The realization that shapes everything: the compiler is not the obstacle
|
||
|
||
`zig cc` is a full Clang-based C cross-compiler, and CPython is portable C with
|
||
official precedent for stranger targets than danos — the WASI port is upstream
|
||
tier-2, and it runs **without fork, without dynamic loading, and without working
|
||
threads**. Every "CPython can't possibly run there" objection has already been
|
||
answered upstream by a target *more* constrained than danos.
|
||
|
||
What CPython actually needs is a **C environment**: headers and a `libc.a`. danos
|
||
has neither — and that is the whole project. In the language of the Zig roadmap's
|
||
three doors, this is the **door-2-shaped work** (the deferred "musl door"), not the
|
||
`std.os.danos` seam: CPython never touches Zig's std.
|
||
|
||
### The same surface, a third time
|
||
|
||
The Zig roadmap observed that door 1 (`std.os.danos`) and door 2 (a libc) implement
|
||
the *same* ~30 danos-facing operations at different layers. CPython consumes that
|
||
identical surface through C spellings. So nothing here is throwaway: the
|
||
danos-native operations backing `runtime.os` are the same ones the libc bottoms out
|
||
in, and the gaps this track must close (stdio byte streams, cwd, environment,
|
||
entropy) are **exactly the Phase-1 gaps the Zig roadmap already lists**. The two
|
||
tracks share a road until Python forks off at "build the libc."
|
||
|
||
## Where danos stands: coverage vs. the gaps
|
||
|
||
Judged against the minimal CPython configuration (static, WASI-like):
|
||
|
||
| CPython need | danos today | Gap |
|
||
|--------------|-------------|-----|
|
||
| open/read/write/close/lseek, readdir | VFS + FAT via `runtime.fs` | none — wrap in C |
|
||
| mkdir / unlink / rename / truncate | done (self-hosting Phase 2) | none |
|
||
| stat with mtime | done (`wall_clock` + FAT mtime) | none |
|
||
| mmap/munmap (object allocator) | native syscalls | none |
|
||
| monotonic + wall clock | `clock` + `wall_clock` syscalls | none |
|
||
| a place for `Lib/` | FAT boot image | none — better than WASI has it |
|
||
| fork / exec | not needed (subprocess disabled at first) | — |
|
||
| dynamic loading | not needed (static extension modules) | — |
|
||
| getcwd / chdir | — | **missing** (shared with Zig Phase 1) |
|
||
| environment variables | `Init` has no env | **missing** (can start empty) |
|
||
| entropy | — | **missing** (hash seed; `PYTHONHASHSEED` pins it meanwhile) |
|
||
| byte-stream stdin/stdout (fd 0/1/2) | `debug_write` out; structured `InputEvent` in | **missing** (shared with Zig Phase 1; the REPL needs it) |
|
||
| signals | — | stubs suffice (WASI precedent); M17 signals-over-IPC maps on later |
|
||
| threads | native `thread_spawn`/futex | build threadless first; a pthread subset later (xonsh needs it) |
|
||
|
||
The clustering repeats the Zig roadmap's: **files, memory, and time are done; the
|
||
work is the C packaging plus the small seam pieces** (tty bytes, cwd, env, entropy).
|
||
|
||
## The libc decision: hand-rolled in Zig, computation lifted from musl
|
||
|
||
Two viable shapes were considered:
|
||
|
||
| Option | What it is | Verdict |
|
||
|--------|-----------|---------|
|
||
| **Mini-libc in Zig** | C-ABI-exporting Zig library over `runtime.os`/`runtime.fs`, shipped as headers + `libc.a`. | **Take this.** Reuses the danos-native surface directly; no Linux assumptions to fight. |
|
||
| **Port musl** | Full musl with a danos syscall backend. | Defer, again. musl assumes Linux syscall semantics in places; heavier than the need. |
|
||
|
||
The trick that makes the mini-libc tractable: musl's `string/`, `math/` (libm —
|
||
CPython needs essentially all of it), and number-conversion layers are **pure
|
||
computation with no syscalls**. Lift those wholesale (MIT-licensed, designed to
|
||
compile standalone) and hand-write only:
|
||
|
||
- the OS-facing bottom: fds, `mmap`, clocks, `exit`, `getcwd` — thin C-ABI wrappers
|
||
over `runtime.os`;
|
||
- a `FILE*` stdio layer (buffered, over the fd layer);
|
||
- `malloc` over danos `mmap` (a simple allocator is fine; CPython does its own
|
||
small-object arena management above it);
|
||
- the headers (`stdio.h`, `stdlib.h`, `string.h`, `math.h`, `errno.h`, …).
|
||
|
||
Estimate: **100–150 functions**, of which the hard 40% (libm, string, printf/strtod
|
||
cores) are lifted, not written. Correctness hot spots are `strtod`/`dtoa` (Python's
|
||
float repr round-trips through them) — another reason to lift musl's, not improvise.
|
||
|
||
## C interop: static extension modules, not ctypes
|
||
|
||
"Python can interface with C libraries" is true on danos with one important
|
||
correction: **`ctypes` does not work at first** — it is built on `dlopen` + libffi,
|
||
both of which arrive only with the [dynamic-libraries](dynamic-libraries.md)
|
||
milestone (D2). Until then the interop story is the other, older one:
|
||
|
||
- **Extension modules statically linked into the interpreter** via CPython's
|
||
`Modules/Setup` mechanism (the standard route for embedded/static builds).
|
||
- **Zig speaks C ABI natively**, so danos extension modules are written in Zig
|
||
against `Python.h` — no C required. Two modules are planned from the start:
|
||
- **`danos`** — the system module: endpoints, send/receive, capability passing,
|
||
spawn, exit notification. This is what makes a Python *service* possible: an
|
||
event loop over IPC, speaking the same wire protocols as Zig services.
|
||
- **UI toolkit bindings** — the in-progress danos UI toolkit exposed to Python,
|
||
so application prototypes drive real windows.
|
||
|
||
The package story follows: **pure-Python packages work** (unpack into
|
||
`Lib/site-packages` on the FAT image); packages with C extensions must be
|
||
cross-compiled and baked into the interpreter — a curated set chosen per image,
|
||
not `pip install`. That is the honest shape of the stop-gap.
|
||
|
||
## The roadmap
|
||
|
||
### Phase 0 — Toolchain + libc bring-up
|
||
|
||
`zig cc -target x86_64-freestanding-none` plus `-isystem` the danos headers and the
|
||
mini-libc archive. No compiler fork required — this track deliberately avoids the
|
||
Zig roadmap's Phase-0 gate (if the fork lands first, the triple becomes a clean
|
||
`x86_64-danos`; nothing else changes). Exit criterion: a **hello-world C program**
|
||
compiles on the host and runs on danos, printing via the libc's `write`.
|
||
|
||
### Phase 1 — The shared seam pieces
|
||
|
||
The same list as Zig self-hosting Phase 1, closed once for both tracks:
|
||
|
||
- fd 0/1/2 as console **byte** streams (output exists as `debug_write`; input is a
|
||
new small thing — cooked line input first, raw mode when the REPL wants editing);
|
||
- `getcwd`/`chdir`;
|
||
- environment variables (an empty block is a valid start);
|
||
- an entropy syscall or service (until then, builds pin `PYTHONHASHSEED`).
|
||
|
||
### Phase 2 — Cross-compile CPython, minimal configuration
|
||
|
||
Pin one CPython release (3.13 — strongest WASI-era cross-compile support). The
|
||
mechanics are well-trodden upstream since 3.11:
|
||
|
||
- a same-version **build-Python on the host** (`--with-build-python`);
|
||
- a `config.site` cache answering what configure cannot probe cross
|
||
(`ac_cv_file__dev_ptmx=no` and friends);
|
||
- a `config.sub` patch so `x86_64-unknown-danos` parses;
|
||
- `--disable-shared`, static `Modules/Setup` with a minimal module set
|
||
(`posix`, `errno`, `_io`, `_codecs`, `time`, `math`, …);
|
||
- `Lib/` shipped on the FAT image; `PYTHONHOME` pointed at it.
|
||
|
||
Exit criterion: `python -c 'print(2**100)'` runs on danos under QEMU.
|
||
|
||
### Phase 3 — Terminal + REPL: the first real application
|
||
|
||
Depends on the display track's font rendering (already its stated next step) and
|
||
Phase 1's tty. A terminal emulator drawing a `python` REPL is the milestone demo:
|
||
interactive, self-evidently real, and it needs **zero** process-control machinery.
|
||
|
||
### Phase 4 — The `danos` module and Python services
|
||
|
||
Write the `danos` extension module and the UI-toolkit bindings; prototype one real
|
||
service in Python (a policy-shaped one — e.g. hot-plug policy or a settings
|
||
service) speaking the existing IPC protocols. This is the payoff phase for
|
||
"prototyping a service or application."
|
||
|
||
### Phase 5 — Process control, then the shell
|
||
|
||
The shell — any shell, in any language — forces the surface danos has deferred so
|
||
far: **exec-of-path, argv/envp passing, numeric exit status (`WEXITSTATUS`, not the
|
||
categorical `ExitReason`), fd inheritance, and pipes.** That is a kernel/VFS
|
||
milestone cluster of its own. Then, in order:
|
||
|
||
1. `subprocess` enabled in CPython (maps onto danos spawn — see below);
|
||
2. a **small Python shell** (a few hundred lines over `subprocess` + line input, no
|
||
job control) — the forcing function that reveals which process-control pieces
|
||
actually matter;
|
||
3. **explicitly deferred:** a pthread subset over `thread_spawn`/futex
|
||
(create/join/mutex/condition/thread-locals), signals via M17 signals-over-IPC,
|
||
termios job control — and then **xonsh**, which wants all three.
|
||
|
||
### The spawn model fits
|
||
|
||
One genuinely good alignment: **CPython does not need fork.** `subprocess` maps
|
||
cleanly onto a posix_spawn-style model — exactly what danos has — and the existing
|
||
exit-notification-via-endpoint is a *better* fit for `Popen.wait` than Unix's
|
||
`wait` semantics. `os.fork` simply won't exist, as on Windows, and almost nothing
|
||
in practice cares.
|
||
|
||
## Risks and gotchas
|
||
|
||
- **Binary size — and the size doctrine that makes it acceptable.** danos's
|
||
leanness mandate applies to the **operating system**: the kernel and the system
|
||
services stay small (the kernel is measured in kilobytes, not megabytes), and
|
||
nothing in this track changes that — Python never enters the OS layer. An
|
||
**application** budget is different: a statically-linked CPython with its
|
||
module set will be tens of megabytes in ReleaseSafe (the measured ~2×
|
||
safety-check factor compounds it), and that is *allowed* — applications live
|
||
on the FAT image, not in the kernel's world. It still shapes the image, and it
|
||
means every Python service shares one interpreter binary + per-service
|
||
scripts, so the spawn model needs **argv** before "run this .py" works at all.
|
||
- **FAT mtime granularity is 2 seconds.** CPython's `.pyc` cache validation is
|
||
mtime-based by default; a rapid edit-run cycle can see stale bytecode. Use
|
||
hash-based `.pyc` invalidation (PEP 552, `--invalidation-mode checked-hash` at
|
||
freeze time) or accept the quirk during bring-up.
|
||
- **FAT name lookups are case-insensitive.** Long file names preserve case but
|
||
match insensitively — the same world Python inhabits on Windows/macOS, so
|
||
importlib copes, but two modules differing only by case cannot coexist on the
|
||
image.
|
||
- **`strtod`/float repr correctness.** Python's float round-tripping is exacting;
|
||
lift musl's conversions rather than writing them, and run CPython's float tests
|
||
early.
|
||
- **Threadless build is load-bearing, initially.** Like WASI, the first builds have
|
||
no working `threading`. The escape hatch is real (danos has native threads and
|
||
futexes; a pthread subset is Phase-5 work) but keep the configuration honestly
|
||
single-threaded until then.
|
||
- **The test suite is the porting harness.** CPython ships its own conformance
|
||
suite; getting `test_builtin`, `test_int`, `test_float`, `test_io` running on
|
||
danos early converts "it seems to work" into a checklist. Budget image space for
|
||
the test `Lib/` tree during bring-up.
|
||
- **Entropy before exposure.** `PYTHONHASHSEED=0` is fine for bring-up and wrong
|
||
forever; hash randomization exists because attacker-controlled dict keys are a
|
||
denial-of-service vector. Land the entropy source before any Python service
|
||
parses external input.
|
||
|
||
## Related
|
||
|
||
- [python-on-danos-milestones.md](python-on-danos-milestones.md) — the execution
|
||
plan (P0–P5) for this note.
|
||
- [c-library-compatibility.md](c-library-compatibility.md) — the mini-libc
|
||
(libdanos-c) design behind Phase 0.
|
||
- [character-devices-and-tty.md](character-devices-and-tty.md) — the stream-node /
|
||
console / no-pty design behind Phase 1.
|
||
- [zig-self-hosting.md](zig-self-hosting.md) — the sibling track; shares Phase 1,
|
||
diverges at the libc.
|
||
- [os-development/syscall.md](os-development/syscall.md) — the kernel ABI the
|
||
mini-libc bottoms out in.
|
||
- [os-development/vdso.md](os-development/vdso.md) — the public ABI boundary the
|
||
`danos` extension module wraps.
|
||
- [os-development/sysv.md](os-development/sysv.md) — the entry stack (argv/envp)
|
||
the spawn-argv work extends.
|
||
- [device-driver-development/ipc.md](device-driver-development/ipc.md) — the IPC
|
||
surface Python services speak.
|
||
- [file-system-development/danos-file-system-hierarchy-FSH.md](file-system-development/danos-file-system-hierarchy-FSH.md)
|
||
— where `Lib/` and `site-packages` land on the image.
|