Phase 1a: add the native runtime.fs, retire the posix shim

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.
This commit is contained in:
Daniel Samson 2026-07-13 19:43:56 +01:00
parent 53e42837e0
commit 347a041d85
No known key found for this signature in database
GPG Key ID: A9EB2589C60F7268
15 changed files with 347 additions and 471 deletions

View File

@ -58,7 +58,6 @@ fn addUserBinary(
b: *std.Build, b: *std.Build,
target: std.Build.ResolvedTarget, target: std.Build.ResolvedTarget,
runtime_module: *std.Build.Module, runtime_module: *std.Build.Module,
posix_module: *std.Build.Module,
mmio_module: *std.Build.Module, mmio_module: *std.Build.Module,
xkeyboard_config_module: *std.Build.Module, xkeyboard_config_module: *std.Build.Module,
acpi_ids_module: *std.Build.Module, acpi_ids_module: *std.Build.Module,
@ -78,9 +77,6 @@ fn addUserBinary(
.stack_protector = false, .stack_protector = false,
.imports = &.{ .imports = &.{
.{ .name = "runtime", .module = runtime_module }, .{ .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/. // Typed volatile MMIO + memory barriers, for drivers. See library/mmio/.
.{ .name = "mmio", .module = mmio_module }, .{ .name = "mmio", .module = mmio_module },
// Keyboard layouts (keycode + modifiers -> keysym/character), available // 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 // 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. // build-time packer tools/make-initial-ramdisk.py (produces it). No dependencies.
const initial_ramdisk_module = b.addModule("initial-ramdisk", .{ 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, // Built by the shared user-binary recipe (see addUserBinary): freestanding,
// linked into the kernel's user region against the `runtime` runtime library, and // linked into the kernel's user region against the `runtime` runtime library, and
// started in ring 3 by the kernel's user-ELF loader. // 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" } } }); const init_install = b.addInstallArtifact(init_exe, .{ .dest_dir = .{ .override = .{ .custom = "system/services" } } });
b.getInstallStep().dependOn(&init_install.step); 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 // 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, // 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). // 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 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, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs-test", "system/services/vfs/vfs-test.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, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-bus", "system/drivers/ps2-bus/ps2-bus.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, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.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, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.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, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "usb-xhci-bus", "system/drivers/usb-xhci-bus/usb-xhci-bus.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 // 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-abi, and reports each interface's (class,subclass,protocol) identity via
// usb-ids.packTriple. // 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 // 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 // opens its device through runtime.usb (the transfer protocol) and publishes to
// the input service. They build chapter-9 class requests from usb-abi. // 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); 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); 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 // 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`. // 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); 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 // 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. // 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_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, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "fat-test", "system/services/fat/fat-test.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, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "pci-bus", "system/drivers/pci-bus/pci-bus.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 // 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. // 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); 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 // 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. // 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 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, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-list", "system/services/device-list/device-list.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 // The discovery service: one swappable process per firmware
// (docs/discovery.md), bundled under the neutral ramdisk name // (docs/discovery.md), bundled under the neutral ramdisk name
// "discovery" so the device manager never learns which firmware it is on. // "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", .acpi => "system/services/acpi/acpi.zig",
.fdt => "system/services/fdt/fdt.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); 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. // 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); device_manager_exe.root_module.addImport("pci-class", pci_class_module);
// The manager matches reported USB interfaces by their (class,subclass,protocol) // 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); 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 // 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. // 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_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, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input-source", "system/services/input-source/input-source.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, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input-test", "system/services/input-test/input-test.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, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "args-echo", "system/services/args-echo/args-echo.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, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "process-test", "system/services/process-test/process-test.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, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "log-flush", "system/services/log-flush/log-flush.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 // 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: // (the container format is trivial, and Python sidesteps std API churn). Args:

View File

@ -197,21 +197,22 @@ system/ → /system danos's own internals (the self-representation)
services/ init/ vfs/ device-manager/ system servers → /system/services (vfs/ holds services/ init/ vfs/ device-manager/ system servers → /system/services (vfs/ holds
vfs.zig, vfs-test.zig, protocol.zig) vfs.zig, vfs-test.zig, protocol.zig)
library/ → /lib libraries, one sub-directory each library/ → /lib libraries, one sub-directory each
runtime/ the danos-native runtime — the stable application ABI runtime/ the danos-native runtime + file API (fs) — the stable application ABI
posix/ POSIX/C compatibility, layered over runtime
boot/ → /boot the loaders boot/ → /boot the loaders
tools/ test/ host-side build + QEMU test harness tools/ test/ host-side build + QEMU test harness
``` ```
A sub-project exposes its **public interface as a module**: `system/services/vfs/` owns 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 the VFS wire protocol (`protocol.zig`, the `vfs-protocol` module), which the runtime's
layer imports by name. `usb`/`block` drivers will expose their protocols the same way. 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 There is **no POSIX/C compatibility layer today**: danos programs do file I/O through the
verbatim (`stat`, `O_CREAT`, `fopen`, `errno`). Everywhere else follows the danos danos-native `runtime.fs` (open/read/write/list over the VFS). A hand-rolled POSIX shim
naming rule with no exception — see [coding-standards.md](coding-standards.md). The (`library/posix/`) was retired as premature — the real POSIX/C surface will come later
POSIX layer calls the runtime, never the kernel's system calls directly, so it never from the `std.os.danos` seam (and, eventually, musl) when danos becomes a Zig target (see
appears in the private-ABI path. [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 ## Source map
@ -235,8 +236,7 @@ appears in the private-ABI path.
| Framebuffer text console (mirrors to serial) | `system/kernel/console.zig` | | Framebuffer text console (mirrors to serial) | `system/kernel/console.zig` |
| In-kernel test cases | `system/kernel/tests.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/` | | 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/` | | danos-native runtime (`runtime`): syscall wrappers, heap, IPC, device access, the file API (`fs`) — the stable application ABI | `library/runtime/` |
| POSIX/C compatibility (`posix`): unistd, stdio — the one place POSIX names are allowed | `library/posix/` |
| System services (init, the VFS server + `protocol`, the device-manager) | `system/services/` | | 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/` | | 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` | | Build + `run-x86-64` (QEMU/OVMF) | `build.zig` |

View File

@ -65,15 +65,18 @@ Three, and only three.
`errno`, `O_CREAT`. We don't get to rename `fwrite` to `fileWrite` — it wouldn't be `errno`, `O_CREAT`. We don't get to rename `fwrite` to `fileWrite` — it wouldn't be
`fwrite` any more. `fwrite` any more.
**This exception is scoped to one place: `library/posix/`.** A file under **This exception is scoped to a file that *is* a foreign ABI, and nothing else.**
`library/posix/` *is* the foreign ABI, so it keeps the ABI's spellings — that is the danos has no such file today: the old `library/posix/` compatibility shim was retired
whole rule for that directory. **Everywhere else, Zig/danos naming applies with no once its callers moved to the danos-native `runtime.fs`, since a hand-rolled POSIX
POSIX exception**, so there is nothing to get wrong: if you're not in layer is premature until danos actually needs it (see
`library/posix/`, expand it. A concept POSIX also has gets a danos name outside that [zig-self-hosting.md](zig-self-hosting.md)). The exception will apply again to the
layer — the VFS wire protocol carries a `FileStatus`, not a `Stat`, and a `create` `std.os.danos` seam when danos becomes a real Zig target — that module *is* the C-ABI
flag, not `O_CREAT`; `library/posix/` is what maps `stat`→`status` and `system` interface, so it keeps `open`/`read`/`errno`/`O_CREAT`. **Everywhere else,
`O_CREAT`→`create` at the boundary. (The `syscall` *wrappers* elsewhere are not an Zig/danos naming applies with no exception**: a concept POSIX also has gets a danos
exception to this — they wrap the private danos ABI, so they use danos names.) 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, 2. **Zig idioms are spelled the way Zig spells them.** Three names are the language's,
not ours, and are left alone: not ours, and are left alone:

View File

@ -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 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 (`terminate`, `reload`, `exited`) and the IPC vocabulary the system already speaks
(`bind`, `subscribe`, `publish`, `endpoint`) — never `SIG*`, never a second word for (`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 a concept that already has one. Literal POSIX arrives later and lives elsewhere: the
**musl-based C layer** (growing out of library/posix) that wires C programs to the `std.os.danos` seam that makes danos a Zig target, and eventually a **musl-based C
danos runtime — musl's syscall surface retargeted at danos system calls and IPC layer** on the same native surface (see [zig-self-hosting.md](zig-self-hosting.md)) —
protocols (files onto the VFS protocol, `sigaction`/`wait` onto this lifecycle, musl's syscall surface retargeted at danos system calls and IPC protocols (files onto
sockets onto whatever networking becomes). Ported programs see POSIX; the system the VFS protocol, `sigaction`/`wait` onto this lifecycle, sockets onto whatever
underneath never does. networking becomes). Ported programs see POSIX; the system underneath never does.
## Why a standard vocabulary ## Why a standard vocabulary

View File

@ -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");

View File

@ -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.

View File

@ -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);
}

232
library/runtime/fs.zig Normal file
View File

@ -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;
}

View File

@ -46,6 +46,11 @@ pub const usb = @import("usb.zig");
/// usb-storage). See library/runtime/block.zig. /// usb-storage). See library/runtime/block.zig.
pub const block = @import("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;`. /// Re-exported so a user binary can `pub const panic = runtime.panic;`.
pub const panic = start.panic; pub const panic = start.panic;

View File

@ -6,6 +6,7 @@
const std = @import("std"); const std = @import("std");
const runtime = @import("runtime"); const runtime = @import("runtime");
const fs = runtime.fs;
fn writeLine(comptime fmt: []const u8, arguments: anytype) void { fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
var line: [128]u8 = undefined; 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 { pub fn main(init: runtime.process.Init) void {
_ = init; _ = init;
const unistd = @import("posix").unistd;
// Wait for /mnt/usb to be mounted the fat server races us at boot (it must // Wait for /mnt/usb to be mounted the fat server races us at boot (it must
// bring up the whole USB storage chain first). // bring up the whole USB storage chain first).
var dir: i32 = -1; var opened: ?fs.Directory = null;
var tries: u32 = 0; var tries: u32 = 0;
while (dir < 0 and tries < 1400) : (tries += 1) { while (opened == null and tries < 1400) : (tries += 1) {
dir = unistd.opendir("/mnt/usb"); opened = fs.openDirectory("/mnt/usb");
if (dir < 0) runtime.system.sleep(50); 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"); _ = runtime.system.write("fat-test: /mnt/usb never became available\n");
return; return;
} };
var count: u32 = 0; var count: u32 = 0;
var entry: unistd.DirEntry = .{}; var entry: fs.Entry = .{};
while (unistd.readdir(dir, &entry)) { while (dir.next(&entry)) {
writeLine("fat-test: entry '{s}' kind={d} size={d}\n", .{ entry.name(), entry.kind, entry.size }); writeLine("fat-test: entry '{s}' kind={d} size={d}\n", .{ entry.name(), @intFromEnum(entry.kind), entry.size });
count += 1; count += 1;
if (count > 32) break; if (count > 32) break;
} }
unistd.closedir(dir); dir.close();
writeLine("fat-test: listed {d} entries\n", .{count}); writeLine("fat-test: listed {d} entries\n", .{count});
// Read a known file off the boot volume through the mount (best effort): the // 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. // kernel image is an ELF, so its first bytes are the ELF magic.
const fd = unistd.open("/mnt/usb/system/kernel", 0); if (fs.open("/mnt/usb/system/kernel", .{})) |opened_file| {
if (fd >= 0) { var file = opened_file;
var magic: [4]u8 = undefined; var magic: [4]u8 = undefined;
const n = unistd.read(fd, &magic); const n = file.read(&magic) orelse 0;
unistd.close(fd); file.close();
if (n == 4 and magic[0] == 0x7F and magic[1] == 'E' and magic[2] == 'L' and magic[3] == 'F') { 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"); _ = runtime.system.write("fat-test: read /mnt/usb/system/kernel ELF magic ok\n");
} else { } else {

View File

@ -14,7 +14,6 @@ const runtime = @import("runtime");
const engine = @import("engine.zig"); const engine = @import("engine.zig");
const on_disk = @import("on-disk.zig"); const on_disk = @import("on-disk.zig");
const protocol = runtime.vfs_protocol; const protocol = runtime.vfs_protocol;
const unistd = @import("posix").unistd;
const dma = runtime.dma; const dma = runtime.dma;
fn writeLine(comptime fmt: []const u8, arguments: anytype) void { 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. // comes up). From here the VFS routes /mnt/usb/... to this server.
var tries: u32 = 0; var tries: u32 = 0;
while (tries < 100) : (tries += 1) { 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}); writeLine("/system/services/fat: mounted {s}\n", .{mount_point});
return true; return true;
} }

View File

@ -19,7 +19,6 @@
const std = @import("std"); const std = @import("std");
const runtime = @import("runtime"); const runtime = @import("runtime");
const unistd = @import("posix").unistd;
const power = runtime.power_protocol; const power = runtime.power_protocol;
/// Where the kernel boot log is persisted on the USB FAT volume an 8.3 name at /// 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 /// USB volume is not mounted, the open fails and it does nothing. Must run while
/// the storage services are still alive (see shutDown). /// the storage services are still alive (see shutDown).
fn flushKernelLog() void { fn flushKernelLog() void {
const fd = unistd.open(log_path, unistd.O_CREAT); var file = runtime.fs.open(log_path, .{ .create = true }) orelse return; // no USB volume mounted
if (fd < 0) return; // no USB volume mounted nothing to persist to defer file.close();
defer unistd.close(fd);
var chunk: [4096]u8 = undefined; var chunk: [4096]u8 = undefined;
var offset: usize = 0; var offset: usize = 0;
while (true) { while (true) {
const got = runtime.system.klogRead(offset, &chunk); const got = runtime.system.klogRead(offset, &chunk);
if (got == 0) break; // reached the end of the accumulated log 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; offset += got;
} }
var line: [96]u8 = undefined; var line: [96]u8 = undefined;

View File

@ -16,19 +16,19 @@
const std = @import("std"); const std = @import("std");
const runtime = @import("runtime"); const runtime = @import("runtime");
const unistd = @import("posix").unistd; const fs = runtime.fs;
const log_path = "/mnt/usb/DANOS.LOG"; const log_path = "/mnt/usb/DANOS.LOG";
/// Copy the whole kernel log to the open fd, looping klog_read -> write until the /// Copy the whole kernel log to the open file, looping klog_read -> write until
/// log is exhausted. Returns the number of bytes written. /// the log is exhausted. Returns the number of bytes written.
fn drainKernelLog(fd: i32) usize { fn drainKernelLog(file: *fs.File) usize {
var chunk: [4096]u8 = undefined; var chunk: [4096]u8 = undefined;
var offset: usize = 0; var offset: usize = 0;
while (true) { while (true) {
const got = runtime.system.klogRead(offset, &chunk); const got = runtime.system.klogRead(offset, &chunk);
if (got == 0) break; // reached the end of the accumulated log 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; offset += got;
} }
return offset; 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 // 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 // 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. // appears no volume, or the no-VFS ramdisk sweep give up silently.
var dir: i32 = -1; var ready = false;
var tries: u32 = 0; var tries: u32 = 0;
while (dir < 0 and tries < 1400) : (tries += 1) { while (tries < 1400) : (tries += 1) {
dir = unistd.opendir("/mnt/usb"); if (fs.openDirectory("/mnt/usb")) |directory| {
if (dir < 0) runtime.system.sleep(50); 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 if (!ready) return; // /mnt/usb never became available nothing to persist to
unistd.closedir(dir);
const fd = unistd.open(log_path, unistd.O_CREAT); var file = fs.open(log_path, .{ .create = true }) orelse return; // could not create the file
if (fd < 0) return; // could not create the file exit quietly const written = drainKernelLog(&file);
const written = drainKernelLog(fd); file.close();
unistd.close(fd);
var line: [96]u8 = undefined; 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); _ = runtime.system.write(std.fmt.bufPrint(&line, "log-flush: wrote {d} bytes to {s}\n", .{ written, log_path }) catch return);

View File

@ -4,12 +4,11 @@
//! header followed by an inline payload (read bytes, or a FileStatus). Everything fits //! header followed by an inline payload (read bytes, or a FileStatus). Everything fits
//! in one IPC message (<= ipc MESSAGE_MAXIMUM = 256 bytes). //! in one IPC message (<= ipc MESSAGE_MAXIMUM = 256 bytes).
//! //!
//! This is a danos-native contract, so it uses danos names throughout the POSIX //! This is a danos-native contract, so it uses danos names throughout. The client
//! spellings (`stat`, `O_CREAT`, ...) live only in the POSIX layer //! side is `runtime.fs` (library/runtime/fs.zig), which programs use directly.
//! (library/posix/unistd.zig), which translates to these.
//! //!
//! This is user-space only the kernel knows nothing of files or paths; it only moves the bytes. //! 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) { pub const Operation = enum(u32) {
open, // open(path) -> node id open, // open(path) -> node id

View File

@ -1,26 +1,26 @@
//! /system/services/vfs/vfs-test a client that proves the VFS round trip end to end: open a //! /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 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. //! on failure it reports what went wrong. Shipped in the initial_ramdisk alongside vfs.
const std = @import("std"); const std = @import("std");
const runtime = @import("runtime"); const runtime = @import("runtime");
const fs = runtime.fs;
pub fn main(init: runtime.process.Init) void { pub fn main(init: runtime.process.Init) void {
const u = @import("posix").unistd;
const payload = "hello-vfs"; const payload = "hello-vfs";
// The "park" role (the vfs-client-death test): open a file, then hold the // 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 // handle forever without closing the kill and the VFS's release-on-death
// are the point. // are the point.
if (init.arguments.count > 1) { if (init.arguments.count > 1) {
var fd: i32 = -1; var parked: ?fs.File = null;
var tries: u32 = 0; var tries: u32 = 0;
while (fd < 0 and tries < 200) : (tries += 1) { while (parked == null and tries < 200) : (tries += 1) {
fd = u.open("parked", u.O_CREAT); parked = fs.open("parked", .{ .create = true });
if (fd < 0) runtime.system.sleep(20); if (parked == null) runtime.system.sleep(20);
} }
if (fd < 0) { if (parked == null) {
_ = runtime.system.write("vfstest: park open failed\n"); _ = runtime.system.write("vfstest: park open failed\n");
return; 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. // 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; var tries: u32 = 0;
while (fd < 0 and tries < 200) : (tries += 1) { while (opened == null and tries < 200) : (tries += 1) {
fd = u.open("greeting", u.O_CREAT); opened = fs.open("greeting", .{ .create = true });
if (fd < 0) runtime.system.sleep(20); if (opened == null) runtime.system.sleep(20);
} }
if (fd < 0) { var greeting = opened orelse {
_ = runtime.system.write("vfstest: open failed\n"); _ = runtime.system.write("vfstest: open failed\n");
return; 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"); _ = runtime.system.write("vfstest: write failed\n");
return; return;
} }
_ = u.lseek(fd, 0, u.SEEK_SET); greeting.seekTo(0);
var buffer: [32]u8 = undefined; var buffer: [32]u8 = undefined;
const n = u.read(fd, &buffer); const n = greeting.read(&buffer) orelse 0;
u.close(fd); 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) { while (true) {
_ = runtime.system.write("vfstest: ok\n"); _ = runtime.system.write("vfstest: ok\n");
runtime.system.sleep(1000); runtime.system.sleep(1000);