From 347a041d855c36117ebb3ea2ebc2bfb36781d944 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:43:56 +0100 Subject: [PATCH] Phase 1a: add the native runtime.fs, retire the posix shim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first step of the Zig self-hosting roadmap (docs/zig-self-hosting.md): give danos programs a danos-native file API and remove the premature POSIX compatibility shim. This also resolves the earlier misplacement of a full-write helper into the compat layer — that behaviour now lives natively in runtime.fs.File.writeAll. - library/runtime/fs.zig: the danos-native file client over the VFS (open/read/ write/writeAll/seekTo/attributes/close, directory listing, mount). Handles are *values* — a File/Directory owns its VFS node id and byte offset — so there is no per-process fd table or descriptor limit, unlike the POSIX fd model the shim emulated. This is where the operations that later become std.os.danos are staged. - Retire library/posix/ (unistd, stdio): only five call sites used it, all file operations, all migrated to runtime.fs — fat (mount), the vfs-test and fat-test clients, and init/log-flush (the boot-log flush). stdio was already dead. - build.zig: drop the posix module, its addUserBinary parameter, the per-binary import, and the ~26 call-site arguments. - Docs: the VFS protocol's client is now runtime.fs; the docs index and coding-standards note posix is retired and the foreign-ABI naming exception now applies to the future std.os.danos seam; the process-lifecycle note points the future musl layer at that same seam rather than the deleted directory. Deferred by design (see the roadmap): the C-ABI runtime.os errno seam is built at fork time (its shape must match std/os/danos.zig); truncate/mkdir/rename are Phase 2; stdio-byte fds and cwd are later slices. Verified: zig build, zig build test, zig build check-fat-image, and a sequential QEMU sweep — vfs, vfs-client-death (the park/hold-handle path), fat-mount, log-flush, orderly-shutdown, initial-ramdisk (log-flush silent in the bare sweep), smoke, init, usb-storage, device-manager — all green. --- build.zig | 62 +++---- docs/README.md | 22 +-- docs/coding-standards.md | 21 ++- docs/process-lifecycle.md | 12 +- library/posix/posix.zig | 13 -- library/posix/stdio.zig | 115 ------------ library/posix/unistd.zig | 219 ---------------------- library/runtime/fs.zig | 232 ++++++++++++++++++++++++ library/runtime/runtime.zig | 5 + system/services/fat/fat-test.zig | 30 +-- system/services/fat/fat.zig | 3 +- system/services/init/init.zig | 8 +- system/services/log-flush/log-flush.zig | 33 ++-- system/services/vfs/protocol.zig | 7 +- system/services/vfs/vfs-test.zig | 36 ++-- 15 files changed, 347 insertions(+), 471 deletions(-) delete mode 100644 library/posix/posix.zig delete mode 100644 library/posix/stdio.zig delete mode 100644 library/posix/unistd.zig create mode 100644 library/runtime/fs.zig diff --git a/build.zig b/build.zig index c0d1829..304bc9e 100644 --- a/build.zig +++ b/build.zig @@ -58,7 +58,6 @@ fn addUserBinary( b: *std.Build, target: std.Build.ResolvedTarget, runtime_module: *std.Build.Module, - posix_module: *std.Build.Module, mmio_module: *std.Build.Module, xkeyboard_config_module: *std.Build.Module, acpi_ids_module: *std.Build.Module, @@ -78,9 +77,6 @@ fn addUserBinary( .stack_protector = false, .imports = &.{ .{ .name = "runtime", .module = runtime_module }, - // POSIX/C compatibility layer, available to any program that wants it - // (danos-native code uses `runtime` directly). See library/posix/. - .{ .name = "posix", .module = posix_module }, // Typed volatile MMIO + memory barriers, for drivers. See library/mmio/. .{ .name = "mmio", .module = mmio_module }, // Keyboard layouts (keycode + modifiers -> keysym/character), available @@ -280,18 +276,6 @@ pub fn build(b: *std.Build) void { }, }); - // The POSIX / C compatibility layer, a separate library layered strictly over the - // runtime (it calls the runtime's IPC/heap, never system calls directly). This is - // the one place POSIX/C spellings are allowed verbatim — see docs/coding-standards.md - // and library/posix/posix.zig. - const posix_module = b.addModule("posix", .{ - .root_source_file = b.path("library/posix/posix.zig"), - .imports = &.{ - .{ .name = "runtime", .module = runtime_module }, - .{ .name = "vfs-protocol", .module = vfs_protocol_module }, - }, - }); - // The initial_ramdisk container format, shared by the kernel (unpacks it) and the // build-time packer tools/make-initial-ramdisk.py (produces it). No dependencies. const initial_ramdisk_module = b.addModule("initial-ramdisk", .{ @@ -363,7 +347,7 @@ pub fn build(b: *std.Build) void { // Built by the shared user-binary recipe (see addUserBinary): freestanding, // linked into the kernel's user region against the `runtime` runtime library, and // started in ring 3 by the kernel's user-ELF loader. - const init_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "init", "system/services/init/init.zig"); + const init_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "init", "system/services/init/init.zig"); const init_install = b.addInstallArtifact(init_exe, .{ .dest_dir = .{ .override = .{ .custom = "system/services" } } }); b.getInstallStep().dependOn(&init_install.step); @@ -371,12 +355,12 @@ pub fn build(b: *std.Build) void { // Each is built by the same user-binary recipe, then packed into one image by // the host-side make-initial-ramdisk tool. The bootloader ferries the image to the kernel, // which unpacks it and spawns each program (system/initial-ramdisk.zig). - const vfs_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs", "system/services/vfs/vfs.zig"); - const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs-test", "system/services/vfs/vfs-test.zig"); - const ps2_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-bus", "system/drivers/ps2-bus/ps2-bus.zig"); - const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig"); - const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig"); - const usb_xhci_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-xhci-bus", "system/drivers/usb-xhci-bus/usb-xhci-bus.zig"); + const vfs_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs", "system/services/vfs/vfs.zig"); + const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs-test", "system/services/vfs/vfs-test.zig"); + const ps2_bus_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-bus", "system/drivers/ps2-bus/ps2-bus.zig"); + const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig"); + const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig"); + const usb_xhci_bus_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-xhci-bus", "system/drivers/usb-xhci-bus/usb-xhci-bus.zig"); // The xHCI bus driver builds chapter-9 requests and decodes descriptors from // usb-abi, and reports each interface's (class,subclass,protocol) identity via // usb-ids.packTriple. @@ -386,26 +370,26 @@ pub fn build(b: *std.Build) void { // The USB HID class drivers: keyboard and mouse. They own no hardware — each // opens its device through runtime.usb (the transfer protocol) and publishes to // the input service. They build chapter-9 class requests from usb-abi. - const usb_hid_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-hid-keyboard", "system/drivers/usb-hid/keyboard.zig"); + const usb_hid_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-hid-keyboard", "system/drivers/usb-hid/keyboard.zig"); usb_hid_keyboard_exe.root_module.addImport("usb-abi", usb_abi_module); - const usb_hid_mouse_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-hid-mouse", "system/drivers/usb-hid/mouse.zig"); + const usb_hid_mouse_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-hid-mouse", "system/drivers/usb-hid/mouse.zig"); usb_hid_mouse_exe.root_module.addImport("usb-abi", usb_abi_module); // The USB mass-storage class driver: opens its device via runtime.usb, drives it // with Bulk-Only Transport + SCSI, and serves the block protocol under `.block`. - const usb_storage_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-storage", "system/drivers/usb-storage/usb-storage.zig"); + const usb_storage_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-storage", "system/drivers/usb-storage/usb-storage.zig"); usb_storage_exe.root_module.addImport("block-protocol", block_protocol_module); // The FAT filesystem server: mounts the block device and serves it into the VFS // at /mnt/usb. Its engine (engine.zig / on-disk.zig) is imported relatively. - const fat_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat", "system/services/fat/fat.zig"); - const fat_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat-test", "system/services/fat/fat-test.zig"); - const pci_bus_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "pci-bus", "system/drivers/pci-bus/pci-bus.zig"); + const fat_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat", "system/services/fat/fat.zig"); + const fat_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat-test", "system/services/fat/fat-test.zig"); + const pci_bus_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "pci-bus", "system/drivers/pci-bus/pci-bus.zig"); // The PCI bus driver decodes each function's class triple to human names in its // boot log (class/subclass/prog-IF), so pull in the shared pci-class reference. pci_bus_exe.root_module.addImport("pci-class", pci_class_module); // A test fixture, not a real driver: hellos to the device manager, then faults — // what the driver-restart scenario drives the crash-loop cap with. - const crash_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "crash-test", "system/services/crash-test/crash-test.zig"); - const device_list_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-list", "system/services/device-list/device-list.zig"); + const crash_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "crash-test", "system/services/crash-test/crash-test.zig"); + const device_list_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-list", "system/services/device-list/device-list.zig"); // The discovery service: one swappable process per firmware // (docs/discovery.md), bundled under the neutral ramdisk name // "discovery" so the device manager never learns which firmware it is on. @@ -419,9 +403,9 @@ pub fn build(b: *std.Build) void { .acpi => "system/services/acpi/acpi.zig", .fdt => "system/services/fdt/fdt.zig", }; - const discovery_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "discovery", discovery_source); + const discovery_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "discovery", discovery_source); if (discovery == .acpi) discovery_exe.root_module.addImport("aml", aml_module); - const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-manager", "system/services/device-manager/device-manager.zig"); + const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-manager", "system/services/device-manager/device-manager.zig"); // Names the xHCI PCI class triple from the shared taxonomy instead of a bare 0x0C0330. device_manager_exe.root_module.addImport("pci-class", pci_class_module); // The manager matches reported USB interfaces by their (class,subclass,protocol) @@ -429,12 +413,12 @@ pub fn build(b: *std.Build) void { device_manager_exe.root_module.addImport("usb-ids", usb_ids_module); // The input service and its exercisers: the fan-out server, a hardware-free synthetic // source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md. - const input_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input", "system/services/input/input.zig"); - const input_source_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input-source", "system/services/input-source/input-source.zig"); - const input_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input-test", "system/services/input-test/input-test.zig"); - const args_echo_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "args-echo", "system/services/args-echo/args-echo.zig"); - const process_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "process-test", "system/services/process-test/process-test.zig"); - const log_flush_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "log-flush", "system/services/log-flush/log-flush.zig"); + const input_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input", "system/services/input/input.zig"); + const input_source_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input-source", "system/services/input-source/input-source.zig"); + const input_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input-test", "system/services/input-test/input-test.zig"); + const args_echo_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "args-echo", "system/services/args-echo/args-echo.zig"); + const process_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "process-test", "system/services/process-test/process-test.zig"); + const log_flush_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "log-flush", "system/services/log-flush/log-flush.zig"); // Pack the user binaries into the initial_ramdisk image with the host-side Python tool // (the container format is trivial, and Python sidesteps std API churn). Args: diff --git a/docs/README.md b/docs/README.md index 988d909..33e0d42 100644 --- a/docs/README.md +++ b/docs/README.md @@ -197,21 +197,22 @@ system/ → /system danos's own internals (the self-representation) services/ init/ vfs/ device-manager/ system servers → /system/services (vfs/ holds vfs.zig, vfs-test.zig, protocol.zig) library/ → /lib libraries, one sub-directory each - runtime/ the danos-native runtime — the stable application ABI - posix/ POSIX/C compatibility, layered over runtime + runtime/ the danos-native runtime + file API (fs) — the stable application ABI boot/ → /boot the loaders tools/ test/ host-side build + QEMU test harness ``` A sub-project exposes its **public interface as a module**: `system/services/vfs/` owns -the VFS wire protocol (`protocol.zig`, the `vfs-protocol` module), which the POSIX -layer imports by name. `usb`/`block` drivers will expose their protocols the same way. +the VFS wire protocol (`protocol.zig`, the `vfs-protocol` module), which the runtime's +file API (`runtime.fs`) imports by name. `usb`/`block` drivers expose their protocols the +same way. -`library/posix/` is special: it is the **one place** POSIX/C spellings are allowed -verbatim (`stat`, `O_CREAT`, `fopen`, `errno`). Everywhere else follows the danos -naming rule with no exception — see [coding-standards.md](coding-standards.md). The -POSIX layer calls the runtime, never the kernel's system calls directly, so it never -appears in the private-ABI path. +There is **no POSIX/C compatibility layer today**: danos programs do file I/O through the +danos-native `runtime.fs` (open/read/write/list over the VFS). A hand-rolled POSIX shim +(`library/posix/`) was retired as premature — the real POSIX/C surface will come later +from the `std.os.danos` seam (and, eventually, musl) when danos becomes a Zig target (see +[zig-self-hosting.md](zig-self-hosting.md)). When it does, the foreign-ABI naming +exception in [coding-standards.md](coding-standards.md) applies to that seam. ## Source map @@ -235,8 +236,7 @@ appears in the private-ABI path. | Framebuffer text console (mirrors to serial) | `system/kernel/console.zig` | | In-kernel test cases | `system/kernel/tests.zig` | | Arch-specific kernel code (`halt`, GDT/IDT/TSS, exception + interrupt stubs, page tables, APIC/IO-APIC/timer, serial, linker script) | `system/kernel/architecture/x86_64/` | -| danos-native runtime (`runtime`): syscall wrappers, heap, IPC, device access — the stable application ABI | `library/runtime/` | -| POSIX/C compatibility (`posix`): unistd, stdio — the one place POSIX names are allowed | `library/posix/` | +| danos-native runtime (`runtime`): syscall wrappers, heap, IPC, device access, the file API (`fs`) — the stable application ABI | `library/runtime/` | | System services (init, the VFS server + `protocol`, the device-manager) | `system/services/` | | Device drivers, one sub-project each (`pci-bus`, `ps2-bus`, `usb-xhci-bus` bus drivers) | `system/drivers/` | | Build + `run-x86-64` (QEMU/OVMF) | `build.zig` | diff --git a/docs/coding-standards.md b/docs/coding-standards.md index 8ed8dda..7015eb8 100644 --- a/docs/coding-standards.md +++ b/docs/coding-standards.md @@ -65,15 +65,18 @@ Three, and only three. `errno`, `O_CREAT`. We don't get to rename `fwrite` to `fileWrite` — it wouldn't be `fwrite` any more. - **This exception is scoped to one place: `library/posix/`.** A file under - `library/posix/` *is* the foreign ABI, so it keeps the ABI's spellings — that is the - whole rule for that directory. **Everywhere else, Zig/danos naming applies with no - POSIX exception**, so there is nothing to get wrong: if you're not in - `library/posix/`, expand it. A concept POSIX also has gets a danos name outside that - layer — the VFS wire protocol carries a `FileStatus`, not a `Stat`, and a `create` - flag, not `O_CREAT`; `library/posix/` is what maps `stat`→`status` and - `O_CREAT`→`create` at the boundary. (The `syscall` *wrappers* elsewhere are not an - exception to this — they wrap the private danos ABI, so they use danos names.) + **This exception is scoped to a file that *is* a foreign ABI, and nothing else.** + danos has no such file today: the old `library/posix/` compatibility shim was retired + once its callers moved to the danos-native `runtime.fs`, since a hand-rolled POSIX + layer is premature until danos actually needs it (see + [zig-self-hosting.md](zig-self-hosting.md)). The exception will apply again to the + `std.os.danos` seam when danos becomes a real Zig target — that module *is* the C-ABI + `system` interface, so it keeps `open`/`read`/`errno`/`O_CREAT`. **Everywhere else, + Zig/danos naming applies with no exception**: a concept POSIX also has gets a danos + name — the VFS wire protocol carries a `FileStatus`, not a `Stat`, and a `create` + flag, not `O_CREAT`; the boundary is where `stat`→`status` and `O_CREAT`→`create` get + mapped. (The `syscall` *wrappers* elsewhere are not an exception — they wrap the + private danos ABI, so they use danos names.) 2. **Zig idioms are spelled the way Zig spells them.** Three names are the language's, not ours, and are left alone: diff --git a/docs/process-lifecycle.md b/docs/process-lifecycle.md index b5c5103..811ca97 100644 --- a/docs/process-lifecycle.md +++ b/docs/process-lifecycle.md @@ -17,12 +17,12 @@ was a mistake) without inheriting the mechanism, the API, or the names. The nami rule is danos's own and it is strict: plain words that communicate intent (`terminate`, `reload`, `exited`) and the IPC vocabulary the system already speaks (`bind`, `subscribe`, `publish`, `endpoint`) — never `SIG*`, never a second word for -a concept that already has one. Literal POSIX arrives later and lives elsewhere: a -**musl-based C layer** (growing out of library/posix) that wires C programs to the -danos runtime — musl's syscall surface retargeted at danos system calls and IPC -protocols (files onto the VFS protocol, `sigaction`/`wait` onto this lifecycle, -sockets onto whatever networking becomes). Ported programs see POSIX; the system -underneath never does. +a concept that already has one. Literal POSIX arrives later and lives elsewhere: the +`std.os.danos` seam that makes danos a Zig target, and eventually a **musl-based C +layer** on the same native surface (see [zig-self-hosting.md](zig-self-hosting.md)) — +musl's syscall surface retargeted at danos system calls and IPC protocols (files onto +the VFS protocol, `sigaction`/`wait` onto this lifecycle, sockets onto whatever +networking becomes). Ported programs see POSIX; the system underneath never does. ## Why a standard vocabulary diff --git a/library/posix/posix.zig b/library/posix/posix.zig deleted file mode 100644 index a6af8e7..0000000 --- a/library/posix/posix.zig +++ /dev/null @@ -1,13 +0,0 @@ -//! DanOS's POSIX / C compatibility layer — `unistd`, `stdio`, and (later) the C -//! `errno` / `struct stat` / `extern "C"` surface. This is the *one* place POSIX and -//! C spellings are allowed to appear verbatim (see docs/coding-standards.md): a file -//! under library/posix/ *is* the foreign ABI, so it keeps the ABI's names. Everything -//! it touches on the danos side (the VFS protocol, the runtime) uses danos names, -//! which this layer translates to at the boundary. -//! -//! It is layered strictly *over* the runtime: it calls the runtime's IPC and heap, -//! never the kernel's system calls directly. danos-native applications use the -//! runtime; this exists so *POSIX* software can too. - -pub const unistd = @import("unistd.zig"); -pub const stdio = @import("stdio.zig"); diff --git a/library/posix/stdio.zig b/library/posix/stdio.zig deleted file mode 100644 index 4312669..0000000 --- a/library/posix/stdio.zig +++ /dev/null @@ -1,115 +0,0 @@ -//! A small C stdio layer over the POSIX-style file API (unistd.zig). Unbuffered -//! for now — each fread/fwrite is one VFS round trip; an internal buffer (fewer -//! IPC calls) is a later optimisation. Both a Zig-callable API and `extern "C"` -//! symbols are provided, so Zig and future C programs share it. - -const std = @import("std"); -const unistd = @import("unistd.zig"); -const heap = @import("runtime").heap; - -pub const SEEK_SET = unistd.SEEK_SET; -pub const SEEK_CURRENT = unistd.SEEK_CURRENT; -pub const SEEK_END = unistd.SEEK_END; - -/// A C `FILE`: an fd plus sticky end-of-file / error flags. Allocated on the -/// heap; `fclose` frees it. -pub const FILE = extern struct { - fd: i32, - eof: c_int = 0, - err: c_int = 0, -}; - -fn flagsFor(mode: []const u8) u32 { - if (mode.len == 0) return 0; - return switch (mode[0]) { - 'w', 'a' => unistd.O_CREAT, - else => 0, - }; -} - -/// Open `path` in `mode` ("r"/"w"/"a", '+' ignored for now). Returns null on error. -pub fn fopen(path: []const u8, mode: []const u8) ?*FILE { - const fd = unistd.open(path, flagsFor(mode)); - if (fd < 0) return null; - const f = heap.allocator().create(FILE) catch { - unistd.close(fd); - return null; - }; - f.* = .{ .fd = fd }; - if (mode.len > 0 and mode[0] == 'a') _ = unistd.lseek(fd, 0, unistd.SEEK_END); - return f; -} - -pub fn fclose(f: *FILE) c_int { - unistd.close(f.fd); - heap.allocator().destroy(f); - return 0; -} - -/// Read `size*nmemb` bytes; returns the number of whole items read. -pub fn fread(buffer: []u8, size: usize, nmemb: usize, f: *FILE) usize { - const total = size * nmemb; - if (total == 0) return 0; - const n = unistd.read(f.fd, buffer[0..@min(buffer.len, total)]); - if (n <= 0) { - f.eof = 1; - return 0; - } - return @as(usize, @intCast(n)) / size; -} - -/// Write `size*nmemb` bytes; returns the number of whole items written. -pub fn fwrite(data: []const u8, size: usize, nmemb: usize, f: *FILE) usize { - const total = @min(data.len, size * nmemb); - if (total == 0) return 0; - const n = unistd.write(f.fd, data[0..total]); - if (n <= 0) { - f.err = 1; - return 0; - } - return @as(usize, @intCast(n)) / size; -} - -pub fn fseek(f: *FILE, off: i64, whence: u32) c_int { - f.eof = 0; - return if (unistd.lseek(f.fd, off, whence) < 0) -1 else 0; -} - -pub fn ftell(f: *FILE) i64 { - return unistd.lseek(f.fd, 0, unistd.SEEK_CURRENT); -} - -pub fn rewind(f: *FILE) void { - _ = fseek(f, 0, SEEK_SET); -} - -pub fn feof(f: *FILE) c_int { - return f.eof; -} - -pub fn ferror(f: *FILE) c_int { - return f.err; -} - -pub fn fputs(s: []const u8, f: *FILE) c_int { - return if (unistd.write(f.fd, s) < 0) -1 else 0; -} - -pub fn fputc(c: u8, f: *FILE) c_int { - const b = [_]u8{c}; - return if (unistd.write(f.fd, &b) == 1) c else -1; -} - -pub fn fgetc(f: *FILE) c_int { - var b: [1]u8 = undefined; - const n = unistd.read(f.fd, &b); - if (n <= 0) { - f.eof = 1; - return -1; // EOF - } - return b[0]; -} - -// Real `extern "C"` symbols (fopen/fread/fseek/...) — with a C-string signature -// distinct from the Zig slice API above — land with the first C program, wired -// via @export so they don't collide with these Zig names. diff --git a/library/posix/unistd.zig b/library/posix/unistd.zig deleted file mode 100644 index 02148c0..0000000 --- a/library/posix/unistd.zig +++ /dev/null @@ -1,219 +0,0 @@ -//! POSIX-style file API for user programs — the low level under C stdio. Files -//! are named objects served by the user-space VFS server (system/services/vfs/vfs.zig); each -//! call marshals a request, IPC_Calls the VFS, and unmarshals the reply. The -//! kernel knows nothing of files or fds — the fd table lives here, per process. - -const std = @import("std"); -const protocol = @import("vfs-protocol"); -const ipc = @import("runtime").ipc; - -pub const O_CREAT = protocol.create; -pub const SEEK_SET: u32 = 0; -pub const SEEK_CURRENT: u32 = 1; -pub const SEEK_END: u32 = 2; - -// Resolve (and cache) the VFS server endpoint, looked up by well-known id. -var vfs_handle: usize = 0; -var vfs_resolved = false; -fn vfs() ?usize { - if (!vfs_resolved) { - vfs_handle = ipc.lookup(.vfs) orelse return null; - vfs_resolved = true; - } - return vfs_handle; -} - -const maximum_fds = 32; -const Fd = struct { used: bool = false, node: u64 = 0, offset: u64 = 0 }; -var fds = [_]Fd{.{}} ** maximum_fds; - -fn allocFd() ?usize { - for (&fds, 0..) |*f, i| { - if (!f.used) { - f.* = .{ .used = true }; - return i; - } - } - return null; -} - -const Result = struct { reply: protocol.Reply, payload: []u8 }; - -/// One request/reply round trip: [Request header][send payload] -> VFS -> -/// [Reply header][receive payload]. The receive payload is written into `out`. -fn transact(request: protocol.Request, send: []const u8, out: []u8) ?Result { - const h = vfs() orelse return null; - var message: [protocol.message_maximum]u8 = undefined; - @memcpy(message[0..protocol.request_size], std.mem.asBytes(&request)); - const slen = @min(send.len, protocol.maximum_payload); - @memcpy(message[protocol.request_size..][0..slen], send[0..slen]); - - var rbuf: [protocol.message_maximum]u8 = undefined; - const n = ipc.call(h, message[0 .. protocol.request_size + slen], &rbuf) catch return null; - if (n < protocol.reply_size) return null; - const reply = std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]); - const rpl = @min(n - protocol.reply_size, out.len); - @memcpy(out[0..rpl], rbuf[protocol.reply_size..][0..rpl]); - return .{ .reply = reply, .payload = out[0..rpl] }; -} - -/// Open (or create, with O_CREAT) `path`; returns an fd or -1. -pub fn open(path: []const u8, flags: u32) i32 { - const fd = allocFd() orelse return -1; - const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = flags }; - const r = transact(request, path, &.{}) orelse { - fds[fd].used = false; - return -1; - }; - if (r.reply.status != 0) { - fds[fd].used = false; - return -1; - } - fds[fd] = .{ .used = true, .node = r.reply.node, .offset = 0 }; - return @intCast(fd); -} - -fn fdPtr(fd: i32) ?*Fd { - if (fd < 0 or fd >= maximum_fds) return null; - const f = &fds[@intCast(fd)]; - return if (f.used) f else null; -} - -/// Read up to `buffer.len` bytes at the current offset; returns the count or -1. -pub fn read(fd: i32, buffer: []u8) isize { - const f = fdPtr(fd) orelse return -1; - const want: u32 = @intCast(@min(buffer.len, protocol.maximum_payload)); - const request = protocol.Request{ .operation = .read, .node = f.node, .offset = f.offset, .len = want, .flags = 0 }; - const r = transact(request, &.{}, buffer) orelse return -1; - if (r.reply.status != 0) return -1; - f.offset += r.reply.len; - return @intCast(r.reply.len); -} - -/// Write `data` at the current offset; returns the count or -1. -pub fn write(fd: i32, data: []const u8) isize { - const f = fdPtr(fd) orelse return -1; - const want: u32 = @intCast(@min(data.len, protocol.maximum_payload)); - const request = protocol.Request{ .operation = .write, .node = f.node, .offset = f.offset, .len = want, .flags = 0 }; - const r = transact(request, data[0..want], &.{}) orelse return -1; - if (r.reply.status != 0) return -1; - f.offset += r.reply.len; - return @intCast(r.reply.len); -} - -/// Write all of `data`, looping until it is fully written (a single `write` caps -/// each call at the VFS payload size). Returns the total written, or -1 if a -/// underlying write failed before any progress on that chunk. -pub fn writeAll(fd: i32, data: []const u8) isize { - var written: usize = 0; - while (written < data.len) { - const n = write(fd, data[written..]); - if (n <= 0) return if (written == 0) -1 else @intCast(written); - written += @intCast(n); - } - return @intCast(written); -} - -/// Reposition the fd's offset. Returns the new offset or -1. (SEEK_END needs the -/// file size, which `stat` provides; handled by fetching it here.) -pub fn lseek(fd: i32, off: i64, whence: u32) i64 { - const f = fdPtr(fd) orelse return -1; - const base: i64 = switch (whence) { - SEEK_SET => 0, - SEEK_CURRENT => @intCast(f.offset), - SEEK_END => blk: { - const request = protocol.Request{ .operation = .status, .node = f.node, .offset = 0, .len = 0, .flags = 0 }; - var sbuf: [@sizeOf(protocol.FileStatus)]u8 = undefined; - const r = transact(request, &.{}, &sbuf) orelse return -1; - if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.FileStatus)) return -1; - const st = std.mem.bytesToValue(protocol.FileStatus, sbuf[0..@sizeOf(protocol.FileStatus)]); - break :blk @intCast(st.size); - }, - else => return -1, - }; - const pos = base + off; - if (pos < 0) return -1; - f.offset = @intCast(pos); - return pos; -} - -/// Stat `path`. Returns 0 or -1. -pub fn stat(path: []const u8, out: *protocol.FileStatus) i32 { - // Open, stat by node, close — simple and enough for now. - const fd = open(path, 0); - if (fd < 0) return -1; - defer close(fd); - const f = fdPtr(fd).?; - const request = protocol.Request{ .operation = .status, .node = f.node, .offset = 0, .len = 0, .flags = 0 }; - var sbuf: [@sizeOf(protocol.FileStatus)]u8 = undefined; - const r = transact(request, &.{}, &sbuf) orelse return -1; - if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.FileStatus)) return -1; - out.* = std.mem.bytesToValue(protocol.FileStatus, sbuf[0..@sizeOf(protocol.FileStatus)]); - return 0; -} - -/// Close an fd (best effort — tells the VFS to release the open file). -pub fn close(fd: i32) void { - const f = fdPtr(fd) orelse return; - const request = protocol.Request{ .operation = .close, .node = f.node, .offset = 0, .len = 0, .flags = 0 }; - _ = transact(request, &.{}, &.{}); - f.used = false; -} - -/// Mount a filesystem backend (its server endpoint) at absolute path `target`; -/// the VFS then routes every path under `target` to that backend. Returns 0 or -/// -1. This is the one call that hands the VFS a capability (the backend). -pub fn mount(target: []const u8, backend: usize) i32 { - const h = vfs() orelse return -1; - const request = protocol.Request{ .operation = .mount, .node = 0, .offset = 0, .len = @intCast(target.len), .flags = 0 }; - var message: [protocol.message_maximum]u8 = undefined; - @memcpy(message[0..protocol.request_size], std.mem.asBytes(&request)); - const tlen = @min(target.len, protocol.maximum_payload); - @memcpy(message[protocol.request_size..][0..tlen], target[0..tlen]); - var rbuf: [protocol.message_maximum]u8 = undefined; - const result = ipc.callCap(h, message[0 .. protocol.request_size + tlen], &rbuf, backend) catch return -1; - if (result.len < protocol.reply_size) return -1; - return if (std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]).status == 0) 0 else -1; -} - -/// A directory entry filled by `readdir`. -pub const DirEntry = struct { - kind: u32 = 0, // a protocol.NodeKind - size: u64 = 0, - name_buffer: [64]u8 = undefined, - name_len: usize = 0, - - pub fn name(self: *const DirEntry) []const u8 { - return self.name_buffer[0..self.name_len]; - } -}; - -/// Open a directory for reading with `readdir`. Returns an fd or -1. -pub fn opendir(path: []const u8) i32 { - return open(path, protocol.directory); -} - -/// Read the next entry of a directory fd into `entry`; returns false at EOF or on -/// error. Advances the fd's cursor by one entry. -pub fn readdir(fd: i32, entry: *DirEntry) bool { - const f = fdPtr(fd) orelse return false; - const request = protocol.Request{ .operation = .readdir, .node = f.node, .offset = f.offset, .len = 0, .flags = 0 }; - var buffer: [protocol.message_maximum]u8 = undefined; - const r = transact(request, &.{}, &buffer) orelse return false; - if (r.reply.status != 0 or r.reply.len == 0) return false; // error or EOF - if (r.payload.len < protocol.directory_entry_size) return false; - const header = std.mem.bytesToValue(protocol.DirectoryEntry, r.payload[0..protocol.directory_entry_size]); - entry.kind = header.kind; - entry.size = header.size; - const source = r.payload[protocol.directory_entry_size..]; - const nlen = @min(@min(@as(usize, header.name_len), source.len), entry.name_buffer.len); - @memcpy(entry.name_buffer[0..nlen], source[0..nlen]); - entry.name_len = nlen; - f.offset += 1; - return true; -} - -/// Close a directory fd (same as `close`). -pub fn closedir(fd: i32) void { - close(fd); -} diff --git a/library/runtime/fs.zig b/library/runtime/fs.zig new file mode 100644 index 0000000..9792af0 --- /dev/null +++ b/library/runtime/fs.zig @@ -0,0 +1,232 @@ +//! runtime.fs — the danos-native file API. A program opens, reads, writes, and +//! lists files served by the user-space VFS (system/services/vfs), each call +//! marshalling a vfs-protocol request over IPC. This is the danos-native layer +//! danos programs use directly; it is also where the file operations that later +//! become `std.os.danos` are staged (see docs/zig-self-hosting.md). It replaces +//! the old POSIX `unistd` shim — a compatibility spelling danos does not need yet. +//! +//! Handles are *values*, not entries in a global descriptor table: a `File` / +//! `Directory` owns its VFS node id and (for files) a byte offset. So there is no +//! per-process fd limit and no shared table to synchronise — the danos-native +//! shape, unlike the POSIX fd model the old shim emulated. + +const std = @import("std"); +const ipc = @import("ipc.zig"); +const protocol = @import("vfs-protocol"); + +/// The kind of a filesystem node — re-exported so a caller need not import the +/// wire protocol. +pub const Kind = protocol.NodeKind; + +/// A node's metadata (the answer to a status request). +pub const Attributes = struct { size: u64, kind: Kind }; + +// Map a wire `NodeKind` value to the enum, defaulting anything unrecognised to +// `.regular` (the server is trusted, but a value outside the enum would be +// illegal to `@enumFromInt` directly). +fn kindFromWire(value: u32) Kind { + return switch (value) { + @intFromEnum(Kind.directory) => .directory, + @intFromEnum(Kind.character_device) => .character_device, + @intFromEnum(Kind.block_device) => .block_device, + @intFromEnum(Kind.symbolic_link) => .symbolic_link, + @intFromEnum(Kind.fifo) => .fifo, + @intFromEnum(Kind.socket) => .socket, + else => .regular, + }; +} + +/// How to open a path. +pub const OpenOptions = struct { + /// Create the file if it does not exist. + create: bool = false, + /// Open a directory node (for listing) rather than a file. + directory: bool = false, + + fn wireFlags(self: OpenOptions) u32 { + var f: u32 = 0; + if (self.create) f |= protocol.create; + if (self.directory) f |= protocol.directory; + return f; + } +}; + +// The VFS server endpoint, looked up once by well-known id and cached. +var vfs_handle: ipc.Handle = 0; +var vfs_resolved = false; +fn vfs() ?ipc.Handle { + if (!vfs_resolved) { + vfs_handle = ipc.lookup(.vfs) orelse return null; + vfs_resolved = true; + } + return vfs_handle; +} + +const Result = struct { reply: protocol.Reply, payload: []u8 }; + +// One request/reply round trip: [Request header][send payload] -> VFS -> +// [Reply header][receive payload]. The receive payload lands in `out`. +fn transact(request: protocol.Request, send: []const u8, out: []u8) ?Result { + const h = vfs() orelse return null; + var message: [protocol.message_maximum]u8 = undefined; + @memcpy(message[0..protocol.request_size], std.mem.asBytes(&request)); + const slen = @min(send.len, protocol.maximum_payload); + @memcpy(message[protocol.request_size..][0..slen], send[0..slen]); + + var rbuf: [protocol.message_maximum]u8 = undefined; + const n = ipc.call(h, message[0 .. protocol.request_size + slen], &rbuf) catch return null; + if (n < protocol.reply_size) return null; + const reply = std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]); + const rpl = @min(n - protocol.reply_size, out.len); + @memcpy(out[0..rpl], rbuf[protocol.reply_size..][0..rpl]); + return .{ .reply = reply, .payload = out[0..rpl] }; +} + +/// An open file: a VFS node plus a byte cursor. Read and write advance the cursor. +pub const File = struct { + node: u64, + offset: u64 = 0, + + /// Read up to `buffer.len` bytes at the current offset; returns the count, or + /// null on error. + pub fn read(self: *File, buffer: []u8) ?usize { + const want: u32 = @intCast(@min(buffer.len, protocol.maximum_payload)); + const request = protocol.Request{ .operation = .read, .node = self.node, .offset = self.offset, .len = want, .flags = 0 }; + const r = transact(request, &.{}, buffer) orelse return null; + if (r.reply.status != 0) return null; + self.offset += r.reply.len; + return r.reply.len; + } + + /// Write `data` at the current offset; returns the count written. A single + /// call is capped at the VFS payload size, so the return may be short — use + /// `writeAll` to write the whole slice. Null on error. + pub fn write(self: *File, data: []const u8) ?usize { + const want: u32 = @intCast(@min(data.len, protocol.maximum_payload)); + const request = protocol.Request{ .operation = .write, .node = self.node, .offset = self.offset, .len = want, .flags = 0 }; + const r = transact(request, data[0..want], &.{}) orelse return null; + if (r.reply.status != 0) return null; + self.offset += r.reply.len; + return r.reply.len; + } + + /// Write all of `data`, looping past the per-call payload cap. Returns the + /// total written, or null if a write failed before any progress. + pub fn writeAll(self: *File, data: []const u8) ?usize { + var written: usize = 0; + while (written < data.len) { + const n = self.write(data[written..]) orelse return if (written == 0) null else written; + if (n == 0) return written; // no forward progress; stop rather than spin + written += n; + } + return written; + } + + /// Move the read/write cursor to an absolute byte position. + pub fn seekTo(self: *File, position: u64) void { + self.offset = position; + } + + /// This file's metadata. + pub fn attributes(self: *File) ?Attributes { + const request = protocol.Request{ .operation = .status, .node = self.node, .offset = 0, .len = 0, .flags = 0 }; + var buffer: [@sizeOf(protocol.FileStatus)]u8 = undefined; + const r = transact(request, &.{}, &buffer) orelse return null; + if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.FileStatus)) return null; + const status = std.mem.bytesToValue(protocol.FileStatus, buffer[0..@sizeOf(protocol.FileStatus)]); + return .{ .size = status.size, .kind = kindFromWire(status.kind) }; + } + + /// Release the VFS's open handle for this file. + pub fn close(self: *File) void { + const request = protocol.Request{ .operation = .close, .node = self.node, .offset = 0, .len = 0, .flags = 0 }; + _ = transact(request, &.{}, &.{}); + } +}; + +/// Open (or create, with `.create`) `path`. Returns the open file, or null. +pub fn open(path: []const u8, options: OpenOptions) ?File { + const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = options.wireFlags() }; + const r = transact(request, path, &.{}) orelse return null; + if (r.reply.status != 0) return null; + return .{ .node = r.reply.node }; +} + +/// A path's metadata without keeping it open (open -> status -> close). +pub fn attributes(path: []const u8) ?Attributes { + var file = open(path, .{}) orelse return null; + defer file.close(); + return file.attributes(); +} + +/// Whether `path` resolves — handy as a readiness check (e.g. waiting for a mount +/// to come up before writing to it). +pub fn exists(path: []const u8) bool { + return attributes(path) != null; +} + +/// One entry returned by `Directory.next`. +pub const Entry = struct { + kind: Kind = .regular, + size: u64 = 0, + name_buffer: [64]u8 = undefined, + name_len: usize = 0, + + pub fn name(self: *const Entry) []const u8 { + return self.name_buffer[0..self.name_len]; + } +}; + +/// An open directory being listed, cursor-advanced by `next`. +pub const Directory = struct { + node: u64, + cursor: u64 = 0, + + /// Fill `entry` with the next directory entry; false at end of directory or + /// on error. + pub fn next(self: *Directory, entry: *Entry) bool { + const request = protocol.Request{ .operation = .readdir, .node = self.node, .offset = self.cursor, .len = 0, .flags = 0 }; + var buffer: [protocol.message_maximum]u8 = undefined; + const r = transact(request, &.{}, &buffer) orelse return false; + if (r.reply.status != 0 or r.reply.len == 0) return false; // error or EOF + if (r.payload.len < protocol.directory_entry_size) return false; + const header = std.mem.bytesToValue(protocol.DirectoryEntry, r.payload[0..protocol.directory_entry_size]); + entry.kind = kindFromWire(header.kind); + entry.size = header.size; + const source = r.payload[protocol.directory_entry_size..]; + const nlen = @min(@min(@as(usize, header.name_len), source.len), entry.name_buffer.len); + @memcpy(entry.name_buffer[0..nlen], source[0..nlen]); + entry.name_len = nlen; + self.cursor += 1; + return true; + } + + /// Release the VFS's open handle for this directory. + pub fn close(self: *Directory) void { + var f = File{ .node = self.node }; + f.close(); + } +}; + +/// Open `path` as a directory for listing. Returns null if it isn't one / on error. +pub fn openDirectory(path: []const u8) ?Directory { + const file = open(path, .{ .directory = true }) orelse return null; + return .{ .node = file.node }; +} + +/// Mount a filesystem backend (its server endpoint) at absolute path `target`; +/// the VFS then routes everything under `target` to that backend. This is the one +/// call that hands the VFS a capability (the backend endpoint). Returns true on +/// success. +pub fn mount(target: []const u8, backend: ipc.Handle) bool { + const h = vfs() orelse return false; + const request = protocol.Request{ .operation = .mount, .node = 0, .offset = 0, .len = @intCast(target.len), .flags = 0 }; + var message: [protocol.message_maximum]u8 = undefined; + @memcpy(message[0..protocol.request_size], std.mem.asBytes(&request)); + const tlen = @min(target.len, protocol.maximum_payload); + @memcpy(message[protocol.request_size..][0..tlen], target[0..tlen]); + var rbuf: [protocol.message_maximum]u8 = undefined; + const result = ipc.callCap(h, message[0 .. protocol.request_size + tlen], &rbuf, backend) catch return false; + if (result.len < protocol.reply_size) return false; + return std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]).status == 0; +} diff --git a/library/runtime/runtime.zig b/library/runtime/runtime.zig index a85a799..bd1031c 100644 --- a/library/runtime/runtime.zig +++ b/library/runtime/runtime.zig @@ -46,6 +46,11 @@ pub const usb = @import("usb.zig"); /// usb-storage). See library/runtime/block.zig. pub const block = @import("block.zig"); +/// The danos-native file API (open/read/write/list over the user-space VFS) — the +/// layer danos programs use directly, and where the operations that later become +/// `std.os.danos` are staged. See docs/zig-self-hosting.md. +pub const fs = @import("fs.zig"); + /// Re-exported so a user binary can `pub const panic = runtime.panic;`. pub const panic = start.panic; diff --git a/system/services/fat/fat-test.zig b/system/services/fat/fat-test.zig index 136f719..1bf3e2f 100644 --- a/system/services/fat/fat-test.zig +++ b/system/services/fat/fat-test.zig @@ -6,6 +6,7 @@ const std = @import("std"); const runtime = @import("runtime"); +const fs = runtime.fs; fn writeLine(comptime fmt: []const u8, arguments: anytype) void { var line: [128]u8 = undefined; @@ -14,38 +15,37 @@ fn writeLine(comptime fmt: []const u8, arguments: anytype) void { pub fn main(init: runtime.process.Init) void { _ = init; - const unistd = @import("posix").unistd; // Wait for /mnt/usb to be mounted — the fat server races us at boot (it must // bring up the whole USB storage chain first). - var dir: i32 = -1; + var opened: ?fs.Directory = null; var tries: u32 = 0; - while (dir < 0 and tries < 1400) : (tries += 1) { - dir = unistd.opendir("/mnt/usb"); - if (dir < 0) runtime.system.sleep(50); + while (opened == null and tries < 1400) : (tries += 1) { + opened = fs.openDirectory("/mnt/usb"); + if (opened == null) runtime.system.sleep(50); } - if (dir < 0) { + var dir = opened orelse { _ = runtime.system.write("fat-test: /mnt/usb never became available\n"); return; - } + }; var count: u32 = 0; - var entry: unistd.DirEntry = .{}; - while (unistd.readdir(dir, &entry)) { - writeLine("fat-test: entry '{s}' kind={d} size={d}\n", .{ entry.name(), entry.kind, entry.size }); + var entry: fs.Entry = .{}; + while (dir.next(&entry)) { + writeLine("fat-test: entry '{s}' kind={d} size={d}\n", .{ entry.name(), @intFromEnum(entry.kind), entry.size }); count += 1; if (count > 32) break; } - unistd.closedir(dir); + dir.close(); writeLine("fat-test: listed {d} entries\n", .{count}); // Read a known file off the boot volume through the mount (best effort): the // kernel image is an ELF, so its first bytes are the ELF magic. - const fd = unistd.open("/mnt/usb/system/kernel", 0); - if (fd >= 0) { + if (fs.open("/mnt/usb/system/kernel", .{})) |opened_file| { + var file = opened_file; var magic: [4]u8 = undefined; - const n = unistd.read(fd, &magic); - unistd.close(fd); + const n = file.read(&magic) orelse 0; + file.close(); if (n == 4 and magic[0] == 0x7F and magic[1] == 'E' and magic[2] == 'L' and magic[3] == 'F') { _ = runtime.system.write("fat-test: read /mnt/usb/system/kernel ELF magic ok\n"); } else { diff --git a/system/services/fat/fat.zig b/system/services/fat/fat.zig index a62c58a..1e04d0e 100644 --- a/system/services/fat/fat.zig +++ b/system/services/fat/fat.zig @@ -14,7 +14,6 @@ const runtime = @import("runtime"); const engine = @import("engine.zig"); const on_disk = @import("on-disk.zig"); const protocol = runtime.vfs_protocol; -const unistd = @import("posix").unistd; const dma = runtime.dma; fn writeLine(comptime fmt: []const u8, arguments: anytype) void { @@ -106,7 +105,7 @@ fn initialise(endpoint: runtime.ipc.Handle) bool { // comes up). From here the VFS routes /mnt/usb/... to this server. var tries: u32 = 0; while (tries < 100) : (tries += 1) { - if (unistd.mount(mount_point, endpoint) == 0) { + if (runtime.fs.mount(mount_point, endpoint)) { writeLine("/system/services/fat: mounted {s}\n", .{mount_point}); return true; } diff --git a/system/services/init/init.zig b/system/services/init/init.zig index 8f2f510..56464eb 100644 --- a/system/services/init/init.zig +++ b/system/services/init/init.zig @@ -19,7 +19,6 @@ const std = @import("std"); const runtime = @import("runtime"); -const unistd = @import("posix").unistd; const power = runtime.power_protocol; /// Where the kernel boot log is persisted on the USB FAT volume — an 8.3 name at @@ -134,15 +133,14 @@ fn subscribePower() void { /// USB volume is not mounted, the open fails and it does nothing. Must run while /// the storage services are still alive (see shutDown). fn flushKernelLog() void { - const fd = unistd.open(log_path, unistd.O_CREAT); - if (fd < 0) return; // no USB volume mounted — nothing to persist to - defer unistd.close(fd); + var file = runtime.fs.open(log_path, .{ .create = true }) orelse return; // no USB volume mounted + defer file.close(); var chunk: [4096]u8 = undefined; var offset: usize = 0; while (true) { const got = runtime.system.klogRead(offset, &chunk); if (got == 0) break; // reached the end of the accumulated log - if (unistd.writeAll(fd, chunk[0..got]) < 0) break; // storage went away + if (file.writeAll(chunk[0..got]) == null) break; // storage went away offset += got; } var line: [96]u8 = undefined; diff --git a/system/services/log-flush/log-flush.zig b/system/services/log-flush/log-flush.zig index 5b9e036..e1b185f 100644 --- a/system/services/log-flush/log-flush.zig +++ b/system/services/log-flush/log-flush.zig @@ -16,19 +16,19 @@ const std = @import("std"); const runtime = @import("runtime"); -const unistd = @import("posix").unistd; +const fs = runtime.fs; const log_path = "/mnt/usb/DANOS.LOG"; -/// Copy the whole kernel log to the open fd, looping klog_read -> write until the -/// log is exhausted. Returns the number of bytes written. -fn drainKernelLog(fd: i32) usize { +/// Copy the whole kernel log to the open file, looping klog_read -> write until +/// the log is exhausted. Returns the number of bytes written. +fn drainKernelLog(file: *fs.File) usize { var chunk: [4096]u8 = undefined; var offset: usize = 0; while (true) { const got = runtime.system.klogRead(offset, &chunk); if (got == 0) break; // reached the end of the accumulated log - if (unistd.writeAll(fd, chunk[0..got]) < 0) break; // storage went away + if (file.writeAll(chunk[0..got]) == null) break; // storage went away offset += got; } return offset; @@ -38,19 +38,22 @@ pub fn main() void { // Wait for the fat server to mount /mnt/usb (it must bring up the whole USB // storage chain first, so it races us at boot). Bounded: if the mount never // appears — no volume, or the no-VFS ramdisk sweep — give up silently. - var dir: i32 = -1; + var ready = false; var tries: u32 = 0; - while (dir < 0 and tries < 1400) : (tries += 1) { - dir = unistd.opendir("/mnt/usb"); - if (dir < 0) runtime.system.sleep(50); + while (tries < 1400) : (tries += 1) { + if (fs.openDirectory("/mnt/usb")) |directory| { + var dir = directory; + dir.close(); + ready = true; + break; + } + runtime.system.sleep(50); } - if (dir < 0) return; // /mnt/usb never became available — nothing to persist to - unistd.closedir(dir); + if (!ready) return; // /mnt/usb never became available — nothing to persist to - const fd = unistd.open(log_path, unistd.O_CREAT); - if (fd < 0) return; // could not create the file — exit quietly - const written = drainKernelLog(fd); - unistd.close(fd); + var file = fs.open(log_path, .{ .create = true }) orelse return; // could not create the file + const written = drainKernelLog(&file); + file.close(); var line: [96]u8 = undefined; _ = runtime.system.write(std.fmt.bufPrint(&line, "log-flush: wrote {d} bytes to {s}\n", .{ written, log_path }) catch return); diff --git a/system/services/vfs/protocol.zig b/system/services/vfs/protocol.zig index 61b34ec..3fb75bc 100644 --- a/system/services/vfs/protocol.zig +++ b/system/services/vfs/protocol.zig @@ -4,12 +4,11 @@ //! header followed by an inline payload (read bytes, or a FileStatus). Everything fits //! in one IPC message (<= ipc MESSAGE_MAXIMUM = 256 bytes). //! -//! This is a danos-native contract, so it uses danos names throughout — the POSIX -//! spellings (`stat`, `O_CREAT`, ...) live only in the POSIX layer -//! (library/posix/unistd.zig), which translates to these. +//! This is a danos-native contract, so it uses danos names throughout. The client +//! side is `runtime.fs` (library/runtime/fs.zig), which programs use directly. //! //! This is user-space only — the kernel knows nothing of files or paths; it only moves the bytes. -//! Shared by library/posix/unistd.zig (client) and system/services/vfs/vfs.zig (server). +//! Shared by library/runtime/fs.zig (client) and system/services/vfs/vfs.zig (server). pub const Operation = enum(u32) { open, // open(path) -> node id diff --git a/system/services/vfs/vfs-test.zig b/system/services/vfs/vfs-test.zig index d76bad9..0bbf6ed 100644 --- a/system/services/vfs/vfs-test.zig +++ b/system/services/vfs/vfs-test.zig @@ -1,26 +1,26 @@ //! /system/services/vfs/vfs-test — a client that proves the VFS round trip end to end: open a -//! file through the `runtime` file API, write to it, seek back, read it, and compare. +//! file through the `runtime.fs` file API, write to it, seek back, read it, and compare. //! On success it heartbeats "vfstest: ok" so the kernel test can observe it; //! on failure it reports what went wrong. Shipped in the initial_ramdisk alongside vfs. const std = @import("std"); const runtime = @import("runtime"); +const fs = runtime.fs; pub fn main(init: runtime.process.Init) void { - const u = @import("posix").unistd; const payload = "hello-vfs"; // The "park" role (the vfs-client-death test): open a file, then hold the // handle forever without closing — the kill and the VFS's release-on-death // are the point. if (init.arguments.count > 1) { - var fd: i32 = -1; + var parked: ?fs.File = null; var tries: u32 = 0; - while (fd < 0 and tries < 200) : (tries += 1) { - fd = u.open("parked", u.O_CREAT); - if (fd < 0) runtime.system.sleep(20); + while (parked == null and tries < 200) : (tries += 1) { + parked = fs.open("parked", .{ .create = true }); + if (parked == null) runtime.system.sleep(20); } - if (fd < 0) { + if (parked == null) { _ = runtime.system.write("vfstest: park open failed\n"); return; } @@ -31,28 +31,28 @@ pub fn main(init: runtime.process.Init) void { } // The VFS server may not have registered yet — retry open until it's up. - var fd: i32 = -1; + var opened: ?fs.File = null; var tries: u32 = 0; - while (fd < 0 and tries < 200) : (tries += 1) { - fd = u.open("greeting", u.O_CREAT); - if (fd < 0) runtime.system.sleep(20); + while (opened == null and tries < 200) : (tries += 1) { + opened = fs.open("greeting", .{ .create = true }); + if (opened == null) runtime.system.sleep(20); } - if (fd < 0) { + var greeting = opened orelse { _ = runtime.system.write("vfstest: open failed\n"); return; - } + }; - if (u.write(fd, payload) != @as(isize, payload.len)) { + if ((greeting.write(payload) orelse 0) != payload.len) { _ = runtime.system.write("vfstest: write failed\n"); return; } - _ = u.lseek(fd, 0, u.SEEK_SET); + greeting.seekTo(0); var buffer: [32]u8 = undefined; - const n = u.read(fd, &buffer); - u.close(fd); + const n = greeting.read(&buffer) orelse 0; + greeting.close(); - if (n == @as(isize, payload.len) and std.mem.eql(u8, buffer[0..@intCast(n)], payload)) { + if (n == payload.len and std.mem.eql(u8, buffer[0..n], payload)) { while (true) { _ = runtime.system.write("vfstest: ok\n"); runtime.system.sleep(1000);