SwiftOS — a from-scratch kernel and userland in Embedded Swift

I've always wanted to really understand how an operating system works underneath — not the textbook block diagram, but the actual machinery: how a core comes up from reset, how address spaces get isolated, how a binary becomes a running process. I decided the most honest way to learn it was to build one. And I wanted to find out how far Embedded Swift could go if you took it all the way down to the metal.

The result is SwiftOS: a small but real operating system written almost entirely in Embedded Swift for 64-bit ARM. It boots on QEMU virt, and — the part I'm still a little amazed by — on a real Hetzner Cloud ARM VM, where it's currently serving its own website.

The Embedded Swift side (the part I think this forum will care about)

  • Toolchain: the swift.org 6.3.2-RELEASE toolchain, which ships an embedded stdlib for the aarch64-none-none-elf target — exactly what I build against.
  • Build flags, roughly:
    -target aarch64-none-none-elf -enable-experimental-feature Embedded -wmo -parse-as-library -Osize -Xllvm -mattr=+strict-align,-neon -Xfrontend -function-sections -import-objc-header kernel/arch/aarch64/io.h
  • Freestanding: no Foundation, no full stdlib. The kernel is value types + Unsafe* pointers at the lowest level, ~Copyable structs with deinit for resource ownership, and classes used sparingly and only after the heap is up (ARC isn't free).
  • Volatile MMIO goes through a tiny C bridge imported via -import-objc-header (io.h); boot and exception vectors are assembly. Almost everything above that is Swift.
  • A few things that bit me and might save someone else time:
    • You need ld.lld, not GNU ld — Embedded Swift's protected empty Array/String singletons land in a section GNU ld mishandles (and it clears a spurious RWX-segment warning too).
    • String/Unicode pulls in libswiftUnicodeDataTables.a from the toolchain — you have to link it explicitly.
    • print() and String output lower to putchar, so the first thing the userland needs is a one-line putchar shim over the UART/syscall.
    • Embedded heap allocations want 16-byte alignment — worth knowing when you're writing the first allocator over sbrk.

What the OS actually does

  • Real MMU isolation — one address space per process, capability-based handles instead of ambient authority.
  • A native userland written in Swift — its own coreutils (ls, ps, top, a calculator/REPL, …), console-login, and sshd, all on our own svc syscall ABI.
  • An in-kernel TCP/IP stack — DHCP, TCP, UDP, DNS, HTTP, and TLS.
  • A three-tier filesystem — an immutable, signed, read-only base image; a RAM tmpfs scratch tier; and a persistent writable /data tier with honest fsync/fdatasync (durable enough to back SQLite).
  • SMP — it schedules across multiple cores (tested at -smp 4), with cross-CPU TLB shootdown and spinlock-protected kernel state.
  • No Linux ABI, static linking only, no dynamic loader. Our own POSIX-like syscall surface; everything gets recompiled. There's a newlib port so "real" C/C++ software can link.

Getting real software to run

  • nginx 1.30.2, statically linked against newlib + OpenSSL, serving HTTPS. This is what's serving the site above.
  • Node.js 24.16.0 running on top of V8 in --v8-lite-mode (jitless — which neatly sidesteps W^X on a kernel that doesn't hand out RWX pages). node --version and node -e "console.log(6*7)"42 both work. It was a long road: a full aarch64-elf GCC 16.1 toolchain with libstdc++ built from source to satisfy V8's C++ runtime. (npm packaging is the next step.)

Running on real hardware

QEMU is forgiving; real hardware is not. Bringing SwiftOS up on a bare Hetzner Cloud ARM VM meant going from device tree + virtio-mmio + GICv2 to ACPI firmware tables (RSDP→MADT/MCFG/SPCR/GTDT, no DT fallback), GICv3, PCIe ECAM enumeration + virtio-pci, and DHCP over virtio-net-pci — then booting headless straight to sshd. That surfaced four bugs that only show up on real hardware (my favorite: PCIe ECAM and the 64-bit virtio window have to be mirrored into every process's page tables, or the kernel can touch the NIC but a userland process like sshd faults the moment it does TX/RX). All of it is gated by a regression test that boots the exact Hetzner topology.

Caveats, because this audience will (rightly) ask

It's minimal and there are rough edges. It started single-core; SMP is recent. Some things stay in C/asm on purpose — third-party code (busybox, newlib), the MMIO/boot/syscall bridges, and a couple of measured toolchain-limitation cases — but the design rule is Swift by default, C only with a documented reason.

I learned an enormous amount doing this — about ARM, virtual memory, drivers, the network stack, and how much "obvious" behavior we take for granted in mature kernels. I'd love feedback from people who actually know this domain: anything I've clearly got wrong, things you'd design differently, or Embedded Swift corners I should be using better. And if there's interest, I'm happy to write up specific parts in more depth — the allocator, the capability model, the SMP bring-up, or the V8-on-a-hobby-kernel saga.

20 Likes